Bug 468808 - "Tabs can't be reordered". r=asuth
authorThomas Schmid <schmid-thomas@gmx.net>
Wed, 16 Feb 2011 14:13:00 -0800
changeset 7250 491fbcf7a1b443860503f245f68941d8f7a1306b
parent 7249 5c62105268eb8ee18537521905ba5208a677b0de
child 7251 33614283e6756b057b026a322b34dfbaa12db5e2
push idunknown
push userunknown
push dateunknown
reviewersasuth
bugs468808
Bug 468808 - "Tabs can't be reordered". r=asuth
mail/base/content/messageDisplay.js
mail/base/content/msgMail3PaneWindow.js
mail/base/content/tabmail.xml
mail/locales/en-US/chrome/messenger/messenger.dtd
mail/test/mozmill/mozmilltests.list
mail/test/mozmill/tabmail/test-tabmail-dragndrop.js
--- a/mail/base/content/messageDisplay.js
+++ b/mail/base/content/messageDisplay.js
@@ -528,17 +528,17 @@ MessageTabDisplayWidget.prototype = {
   onSelectedMessagesChanged:
       function MessageTabDisplayWidget_onSelectedMessagesChanged() {
     // Look at the number of messages left in the db view. If there aren't any,
     // close the tab.
     if (this.folderDisplay.view.dbView.rowCount == 0) {
       if (!this.closing) {
         this.closing = true;
         document.getElementById('tabmail').closeTab(
-            this.folderDisplay._tabInfo);
+            this.folderDisplay._tabInfo, true);
       }
       return true;
     }
     else {
       if (!this.closing)
         document.getElementById('tabmail').setTabTitle(
             this.folderDisplay._tabInfo);
 
@@ -550,29 +550,31 @@ MessageTabDisplayWidget.prototype = {
       this.singleMessageDisplay = true;
       return false;
     }
   },
 
   onMessagesRemoved: function MessageTabDisplayWidget_onMessagesRemoved() {
     if (this.folderDisplay.treeSelection.count == 0 &&
         pref.getBoolPref("mail.close_message_window.on_delete")) {
-      document.getElementById("tabmail").closeTab(this.folderDisplay._tabInfo);
+      document.getElementById("tabmail").closeTab(this.folderDisplay._tabInfo,
+                                                  true);
       return true;
     }
   },
 
   /**
    * A message tab should never ever be blank.  Close the tab if we become
    *  blank.
    */
   clearDisplay: function MessageTabDisplayWidget_clearDisplay() {
     if (!this.closing) {
       this.closing = true;
-      document.getElementById('tabmail').closeTab(this.folderDisplay._tabInfo);
+      document.getElementById('tabmail').closeTab(this.folderDisplay._tabInfo,
+                                                  true);
     }
   }
 };
 
 /**
  * The search dialog has no message preview pane, and so wants a message
  *  display widget that is never visible.  No one other than the search
  *  dialog should use this because the search dialog is bad UI.
--- a/mail/base/content/msgMail3PaneWindow.js
+++ b/mail/base/content/msgMail3PaneWindow.js
@@ -499,16 +499,17 @@ function LoadPostAccountWizard()
   var toolbarset = document.getElementById('customToolbars');
   toolbox.toolbarset = toolbarset;
 
   // XXX Do not select the folder until the window displays or the threadpane
   //  will be at minimum size.  We used to have
   //  gFolderDisplay.ensureRowIsVisible use settimeout itself to defer that
   //  calculation, but that was ugly.  Also, in theory we will open the window
   //  faster if we let the event loop start doing things sooner.
+
   if (startMsgHdr)
     window.setTimeout(loadStartMsgHdr, 0, startMsgHdr);
   else
     window.setTimeout(loadStartFolder, 0, startFolderURI);
 }
 
 function HandleAppCommandEvent(evt)
 {
@@ -603,56 +604,112 @@ function getWindowStateForSessionPersist
  * @param aDontRestoreFirstTab If this is true, the first tab will not be
  *                             restored, and will continue to retain focus at
  *                             the end. This is needed if the window was opened
  *                             with a folder or a message as an argument.
  *
  * @return true if the restoration was successful, false otherwise.
  */
 function atStartupRestoreTabs(aDontRestoreFirstTab) {
+
   let state = sessionStoreManager.loadingWindow(window);
+
   if (state) {
     let tabsState = state.tabs;
     let tabmail = document.getElementById("tabmail");
     tabmail.restoreTabs(tabsState, aDontRestoreFirstTab);
-    return true;
   }
 
-  return false;
+  // it's now safe to load extra Tabs.
+  setTimeout(loadExtraTabs, 0);
+
+  return state ? true : false;
 }
 
+/**
+ * Loads and restores tabs upon opening a window by evaluating window.arguments[1].
+ *
+ * The type of the object is specified by it's action property. It can be
+ * either "restore" or "open". "restore" invokes tabmail.restoreTab() for each
+ * item in the tabs array. While "open" invokes tabmail.openTab() for each item.
+ *
+ * In case a tab can't be restored it will fail silently
+ *
+ * the object need at least the following properties:
+ *
+ * {
+ *   action = "restore" | "open"
+ *   tabs = [];
+ * }
+ *
+ */
 function loadExtraTabs()
 {
-  if ("arguments" in window && window.arguments.length >= 2) {
-    if (window.arguments[1] && (typeof window.arguments[1] == "object") &&
-        ("tabType" in window.arguments[1])) {
-      document.getElementById('tabmail').openTab(window.arguments[1].tabType, window.arguments[1].tabParams);
-    }
+
+  if (!("arguments" in window) || window.arguments.length < 2)
+    return;
+
+  let tab = window.arguments[1];
+  if ((!tab) || (typeof tab != "object"))
+    return;
+
+  let tabmail =  document.getElementById("tabmail");
+
+  // we got no action, so suppose its "legacy" code
+  if (!("action" in tab)) {
+
+    if ("tabType" in tab)
+      tabmail.openTab(tab.tabType, tab.tabParams);
+
+    return;
   }
+
+  if (!("tabs" in tab))
+    return;
+
+  // this is used if a tab is detached to a new window.
+  if (tab.action == "restore") {
+
+    for (let i = 0; i < tab.tabs.length; i++)
+      tabmail.restoreTab(tab.tabs[i]);
+
+    // we currently do not support opening in background or opening a
+    // special position. So select the last tab opened.
+    tabmail.switchToTab(tabmail.tabInfo[tabmail.tabInfo.length-1])
+
+    return;
+  }
+
+  if (tab.action == "open") {
+
+    for (let i = 0; i < tab.tabs.length; i++)
+      if("tabType" in tabs.tab[i])
+        tabmail.openTab(tabs.tab[i].tabType,tabs.tab[i].tabParams);
+
+    return;
+  }
+
 }
 
 /**
  * Loads the given message header at window open. Exactly one out of this and
  * |loadStartFolder| should be called.
  *
  * @param aStartMsgHdr The message header to load at window open
  */
 function loadStartMsgHdr(aStartMsgHdr)
 {
-  setTimeout(loadExtraTabs, 0);
-
   // We'll just clobber the default tab
   atStartupRestoreTabs(true);
 
   MsgDisplayMessageInFolderTab(aStartMsgHdr);
 }
 
 function loadStartFolder(initialUri)
 {
-  setTimeout(loadExtraTabs, 0);
     var defaultServer = null;
     var startFolder;
     var isLoginAtStartUpEnabled = false;
 
     // If a URI was explicitly specified, we'll just clobber the default tab
     let loadFolder = !atStartupRestoreTabs(!!initialUri);
     if (initialUri)
       loadFolder = true;
--- a/mail/base/content/tabmail.xml
+++ b/mail/base/content/tabmail.xml
@@ -18,16 +18,17 @@
   -   David Bienvenu <bienvenu@nventure.com>.
   - Portions created by the Initial Developer are Copyright (C) 2007
   - the Initial Developer. All Rights Reserved.
   -
   - Contributor(s):
   -  Scott MacGregor <mscott@mozilla.org>
   -  Andrew Sutherland <asutherland@asutherland.org>
   -  Magnus Melin <mkmelin+mozilla@iki.fi>
+  -  Thomas Schmid <schmid-thomas@gmx.net>
   -
   - 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
@@ -67,38 +68,59 @@
     -
     - From a javascript perspective, there are three types of code that we
     -  expect to interact with:
     - 1) Code that wants to open new tabs.
     - 2) Code that wants to contribute one or more varieties of tabs.
     - 3) Code that wants to monitor to know when the active tab changes.
     -
     - Consumer code should use the following methods:
-    - * openTab(aTabModeName, aArgs): Open a tab of the given "mode",
-    -   passing the provided arguments as an object.  The tab type author
-    -   should tell you the modes they implement and the required/optional
-    -   arguments.
-    -   One of the arguments you can pass is "background": if this is true,
-    -   the tab will be loaded in the background.
-    - * closeTab(aOptionalTabIndexInfoOrTabNode): If no argument is provided,
-    -   the current tab is closed.  If an argument is provided, it can be
-    -   a tab index, a tab info object, or the tabmail-tab bound element
-    -   that _is_ the tab.  Some tabs cannot be closed, in which case this
-    -   will do nothing.
-    - * switchToTab(aTabIndexInfoOrTabNode): Switch to the tab by providing
-    -   a tab index, tab info object, or tab node (tabmail-tab bound
-    -   element.)  You can also just poke at tabmail.tabContainer and its
-    -   selectedIndex and selectedItem properties.
+    - * openTab(aTabModeName, aArgs)
+    -     Open a tab of the given "mode", passing the provided arguments as an 
+    -     object. The tab type author should tell you the modes they implement 
+    -     and the required/optional arguments.
+    -     One of the arguments you can pass is "background": if this is true,
+    -     the tab will be loaded in the background.
+    - * closeTab(aOptionalTabIndexInfoOrTabNode,aNoUndo): 
+    -     If no argument is provided, the current tab is closed. The first 
+    -     argument specifies a specific tab to be closed. It can be a tab index,
+    -     a tab info object, or a tab's DOM element. In case the second 
+    -     argument is true, the closed tab can't be restored by calling
+    -     undoCloseTab(). 
+    -     Please note, some tabs cannot be closed. Trying to close such tab, 
+    -     willd fail silently.
+    - * undoCloseTab(): 
+    -     Restores the most recent tab closed by the user. 
+    - * switchToTab(aTabIndexInfoOrTabNode): 
+    -     Switch to the tab by providing a tab index, tab info object, or tab 
+    -     node (tabmail-tab bound element.) You can also just poke at 
+    -     tabmail.tabContainer and its selectedIndex and selectedItem
+    -     properties.
     - * setTabIcon(aTabNodeorInfo, aIcon): Sets the tab icon to the specified
-    -   url or removes the icon if no url is specified. Note that this may
-    -   overrride css provided images.
+    -     url or removes the icon if no url is specified. Note that this may
+    -     overrride css provided images.
+    - * replaceTabWithWindow(aTab):
+    -     Detaches a tab from this tabbar to new window. The argument "aTab" is 
+    -     required and can be a tab index, a tab info object or a tabs's 
+    -     DOM element. Calling this method works only for tabs implementing
+    -     session restore.
+    - * moveTabTo(aTab,aIndex):
+    -     moves the given tab to the given Index. The first argument can be
+    -     a tab index, a tab info object or a tab's DOM element. The second
+    -     argument specifies the tabs new absolute position within the tabbar.
+    - 
     - Less-friendly consumer methods:
-    - * removeCurrentTab(): Close the current tab.
-    - * removeTabByNode(aTabElement): Close the tab whose tabmail-tab bound
-    -   element is passed in.
+    - * persistTab(tab):
+    -     serializes a tab into an object, by passing  a tab info object as 
+    -     argument. It is used for session restore and moving tabs between 
+    -     windows. Returns null in case persist failes.
+    - * removeCurrentTab():
+    -     Close the current tab.
+    - * removeTabByNode(aTabElement):
+    -     Close the tab whose tabmail-tab bound element is passed in.
     - Changing the currently displayed tab is accomplished by changing
     -  tabmail.tabContainer's selectedIndex or selectedItem property.
     -
     - Code that lives in a tab should use the following methods:
     - * setTabTitle([aOptionalTabInfo]): Tells us that the title of the current
     -   tab (if no argument is provided) or provided tab needs to be updated.
     -   This will result in a call to the tab mode's logic to update the title.
     -   In the event this is not for the current tab, the caller is responsible
@@ -270,38 +292,52 @@
     <resources>
       <stylesheet src="chrome://messenger/content/tabmail.css"/>
       <stylesheet src="chrome://messenger/skin/tabmail.css"/>
     </resources>
     <content>
       <xul:tabbox anonid="tabbox" flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
                   onselect="if (!('updateCurrentTab' in this.parentNode) || event.target.localName != 'tabs')
                             return; this.parentNode.updateCurrentTab();">
-        <xul:hbox class="tab-drop-indicator-bar" collapsed="true">
-          <xul:hbox class="tab-drop-indicator" mousethrough="always"/>
-        </xul:hbox>
+        <xul:hbox align="start">
+          <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/>
+        </xul:hbox>        
+        
         <xul:hbox class="tabmail-strip" collapsed="false" tooltip="_child" context="_child"
-                  anonid="strip"
-                  ondraggesture="nsDragAndDrop.startDrag(event, this.parentNode.parentNode); event.stopPropagation();"
-                  ondragover="nsDragAndDrop.dragOver(event, this.parentNode.parentNode); event.stopPropagation();"
-                  ondragdrop="nsDragAndDrop.drop(event, this.parentNode.parentNode); event.stopPropagation();"
-                  ondragexit="nsDragAndDrop.dragExit(event, this.parentNode.parentNode); event.stopPropagation();">
+                  anonid="strip">
           <xul:tooltip onpopupshowing="return CreateToolbarTooltip(document, event);"/>
           <xul:menupopup anonid="tabContextMenu"
                 onpopupshowing="
-                  var tabmail = this.parentNode.parentNode.parentNode;
+                  var tabmail = document.getElementById('tabmail');
                   return tabmail.onTabContextMenuShowing(document.popupNode);">
-            <xul:menuitem label="&closeTabCmd.label;" accesskey="&closeTabCmd.accesskey;"
+            
+            <xul:menuitem label="&moveToNewWindow.label;" 
+                          accesskey="&moveToNewWindow.accesskey;"
+                          anonid="openTabInWindow"
+                          oncommand="document.getElementById('tabmail').replaceTabWithWindow(document.popupNode);"/>
+                                     
+            <xul:menuseparator />
+            
+            <xul:menuitem label="&closeOtherTabsCmd2.label;" 
+                          accesskey="&closeOtherTabsCmd2.accesskey;"
+                          anonid="closeOtherTabs"
+                          oncommand="document.getElementById('tabmail').closeOtherTabs(document.popupNode);"/>
+                                                 
+            <xul:menuseparator />
+            
+            <xul:menuitem label="&undoCloseTabCmd.label;"
+                          accesskey="&undoCloseTabCmd.accesskey;"
+                          anonid="undoCloseTab"
+                          oncommand="document.getElementById('tabmail').undoCloseTab();"/>
+                                                                         
+            <xul:menuitem label="&closeTabCmd2.label;" 
+                          accesskey="&closeTabCmd2.accesskey;"
                           anonid="closeTab"
-                          oncommand="var tabmail = this.parentNode.parentNode.parentNode.parentNode;
-                                     tabmail.removeTabByNode(document.popupNode);"/>
-            <xul:menuitem label="&closeOtherTabsCmd.label;" accesskey="&closeOtherTabsCmd.accesskey;"
-                          anonid="closeOtherTabs"
-                          oncommand="var tabmail = this.parentNode.parentNode.parentNode.parentNode;
-                                     tabmail.closeOtherTabs(document.popupNode);"/>
+                          oncommand="document.getElementById('tabmail').closeTab(document.popupNode);"/>
+                                                                          
           </xul:menupopup>
           <xul:tabs class="tabmail-tabs" flex="1"
                     anonid="tabcontainer"
                     setfocus="false"
                     onclick="this.parentNode.parentNode.parentNode.onTabClick(event);">
             <xul:tab selected="true" validate="never" type="folder"
                      maxwidth="250" width="0" minwidth="100" flex="100"
                      class="tabmail-tab" crop="end"/>
@@ -312,16 +348,17 @@
         <children includes="tabpanels"/>
       </xul:tabbox>
     </content>
 
     <implementation implements="nsIController">
       <constructor>
         window.controllers.insertControllerAt(0, this);
         this._restoringTabState = null;
+        this._tabUndo = [];
       </constructor>
       <destructor>
         window.controllers.removeController(this);
       </destructor>
       <field name="currentTabInfo">
         null
       </field>
       <!-- Temporary field that only has a non-null value during a call to
@@ -634,30 +671,62 @@
           if (tabToConsider && tabToConsider.mode == aTabMode)
             return tabToConsider;
           else if (aTabMode.tabs.length)
             return aTabMode.tabs[0];
           else
             return null;
         ]]></body>
       </method>
+      <method name="undoCloseTab">
+        <body><![CDATA[
+          let history = this._tabUndo.pop();
+          
+          if (!history.tab)
+            return;
+
+          if (!this.restoreTab(JSON.parse(history.tab)))
+            return;
+          
+          let idx = Math.min(history.idx,this.tabInfo.length);          
+          let tab = this.tabContainer.childNodes[this.tabInfo.length - 1];
+          this.moveTabTo(tab, idx);
+          
+          this.switchToTab(tab);
+          
+        ]]></body>
+      </method>
       <method name="closeTab">
         <parameter name="aOptTabIndexNodeOrInfo"/>
-        <body>
-          <![CDATA[
+        <parameter name="aNoUndo" />
+        <body><![CDATA[
+        
             let [iTab, tab, tabNode] =
               this._getTabContextForTabbyThing(aOptTabIndexNodeOrInfo, true);
 
             if (!tab.canClose)
               return;
 
             for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
               if ("onTabClosing" in tabMonitor)
                 tabMonitor.onTabClosing(tab);
             }
+
+            if (!aNoUndo) {
+              // Allow user to undo accidentially closed tabs
+              let session = this.persistTab(tab);
+
+              if (session) {
+                this._tabUndo.push(
+                    { tab: JSON.stringify(session), idx: iTab } );
+
+                if (this._tabUndo.length > 10)
+                  this._tabUndo.shift();    
+              }
+            }
             
             let closeFunc = tab.mode.closeTab || tab.mode.tabType.closeTab;
             closeFunc.call(tab.mode.tabType, tab);
              
             this.tabInfo.splice(iTab, 1);
             tab.mode.tabs.splice(tab.mode.tabs.indexOf(tab), 1);
             this.tabContainer.removeChild(tabNode);
             if (this.tabContainer.selectedIndex == -1)
@@ -704,16 +773,154 @@
             for (let i = this.tabInfo.length - 1; i >= 0; i--) {
               let tab = this.tabInfo[i];
               if ((tab != thisTab) && tab.canClose)
                 this.closeTab(tab);
             }
           ]]>
         </body>
       </method>
+      <method name="replaceTabWithWindow">
+        <parameter name="aTab"/>
+        <body>
+          <![CDATA[
+            if (this.tabInfo.length <= 1)
+              return null;
+
+            let tab = this._getTabContextForTabbyThing(aTab, false)[1];
+                                                         
+            if (!tab.canClose)
+              return null;
+            
+            // We use Json and session restore transfer the tab to the new window.
+            tab = this.persistTab(tab);
+            if (!tab)
+              return null;
+        
+            // Converting to JSON and back again creates clean javascript 
+            // object with absolutely no references to our current window.
+            let tab = JSON.parse(JSON.stringify(tab));            
+            
+            this.closeTab(aTab,true);
+                      
+            return window.openDialog("chrome://messenger/content/", "_blank",
+               "chrome,dialog=no,all", null,  
+               { action : "restore", tabs: [tab] } ).focus();
+          ]]>
+        </body>
+      </method>      
+      <method name="moveTabTo">
+        <parameter name="aTab"/>
+        <parameter name="aIndex"/>
+        <body><![CDATA[
+          
+          if ((!aTab) || (aTab.tagName != "tab"))
+            return -1;
+
+          let oldIdx = this.tabContainer.getIndexOfItem(aTab);
+          if (oldIdx < 0)
+            return -1;
+          
+          if (oldIdx == aIndex)
+            return -1;
+            
+          // Cache the old tabInfo
+          let tab = this.tabInfo[oldIdx];          
+          
+          if (!tab)
+            return -1;
+                     
+          // remove the entries form tabInfo, tabMode and tabStrip
+          this.tabInfo.splice(oldIdx, 1);
+          tab.mode.tabs.splice(tab.mode.tabs.indexOf(tab), 1);
+          this.tabContainer.removeChild(aTab);
+          
+
+          // as we removed items, we might need to update indices
+          if (oldIdx < aIndex)
+            aIndex --;
+                       
+          // Read it into tabInfo and tabStrip.
+          this.tabInfo.splice(aIndex, 0, tab);
+          this.tabContainer.insertBefore(aTab, this.tabContainer.childNodes[aIndex]);
+          
+          // Now it's getting a bit ugly, as tabModes stores redundant
+          // information we need to get it in sync with tabInfo.
+          //              
+          // As tabModes.tabs is a subset of tabInfo, every tab can be mapped 
+          // to a tabInfo index. So we check for each tab in tabModes if it is 
+          // directly in front of our moved tab. We do this by looking up the 
+          // index in tabInfo and compare it with the moved tab's index. If we
+          // found our tab, we insert the moved tab directly behind into tabModes
+          
+          // In case find no tab we simply append it 
+          let modeIdx = tab.mode.tabs.length+1;
+          
+          for (let i = 0; i < tab.mode.tabs.length; i++) {
+                    
+            if (this.tabInfo.indexOf(tab.mode.tabs[i]) < aIndex)
+              continue;
+              
+            modeIdx = i;
+            break;
+          }
+                            
+          tab.mode.tabs.splice(modeIdx, 0, tab);          
+          
+          return aIndex;
+         ]]>
+         </body>
+       </method>
+       <method name="persistTab">
+         <parameter name="tab"/>        
+         <body><![CDATA[              
+           /* Returns null in case persist fails */
+           
+           let persistFunc = tab.mode.persistTab || tab.mode.tabType.persistTab;
+        
+           // if we can't restore the tab we can't move it
+           if (!persistFunc)
+             return null; 
+                 
+           //  If there is a non-null tab-state, then persisting succeeded and
+           //  we should store it.  We store the tab's persisted state in its
+           //  own distinct object rather than mixing things up in a dictionary
+           //  to avoid bugs and because we may eventually let extensions store
+           //  per-tab information in the persisted state.
+           
+           let tabState;
+           // Wrap this in an exception handler so that if the persistence
+           // logic fails, things like tab closure still run to completion.
+           try {
+             tabState = persistFunc.call(tab.mode.tabType, tab);
+           }
+           catch(ex) {
+             // Report this so that our unit testing framework sees this
+             // error and (extension) developers likewise can see when their
+             // extensions are ill-behaved.
+             Components.utils.reportError(ex);
+           }
+
+           if (!tabState)
+             return null;
+             
+           let ext = {};
+
+           for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
+             if ("onTabPersist" in tabMonitor) {
+               let monState = tabMonitor.onTabPersist(tab);
+               if (monState !== null)
+                 ext[tabMonitor.monitorName] = monState;
+             }
+           }
+           
+           return {mode: tab.mode.name, state: tabState, ext: ext}           
+        ]]></body>
+      </method>
+      
       <method name="persistTabs">
         <body><![CDATA[
           /**
            * Persist the state of all tab modes implementing persistTab methods
            *  to a JSON-serializable object representation and return it.  Call
            *  restoreTabs with the result to restore the tab state.
            * Calling this method should have no side effects; tabs will not be
            *  closed, displays will not change, etc.  This means the method is
@@ -723,94 +930,88 @@
            * @return {Object} The persisted tab states.
            */
           let state = {
             // Explicitly specify a revision so we don't wish we had later.
             rev: 0,
             // If our currently selected tab gets persisted, we will update this
             selectedIndex: null,
           };
+          
           let tabs = state.tabs = [];
 
           for each (let [iTab, tab] in Iterator(this.tabInfo)) {
-            let persistFunc = tab.mode.persistTab ||
-                              tab.mode.tabType.persistTab;
-            if (!persistFunc)
+          
+            let persistTab = this.persistTab(tab);
+            
+            if (!persistTab)
               continue;
-            let tabState = persistFunc.call(tab.mode.tabType, tab);
-            // If there is a non-null tab-state, then persisting succeeded and
-            //  we should store it.  We store the tab's persisted state in its
-            //  own distinct object rather than mixing things up in a dictionary
-            //  to avoid bugs and because we may eventually let extensions store
-            //  per-tab information in the persisted state.
-            if (tabState != null) {
-              let ext = {};
-
-              for each (let [i, tabMonitor] in Iterator(this.tabMonitors)) {
-                if ("onTabPersist" in tabMonitor) {
-                  let monState = tabMonitor.onTabPersist(tab);
-                  if (monState !== null)
-                    ext[tabMonitor.monitorName] = monState;
-                }
-              }
-
-              tabs.push({
-                mode: tab.mode.name,
-                state: tabState,
-                ext: ext,
-              });
-              // Mark this persisted tab as selected
-              if (iTab == this.tabContainer.selectedIndex)
-                state.selectedIndex = tabs.length - 1;
-            }
+            
+            tabs.push(persistTab);
+            
+            // Mark this persisted tab as selected
+            if (iTab == this.tabContainer.selectedIndex)
+              state.selectedIndex = tabs.length - 1;
           }
 
           return state;
         ]]></body>
-      </method>
+      </method>      
+      <method name="restoreTab">
+        <parameter name="aState"/>
+        <body><![CDATA[
+                    
+          // if we no longer know about the mode, we can't restore the tab        
+          let mode = this.tabModes[aState.mode];        
+          if (!mode)
+            return false;
+          
+          let restoreFunc = mode.restoreTab || mode.tabType.restoreTab;
+        
+          if (!restoreFunc)
+            return false;
+            
+          // normalize the state to have an ext attribute if it does not.
+          if (!("ext" in aState))
+            aState.ext = {};
+            
+          this._restoringTabState = aState;  
+          restoreFunc.call(mode.tabType, this, aState.state);
+          this._restoringTabState = null;
+          
+          return true;
+          
+        ]]></body>
+      </method>          
       <method name="restoreTabs">
         <parameter name="aPersistedState"/>
         <parameter name="aDontRestoreFirstTab"/>
         <body><![CDATA[
           /**
            * Attempts to restore tabs persisted from a prior call to
            *  |persistTabs|.  This is currently a synchronous operation, but in
            *  the future this may kick off an asynchronous mechanism to restore
            *  the tabs one-by-one.
            */
           let tabs = aPersistedState.tabs;
           let indexToSelect = null;
           for each (let [iTab, tabState] in Iterator(tabs)) {
-            // if we no longer know about the mode, we can't restore the tab
-            if (!(tabState.mode in this.tabModes))
-              continue;
-
-            let mode = this.tabModes[tabState.mode];
-            let restoreFunc = mode.restoreTab || mode.tabType.restoreTab;
-            if (!restoreFunc)
-              continue;
-
-            // The first tab is a folder tab (we know that from our
-            // implementation). Tell the folder tab to back off if necessary.
-            // XXX find a better way to do this.
+          
             if (tabState.state.firstTab && aDontRestoreFirstTab)
               tabState.state.dontRestoreFirstTab = aDontRestoreFirstTab;
-
-            // normalize the state to have an ext attribute if it does not.
-            if (!("ext" in tabState))
-              tabState.ext = {};
-            this._restoringTabState = tabState;
-            restoreFunc.call(mode.tabType, this, tabState.state);
-            this._restoringTabState = null;
+          
+            if (!this.restoreTab(tabState))
+              continue;
 
             // If this persisted tab was the selected one, then mark the newest
             //  tab as the guy to select.
             if (iTab == aPersistedState.selectedIndex)
               indexToSelect = this.tabInfo.length - 1;
           }
+          
           if (indexToSelect != null && !aDontRestoreFirstTab)
             this.tabContainer.selectedIndex = indexToSelect;
           else
             this.tabContainer.selectedIndex = 0;
         ]]></body>
       </method>
 
       <!-- Called when the window is being unloaded, this calls the close
@@ -1091,31 +1292,54 @@
         <parameter name="aTabNode"/>
         <body>
           <![CDATA[
             // this happens when the user did not actually-click on a tab but
             // instead on the strip behind it.
             if (aTabNode.localName != "tab")
               return false;
 
-            // If there is only one tab, then then "close all tabs" menu item
-            // should not be enabled
-            let otherTabs = document.getAnonymousElementByAttribute(this,
-              "anonid", "closeOtherTabs");
-            otherTabs.setAttribute("disabled",
-              (this.tabContainer.childNodes.length == 1) ? "true" : "false")
+            let tab = this._getTabContextForTabbyThing(aTabNode, true) [1];
+             
+            // by default "close other tabs" is disabled...
+            document
+              .getAnonymousElementByAttribute(this, "anonid", "closeOtherTabs")
+              .setAttribute("disabled","true")
+                          
+            // ... except if we find at least one other tab that can be closed.
+            for (let i = 0; i < this.tabInfo.length; i++) {
+            
+              if (this.tabInfo[i].canClose && this.tabInfo[i] != tab) {
+              
+                document
+                  .getAnonymousElementByAttribute(this,"anonid", "closeOtherTabs")
+                  .setAttribute("disabled","false")
+                  
+                break;
+              }
+            }
+            
+            document
+              .getAnonymousElementByAttribute(this, "anonid", "closeTab")
+              .setAttribute("disabled", tab.canClose ? "false" : "true");       
+            
+            // enable "Open in new Window" iff tab is closable and...
+            // ... it can persist its state. Other wise it would get destroyed...
+            // ... while moving it to a new window.
+            document
+              .getAnonymousElementByAttribute(this, "anonid", "openTabInWindow")
+              .setAttribute("disabled", 
+                  (tab.canClose && this.persistTab(tab)) ? "false" : "true")
 
-            let closeTabItem = document.getAnonymousElementByAttribute(this,
-              "anonid", "closeTab");
-
-            // If the tab node is not closable, the "Close Tab" item should not
-            // be enabled.
-            let [iTab, tab, tabNode] =
-              this._getTabContextForTabbyThing(aTabNode, true);
-            closeTabItem.setAttribute("disabled", tab.canClose ? "false" : "true");
+            // If the tab history is empty, disable "Undo Close Tab"
+            document
+              .getAnonymousElementByAttribute(this, "anonid", "undoCloseTab")
+              .setAttribute("disabled",
+                  (this._tabUndo.length) ? "false" : "true");
+                          
             return true;
           ]]>
         </body>
       </method>
       <method name="supportsCommand">
         <parameter name="aCommand"/>
         <body>
           <![CDATA[
@@ -1510,16 +1734,26 @@
         {
           if (aIID.equals(Components.interfaces.nsIObserver) ||
               aIID.equals(Components.interfaces.nsISupports))
             return this;
           throw Components.results.NS_NOINTERFACE;
         }
         });
       </field>
+      
+      <field name="_tabDropIndicator">
+        document.getAnonymousElementByAttribute(
+            this.parentNode.parentNode.parentNode, 
+            "anonid", "tab-drop-indicator");
+      </field>
+      
+      <field name="_dragOverDelay">350</field>
+      <field name="_dragTime">0</field>
+      
       <field name="mTabMinWidth">100</field>
       <field name="mTabMaxWidth">250</field>
       <field name="mTabClipWidth">140</field>
       <field name="mCloseButtons">1</field>
 
       <field name="_mAutoHide">false</field>
       <property name="mAutoHide" onget="return this._mAutoHide;">
         <setter><![CDATA[
@@ -1693,28 +1927,372 @@
           this.mDownBoxAnimate.style.opacity = percent;
 
           if (this._animateStep < (this._animatePercents.length - 1))
             this._animateStep++;
           else
             this._stopAnimation();
         ]]></body>
       </method>
+      
+      <method name="_getDragTargetTab">
+        <parameter name="event"/>
+        <body><![CDATA[     
+                                   
+          if (event.target.localName != "tab")
+            return null;
+                      
+          let tab = event.target;
+          
+          if ((event.type != "drop") && (event.type != "dragover")) 
+            return tab;
+            
+          let boxObject = tab.boxObject;
+          
+          if (event.screenX < boxObject.screenX + boxObject.width * .25)
+            return null
+          
+          if (event.screenX > boxObject.screenX + boxObject.width * .75)
+            return null;
+           
+          return tab; 
+        ]]></body>
+      </method>
+    
+      
+      <method name="_getDropIndex">
+        <parameter name="event"/>
+        <body><![CDATA[        
+          let tabs = this.childNodes;                   
+            
+          if (window.getComputedStyle(this, null).direction == "ltr") {                      
+            for (let i = 0; i < tabs.length; i++)              
+              if (event.screenX < (tabs[i].boxObject.screenX + (tabs[i].boxObject.width / 2)))
+                return i;
+          }
+          else  {
+            for (let i = 0; i < tabs.length; i++)
+              if (event.screenX > (tabs[i].boxObject.screenX + (tabs[i].boxObject.width / 2)))
+                return i;         
+          }          
+         
+           return tabs.length;           
+        ]]></body>
+      </method>        
     </implementation>
     <handlers>
       <handler event="TabSelect" action="this._handleTabSelect();"/>
       <handler event="mouseover"><![CDATA[
         if (event.originalTarget == this.mAllTabsButton) {
           this.mAllTabsButton
               .setAttribute("tooltiptext",
                             this.mAllTabsButton.getAttribute("tooltipstring"));
         }
         else
           this.mAllTabsButton.removeAttribute("tooltiptext");
       ]]></handler>
+      <handler event="dragstart"><![CDATA[          
+        let draggedTab = this._getDragTargetTab(event);
+        
+        if (!draggedTab)
+          return;  
+
+        let tabmail = document.getElementById("tabmail");
+        let tab = tabmail.selectedTab;
+        
+        if (!tab || !tab.canClose)
+          return;
+                                          
+        let dt = event.dataTransfer;
+        
+        // If we drag within the same window, we use the tab directly
+        dt.mozSetDataAt("application/x-moz-tabmail-tab", draggedTab, 0);
+        
+        // otherwise we use session restore & JSON to migrate the tab.        
+        let uri = tabmail.persistTab(tab);
+        
+        // In case the tab implements session restore, we use JSON to convert
+        // it into a string
+        // 
+        // If a tab does not support session restore it retuns null. We can't 
+        // moved such tabs to a new window. However moving them within the same
+        // window works perfectly fine
+        
+        if (uri)
+          uri = JSON.stringify(uri);
+        
+        dt.mozSetDataAt("application/x-moz-tabmail-json", uri, 0);
+
+        dt.mozCursor = "default";
+            
+        // Create Drag Image
+        let panel = document.getElementById("tabpanelcontainer");
+        
+        let thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); 
+        thumbnail.width = Math.ceil(screen.availWidth / 5.75);
+        thumbnail.height = Math.round(width * 0.5625);
+        
+        let snippetWidth = panel.boxObject.width * .6;
+        let scale = thumbnail.width / snippetWidth;
+
+        let ctx = thumbnail.getContext("2d");
+    
+        ctx.scale(scale, scale);
+          
+        ctx.drawWindow(window,
+                panel.boxObject.screenX - window.mozInnerScreenX,
+                panel.boxObject.screenY - window.mozInnerScreenY, 
+                snippetWidth,
+                snippetWidth * 0.5625,
+                "rgb(255,255,255)"); 
+
+        var dt = event.dataTransfer;
+        dt.setDragImage(thumbnail, 0, 0);        
+                
+        event.stopPropagation();        
+      ]]></handler>
+      <handler event="dragover"><![CDATA[
+        let dt = event.dataTransfer;
+        
+        if (dt.mozItemCount == 0)
+          return;
+                
+        // Bug 516247:
+        // incase the user is dragging something else than a tab, and
+        // keeps hovering over a tab, we assume he wants to switch to this tab. 
+        
+        if ((dt.mozTypesAt(0)[0] != "application/x-moz-tabmail-tab")
+            && (dt.mozTypesAt(0)[1] != "application/x-moz-tabmail-json"))  {
+          
+          let tab = this._getDragTargetTab(event); 
+         
+          if (!tab)
+            return;
+          
+          event.preventDefault();
+          event.stopPropagation();          
+          
+          if (!this._dragTime) {
+            this._dragTime = Date.now();
+            return;
+          }
+          
+          if (Date.now() <= this._dragTime + this._dragOverDelay)
+            return;
+          
+          let tabmail = document.getElementById("tabmail");
+          
+          if (tabmail.tabContainer.selectedItem == tab)
+            return;
+             
+          tabmail.tabContainer.selectedItem = tab;         
+          
+          return;
+        }
+                            
+        // as some tabs do not support session restore they can't be 
+        // moved to a different or new window. We should not show 
+        // a dropmarker in such a case
+        if (!dt.mozGetDataAt("application/x-moz-tabmail-json", 0)) {
+        
+          let draggedTab = dt.mozGetDataAt("application/x-moz-tabmail-tab", 0);
+           
+          if (!draggedTab)
+            return;
+
+          let tabmail = document.getElementById("tabmail");
+          
+          if (tabmail.tabContainer.getIndexOfItem(draggedTab) == -1)
+            return;
+        }
+
+        dt.effectAllowed = "copyMove";
+
+        event.preventDefault();
+        event.stopPropagation();
+                
+        let ltr = (window.getComputedStyle(this, null).direction == "ltr");        
+        let ind = this._tabDropIndicator;
+        
+        // Let's scroll
+        if (this.hasAttribute("overflow")) {
+                 
+          let target = event.originalTarget.getAttribute("anonid");
+                    
+          let pixelsToScroll = 0;
+          
+          if (target == "scrollbutton-up")
+            pixelsToScroll = this.mTabstrip.scrollIncrement;
+            
+          if (target == "scrollbutton-down")
+            pixelsToScroll = this.mTabstrip.scrollIncrement * -1;
+            
+          if (ltr)
+            pixelsToScroll = pixelsToScroll * -1;
+          
+          if (pixelsToScroll) {
+            // Hide Indicator while Scrolling
+            ind.collapsed = true;
+            this.mTabstrip.scrollByPixels(pixelsToScroll);            
+            return;
+          }
+        }
+                                 
+        let newIndex = this._getDropIndex(event);
+        
+        // fix the DropIndex in case it points to tab that can't be closed        
+        let tabInfo = document.getElementById("tabmail").tabInfo;
+                
+        while ((newIndex < tabInfo.length) && !(tabInfo[newIndex].canClose))
+          newIndex++;  
+                
+                  
+        var scrollRect = this.mTabstrip.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 (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";
+        ind.style.marginTop = (-ind.clientHeight) + "px";
+      ]]></handler>
+      <handler event="drop"><![CDATA[
+        let dt = event.dataTransfer;
+    
+        if (dt.mozItemCount != 1)
+          return;
+        
+        let draggedTab = dt.mozGetDataAt("application/x-moz-tabmail-tab",0);
+        
+        if (!draggedTab)
+          return;
+        
+        event.stopPropagation();        
+        this._tabDropIndicator.collapsed = true;
+      
+        let tabmail = document.getElementById("tabmail");
+        
+        // Is the tab one of our children?
+        if (tabmail.tabContainer.getIndexOfItem(draggedTab) == -1) {
+        
+          // It's a tab from an other window, so we have to trigger session
+          // restore to get our tab
+
+          let tabmail2 = draggedTab.ownerDocument.getElementById("tabmail");        
+          if (!tabmail2)
+            return;
+          
+          let draggedJson = dt.mozGetDataAt("application/x-moz-tabmail-json", 0);                        
+          if (!draggedJson)
+            return;
+        
+          draggedJson = JSON.parse(draggedJson);
+          
+          // Some tab exist only once, so we have to gamble a bit. We close 
+          // the tab and try to reopen it. If something fails the tab is gone.
+ 
+          tabmail2.closeTab(draggedTab,true);
+          
+          if (! tabmail.restoreTab(draggedJson))
+            return;
+                           
+          draggedTab = tabmail.tabContainer.childNodes[tabmail.tabContainer.childNodes.length-1];
+        }
+
+        let idx = this._getDropIndex(event);
+        
+        // fix the DropIndex in case it points to tab that can't be closed        
+        let tabInfo = tabmail.tabInfo;                
+        while ((idx < tabInfo.length) && !(tabInfo[idx].canClose))
+          idx++;  
+                            
+        tabmail.moveTabTo(draggedTab, idx);
+                  
+        tabmail.switchToTab(draggedTab);
+        tabmail.updateCurrentTab();          
+      ]]></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.
+
+        // the user pressed ESC to cancel the drag, or the drag succeded
+        var dt = event.dataTransfer;
+        if ((dt.mozUserCancelled) || (dt.dropEffect != "none"))
+          return;
+          
+        // Disable detach within the browser toolbox
+        var eX = event.screenX;
+        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;
+          let eY = event.screenY;
+          
+          if (eY < endScreenY && eY > window.screenY)        
+            return;
+        }
+
+        // user wants to deatach tab from window...        
+        if (dt.mozItemCount != 1)
+          return;
+        
+        let draggedTab = dt.mozGetDataAt("application/x-moz-tabmail-tab",0);
+        
+        if (!draggedTab)
+          return;
+        
+        document.getElementById("tabmail").replaceTabWithWindow(draggedTab);                    
+                            
+      ]]></handler>
+      
+      <handler event="dragexit"><![CDATA[
+        this._dragTime = 0;
+        
+        this._tabDropIndicator.collapsed = true;                
+        event.stopPropagation();
+        
+      ]]></handler>      
     </handlers>
   </binding>
   
   <!-- alltabs-popup binding
        This binding relies on the structure of the tabbrowser binding.
        Therefore it should only be used as a child of the tabs element.
        This binding is exposed as a pseudo-public-API so themes can customize
        the tabbar appearance without having to be scriptable
--- a/mail/locales/en-US/chrome/messenger/messenger.dtd
+++ b/mail/locales/en-US/chrome/messenger/messenger.dtd
@@ -1,19 +1,30 @@
 <!ENTITY messengerWindow.title "Mail &amp; Newsgroups">
 <!ENTITY titledefault.label    "&brandFullName;">
 <!ENTITY titleSeparator.label " - ">
 
 <!-- File Menu -->
 <!ENTITY newFolderCmd.label "Folder…">
 <!ENTITY newFolderCmd.accesskey "F">
-<!ENTITY closeTabCmd.label "Close Tab">
-<!ENTITY closeTabCmd.accesskey "e">
-<!ENTITY closeOtherTabsCmd.label "Close Other Tabs">
-<!ENTITY closeOtherTabsCmd.accesskey "L">
+<!ENTITY closeTabCmd2.label "Close Tab">
+<!ENTITY closeTabCmd2.accesskey "C">
+<!ENTITY closeOtherTabsCmd2.label "Close Other Tabs">
+<!ENTITY closeOtherTabsCmd2.accesskey "o">
+<!-- LOCALIZATION NOTE (undoCloseTabCmd.label):
+     Menu option to attempt to re-open a recently closed tab.
+     -->
+<!ENTITY undoCloseTabCmd.label "Undo Close Tab">
+<!ENTITY undoCloseTabCmd.accesskey "U">
+<!-- LOCALIZATION NOTE (moveToNewWindow.label):
+     Menu option to cause the current tab to be migrated to a new Thunderbird
+     window.
+     -->
+<!ENTITY moveToNewWindow.label "Move to New Window">
+<!ENTITY moveToNewWindow.accesskey "W">
 <!ENTITY newVirtualFolderCmd.label "Saved Search…">
 <!ENTITY newVirtualFolderCmd.accesskey "S">
 <!ENTITY newOtherAccountsCmd.label "Other Accounts…">
 <!ENTITY newOtherAccountsCmd.accesskey "O">
 <!ENTITY newEmailAccountCmd.label "Mail Account…">
 <!ENTITY newEmailAccountCmd.accesskey "A">
 <!ENTITY openMessageFileCmd.label "Open Saved Message…">
 <!ENTITY openMessageFileCmd.accesskey "O">
--- a/mail/test/mozmill/mozmilltests.list
+++ b/mail/test/mozmill/mozmilltests.list
@@ -11,8 +11,9 @@ junk-commands
 message-header
 message-window
 migration
 migration-from-tb2
 pref-window
 quick-filter-bar
 search-window
 session-store
+tabmail
new file mode 100644
--- /dev/null
+++ b/mail/test/mozmill/tabmail/test-tabmail-dragndrop.js
@@ -0,0 +1,457 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Thunderbird Mail Client.
+ *
+ * The Initial Developer of the Original Code is
+ *   Thomas Schmid <schmid-thomas@gmx.net>
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * 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
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * 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 ***** */
+
+/*
+ * Test rearanging tabs via drag'n'drop.
+ */
+
+var MODULE_NAME = "test-tabmail-dragndrop";
+
+var RELATIVE_ROOT = "../shared-modules";
+var MODULE_REQUIRES = ["folder-display-helpers", "window-helpers"];
+
+var folder;
+let msgHdrsInFolder = [];
+
+// The number of messages in folder.
+const NUM_MESSAGES_IN_FOLDER = 10;
+
+function setupModule(module) {
+  let fdh = collector.getModule("folder-display-helpers");
+  fdh.installInto(module);
+  let wh = collector.getModule("window-helpers");
+  wh.installInto(module);
+
+  folder = create_folder("MessageFolder");
+  make_new_sets_in_folder(folder, [{count: NUM_MESSAGES_IN_FOLDER}]);
+}
+
+/**
+ * Verifies our test environment is setup correctly and initializes
+ * all global variables.
+ */
+function test_tab_reorder_setup_globals() {
+
+  be_in_folder(folder);
+  // Scroll to the top
+  mc.folderDisplay.ensureRowIsVisible(0);
+  let msgHdr = mc.dbView.getMsgHdrAt(1);
+
+  display_message_in_folder_tab(msgHdr, false);
+
+  // Check that the right message is displayed
+  assert_number_of_tabs_open(1);
+  assert_folder_selected_and_displayed(folder);
+  assert_selected_and_displayed(msgHdr);
+
+  assert_row_visible(1);
+
+  //Initialize the globals we'll need for all our tests.
+
+  // Stash messages into arrays for convenience. We do it this way so that the
+  // order of messages in the arrays is the same as in the views.
+  be_in_folder(folder);
+  for (let i = 0; i < NUM_MESSAGES_IN_FOLDER; i++)
+    msgHdrsInFolder.push(mc.dbView.getMsgHdrAt(i));
+
+  // Mark all messages read
+  folder.markAllMessagesRead(null);
+}
+
+/**
+ * Tests reordering tabs by drag'n'drop within the tabbar
+ *
+ * It opens aditional movable and closable tabs. The picks the first
+ * movable tab and drops it onto the third movable tab.
+ */
+function test_tab_reorder_tabbar(){
+
+  // Ensure only one tab is open, otherwise our test most likey fail anyway.
+  mc.tabmail.closeOtherTabs(0);
+  assert_number_of_tabs_open(1);
+
+  try {
+
+    be_in_folder(folder);
+
+    // Open four tabs
+    for (let idx=0; idx < 4 ; idx++) {
+      select_click_row(idx);
+      open_selected_message_in_new_tab(true);
+    }
+
+    // Check if every thing is correctly initalized
+    assert_number_of_tabs_open(5);
+
+    assert_true(mc.tabmail.tabModes["message"].tabs[0] == mc.tabmail.tabInfo[1],
+        " tabMode.tabs and tabInfo out of sync");
+
+    assert_true(mc.tabmail.tabModes["message"].tabs[1] == mc.tabmail.tabInfo[2],
+        " tabMode.tabs and tabInfo out of sync");
+
+    assert_true(mc.tabmail.tabModes["message"].tabs[2] == mc.tabmail.tabInfo[3],
+        " tabMode.tabs and tabInfo out of sync");
+
+    // Start dragging the first tab
+    switch_tab(1);
+    assert_selected_and_displayed(msgHdrsInFolder[0]);
+
+    let tab1 = mc.tabmail.tabContainer.childNodes[1];
+    let tab3 = mc.tabmail.tabContainer.childNodes[3];
+
+    let dt = _synthesizeDragStart(mc.window, tab1, mc.tabmail);
+
+    // Drop it onto the third tab ...
+    _synthesizeDragOver(mc.window, tab3, dt);
+
+    _synthesizeDrop(mc.window, tab3, dt,
+        { screenX : tab3.boxObject.screenX + (tab3.boxObject.width * 0.75),
+          screenY : tab3.boxObject.screenY });
+
+    wait_for_message_display_completion(mc);
+
+    // if every thing went well...
+    assert_number_of_tabs_open(5);
+
+    // ... we should find tab1 at the third position...
+    assert_true(tab1 == mc.tabmail.tabContainer.childNodes[3],
+                "Moving tab1 failed");
+    switch_tab(3);
+    assert_selected_and_displayed(msgHdrsInFolder[0]);
+
+    // ... while tab3 moves one up and gets second.
+    assert_true(tab3 == mc.tabmail.tabContainer.childNodes[2],
+                "Moving tab3 failed");
+    switch_tab(2);
+    assert_selected_and_displayed(msgHdrsInFolder[2]);
+
+    // we have one "message" tab and three "folder" tabs, thus tabInfo[1-3] and
+    // tabMode["message"].tabs[0-2] have to be same, otherwise something went
+    // wrong while moving tabs around
+    assert_true(mc.tabmail.tabModes["message"].tabs[0] == mc.tabmail.tabInfo[1],
+        " tabMode.tabs and tabInfo out of sync");
+
+    assert_true(mc.tabmail.tabModes["message"].tabs[1] == mc.tabmail.tabInfo[2],
+        " tabMode.tabs and tabInfo out of sync");
+
+    assert_true(mc.tabmail.tabModes["message"].tabs[2] == mc.tabmail.tabInfo[3],
+        " tabMode.tabs and tabInfo out of sync");
+  }
+  finally {
+    // finally close the tabs we opened.
+    mc.tabmail.closeOtherTabs(0);
+    assert_number_of_tabs_open(1);
+  }
+}
+
+/**
+ * Tests drag'n'drop tab reordering between windows
+ */
+function test_tab_reorder_window(){
+
+  // Ensure only one tab is open, otherwise our test most likey fail anyway.
+  mc.tabmail.closeOtherTabs(0);
+  assert_number_of_tabs_open(1);
+
+  let mc2 = null;
+
+  try {
+
+    be_in_folder(folder);
+
+    // Open a new tab...
+    select_click_row(1);
+    open_selected_message_in_new_tab(false);
+
+    assert_number_of_tabs_open(2);
+
+    switch_tab(1);
+    assert_selected_and_displayed(msgHdrsInFolder[1]);
+
+    // ...and then a new 3 pane as our drop target.
+    plan_for_new_window("mail:3pane");
+
+    let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]
+                          .getService(Ci.nsIWindowWatcher);
+
+    let args = {msgHdr: msgHdrsInFolder[3]};
+    args.wrappedJSObject = args;
+
+    let aWnd2 = ww.openWindow(null,
+        "chrome://messenger/content/", "",
+        "all,chrome,dialog=no,status,toolbar", args);
+
+    mc2 = wait_for_new_window("mail:3pane");
+    wait_for_message_display_completion(mc2,true);
+
+    // Double check if we are listening to the right window.
+    assert_true(aWnd2 == mc2.window, "Opening Window failed" );
+
+    // Start dragging the first tab ...
+    let tabA = mc.tabmail.tabContainer.childNodes[1];
+    assert_true(tabA, "No movable Tab");
+
+    // We drop onto the Folder Tab, it is guaranteed to exist.
+    let tabB = mc2.tabmail.tabContainer.childNodes[0];
+    assert_true(tabB, "No movable Tab");
+
+    let dt = _synthesizeDragStart(mc.window,tabA,mc.tabmail);
+
+    _synthesizeDragOver(mc2.window, tabB,dt);
+
+    _synthesizeDrop(mc2.window,tabB, dt,
+        { screenX : tabB.boxObject.screenX + (tabB.boxObject.width * 0.75),
+          screenY : tabB.boxObject.screenY });
+
+    wait_for_message_display_completion(mc2);
+
+    assert_true( !! (mc.tabmail.tabContainer.childNodes.length == 1),
+      "Moving tab to new window failed, tab still in old window");
+
+    assert_true( !! (mc2.tabmail.tabContainer.childNodes.length == 2),
+      "Moving tab to new window failed, no new tab in new window");
+
+    assert_selected_and_displayed(mc2,msgHdrsInFolder[1]);
+
+  }
+  finally {
+    // finally close the tabs and windows we opened.
+    mc.tabmail.closeOtherTabs(0);
+    assert_number_of_tabs_open(1);
+
+    if (mc2)
+      mc2.window.close();
+  }
+
+
+}
+
+/**
+ * Tests detaching tabs into windows via drag'n'drop
+ */
+function test_tab_reorder_detach(){
+
+  // Ensure only one tab is open, otherwise our test most likey fail anyway.
+  mc.tabmail.closeOtherTabs(0);
+  assert_number_of_tabs_open(1);
+
+  let mc2 = null;
+
+  try {
+
+    be_in_folder(folder);
+
+    // Open a new tab...
+    select_click_row(2);
+    open_selected_message_in_new_tab(false);
+
+    assert_number_of_tabs_open(2);
+
+    // ... if every thing works we should expect a new window...
+    plan_for_new_window("mail:3pane");
+
+    // ... now start dragging
+
+    mc.tabmail.switchToTab(1);
+
+    let tab1 = mc.tabmail.tabContainer.childNodes[1];
+    let dropContent = mc.e("tabpanelcontainer");
+    let box = dropContent.boxObject;
+
+    let dt = _synthesizeDragStart(mc.window, tab1, mc.tabmail);
+
+    _synthesizeDragOver(mc.window, dropContent, dt);
+
+    // notify tab1 drag has ended
+    _synthesizeDragEnd(mc.window, dropContent, tab1, dt,
+        { screenX : (box.screenX + box.width / 2 ),
+          screenY : (box.screenY + box.height / 2 ) });
+
+    // ... and wait for the new window
+    mc2 = wait_for_new_window("mail:3pane");
+    wait_for_message_display_completion(mc2, true);
+
+    assert_true(mc.tabmail.tabContainer.childNodes.length == 1,
+        "Moving tab to new window failed, tab still in old window");
+
+    assert_true(mc2.tabmail.tabContainer.childNodes.length == 2,
+        "Moving tab to new window failed, no new tab in new window");
+
+    assert_selected_and_displayed(mc2, msgHdrsInFolder[2]);
+
+  }
+  finally {
+    // finally close the tabs and window we opened.
+    mc.tabmail.closeOtherTabs(0);
+    assert_number_of_tabs_open(1);
+
+    if (mc2)
+      mc2.window.close();
+  }
+}
+
+/**
+ * Test undo of recently closed tabs.
+ */
+function test_tab_undo() {
+  // Ensure only one tab is open, otherwise our test most likey fail anyway.
+  mc.tabmail.closeOtherTabs(0);
+  assert_number_of_tabs_open(1);
+
+  try {
+
+    be_in_folder(folder);
+
+    // Open five tabs...
+    for (let idx = 0; idx < 5; idx++) {
+      select_click_row(idx);
+      open_selected_message_in_new_tab(true);
+    }
+
+    assert_number_of_tabs_open(6);
+
+    switch_tab(2);
+    assert_selected_and_displayed(msgHdrsInFolder[1]);
+
+    mc.tabmail.closeTab(2);
+    mc.tabmail.closeTab(2, true);
+    mc.tabmail.closeTab(2);
+
+    assert_number_of_tabs_open(3);
+    assert_selected_and_displayed(mc, msgHdrsInFolder[4]);
+
+    mc.tabmail.undoCloseTab();
+    assert_number_of_tabs_open(4);
+    assert_selected_and_displayed(mc, msgHdrsInFolder[3]);
+
+    // msgHdrsInFolder[2] won't be restorend it was closed with disabled undo.
+
+    mc.tabmail.undoCloseTab();
+    assert_number_of_tabs_open(5);
+    assert_selected_and_displayed(mc, msgHdrsInFolder[1]);
+
+  }
+  finally  {
+    // finally close the tabs opened.
+    mc.tabmail.closeOtherTabs(0);
+    assert_number_of_tabs_open(1);
+  }
+}
+
+/*
+ * A set of private helper functions for drag'n'drop
+ */
+
+/**
+ * Starts a drag new session.
+ * @param {} aWindow
+ * @param {XULElement} aDispatcher
+ *   the element from which the drag session should be started.
+ * @param {XULElement} aListener
+ *   the element who's drop target should be captured and returned.
+ * @return {nsIDataTransfer}
+ *   returns the DataTransfer Object of captured by aListener.
+ */
+function _synthesizeDragStart(aWindow, aDispatcher, aListener)
+{
+  let dt;
+
+  var trapDrag = function(event) {
+
+    if ( !event.dataTransfer )
+      throw "no DataTransfer";
+
+    dt = event.dataTransfer;
+
+    //event.stopPropagation();
+    event.preventDefault();
+  };
+
+  aListener.addEventListener("dragstart", trapDrag, true);
+
+  EventUtils.synthesizeMouse(aDispatcher, 5, 5, {type:"mousedown"}, aWindow);
+  EventUtils.synthesizeMouse(aDispatcher, 5, 10, {type:"mousemove"}, aWindow);
+  EventUtils.synthesizeMouse(aDispatcher, 5, 15, {type:"mousemove"}, aWindow);
+
+  aListener.removeEventListener("dragstart", trapDrag, true);
+
+  return dt;
+}
+
+function _synthesizeDragOver(aWindow, aDispatcher, aDt, aArgs)
+{
+  _synthesizeDragEvent("dragover", aWindow, aDispatcher, aDt, aArgs);
+}
+
+function _synthesizeDragEnd(aWindow, aDispatcher, aListener, aDt, aArgs)
+{
+  _synthesizeDragEvent("dragend", aWindow, aListener, aDt, aArgs);
+
+  //Ensure drag has ended.
+  EventUtils.synthesizeMouse(aDispatcher, 5, 5, {type:"mousemove"}, aWindow);
+  EventUtils.synthesizeMouse(aDispatcher, 5, 10, {type:"mousemove"}, aWindow);
+  EventUtils.synthesizeMouse(aDispatcher, 5, 5, {type:"mouseup"}, aWindow);
+}
+
+function _synthesizeDrop(aWindow, aDispatcher, aDt, aArgs)
+{
+  _synthesizeDragEvent("drop", aWindow, aDispatcher, aDt, aArgs);
+
+  // Ensure drag has ended.
+  EventUtils.synthesizeMouse(aDispatcher, 5, 5, {type:"mousemove"}, aWindow);
+  EventUtils.synthesizeMouse(aDispatcher, 5, 10, {type:"mousemove"}, aWindow);
+  EventUtils.synthesizeMouse(aDispatcher, 5, 5, {type:"mouseup"}, aWindow);
+}
+
+function _synthesizeDragEvent(aType, aWindow, aDispatcher, aDt, aArgs)
+{
+  let screenX;
+  if (aArgs && ("screenX" in aArgs))
+    screenX = aArgs.screenX;
+  else
+    screenX = aDispatcher.boxObject.ScreenX;;
+
+  let screenY;
+  if (aArgs && ("screenY" in aArgs))
+    screenY = aArgs.screenY;
+  else
+    screenY = aDispatcher.boxObject.ScreenY;
+
+  let event = aWindow.document.createEvent("DragEvents");
+  event.initDragEvent(aType, true, true, aWindow, 0,
+      screenX, screenY, 0, 0, false, false, false, false, 0, null, aDt);
+  aDispatcher.dispatchEvent(event);
+}