Merge from m-c.
authorJason Duell <jduell.mcbugs@gmail.com>
Fri, 13 Aug 2010 00:51:25 -0700
changeset 50564 6a52899cc5b2331a8cfa540192511712445c5ad6
parent 50563 6ee35465efab8cd76e59c4b0147241b3751d0690 (current diff)
parent 50387 452db8c688bad65a1003ff00b2606d28410c5373 (diff)
child 50565 7f0bd8be0e64e242ba0d5486abab8bba24952b25
push idunknown
push userunknown
push dateunknown
milestone2.0b4pre
Merge from m-c.
services/sync/FormNotifier.js
services/sync/tests/unit/test_engines_forms_store.js
--- a/accessible/src/html/nsHTMLTableAccessible.cpp
+++ b/accessible/src/html/nsHTMLTableAccessible.cpp
@@ -890,18 +890,26 @@ nsHTMLTableAccessible::GetCellAt(PRInt32
 {
   nsCOMPtr<nsIDOMElement> cellElement;
   nsresult rv = GetCellAt(aRow, aColumn, *getter_AddRefs(cellElement));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIContent> cellContent(do_QueryInterface(cellElement));
   nsAccessible *cell =
     GetAccService()->GetAccessibleInWeakShell(cellContent, mWeakShell);
-  if (cell)
-    CallQueryInterface(cell, aTableCellAccessible);
+
+  if (!cell) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  if (cell != this) {
+    // XXX bug 576838: crazy tables (like table6 in tables/test_table2.html) may
+    // return itself as a cell what makes Orca hang.
+    NS_ADDREF(*aTableCellAccessible = cell);
+  }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsHTMLTableAccessible::GetCellIndexAt(PRInt32 aRow, PRInt32 aColumn,
                                       PRInt32 *aIndex)
 {
--- a/accessible/tests/mochitest/table/test_table_2.html
+++ b/accessible/tests/mochitest/table/test_table_2.html
@@ -71,16 +71,21 @@ function doTest()
   // Test table with display:inline and an outside table. We shouldn't be fooled
   // by the outside table and shouldn't create table accessible and table cell
   // accessible in this case
   accNotCreated = (!isAccessible("table5"));
   ok(accNotCreated, "wrongly created table accessible");
   accNotCreated = (!isAccessible("t5_cell"));
   ok(accNotCreated, "wrongly created table cell accessible");
 
+  // test crazy table
+  var table6 = getAccessible("table6", [nsIAccessibleTable]);
+  ok(!table6.getCellAt(0, 0),
+     "We don't expect cell accessible for crazy table 6!");
+
   SimpleTest.finish();
 }
 SimpleTest.waitForExplicitFinish();
 addA11yLoadEvent(doTest);
   </script>
  </head>
 
  <body >
@@ -133,11 +138,16 @@ addA11yLoadEvent(doTest);
      <table style="display:inline" id="table5">
        <tr><td id="t5_cell">cell0</td></tr>
      </table>
    </td>
    <td>cell1</td>
    </tr>
    </table>
 
+  <div style="display:table;" id="table6">
+    <input type="checkbox">
+    <a href="bar">Bad checkbox</a>
+  </div>
+
   </center>
  </body>
 </html>
--- a/accessible/tests/mochitest/test_name_nsRootAcc.xul
+++ b/accessible/tests/mochitest/test_name_nsRootAcc.xul
@@ -21,16 +21,20 @@
           src="chrome://mochikit/content/a11y/accessible/events.js"></script>
 
   <script type="application/javascript">
   <![CDATA[
     // var gA11yEventDumpID = "eventdump"; // debug stuff
 
     function doTest()
     {
+      // Actually, just disable this test everywhere -- bug 586818.
+      SimpleTest.finish();
+      return;
+
       if (LINUX) {
         todo(false, "Enable test on Linux - see bug 525175.");
         SimpleTest.finish();
         return;
       }
 
       var w = window.openDialog("chrome://mochikit/content/a11y/accessible/name_nsRootAcc_wnd.xul",
                                 "nsRootAcc_name_test", 
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/chrome.manifest
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/chrome.manifest
@@ -1,14 +1,16 @@
 resource testpilot ./
 content testpilot content/
 skin testpilot skin skin/all/
 skin testpilot-os skin skin/linux/ os=Linux
 skin testpilot-os skin skin/mac/ os=Darwin
 skin testpilot-os skin skin/win/ os=WINNT
+overlay chrome://browser/content/macBrowserOverlay.xul chrome://testpilot/content/tp-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion<4.0b1pre
+overlay chrome://browser/content/macBrowserOverlay.xul chrome://testpilot/content/feedback-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion>=4.0b1pre
 overlay chrome://browser/content/browser.xul chrome://testpilot/content/tp-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion<=3.6.*
 overlay chrome://browser/content/browser.xul chrome://testpilot/content/feedback-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion>3.7a1pre
 # For the menubar on Mac
 overlay chrome://testpilot/content/all-studies-window.xul chrome://browser/content/macBrowserOverlay.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} os=Darwin
 
 component {e6e5e58f-7977-485a-b076-2f74bee2677b} components/TestPilot.js
 contract @mozilla.org/testpilot/service;1 {e6e5e58f-7977-485a-b076-2f74bee2677b}
 category profile-after-change testpilot @mozilla.org/testpilot/service;1
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.js
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.js
@@ -281,21 +281,19 @@ var TestPilotXulWindow = {
       this.addThumbnail(newRow, task.thumbnail);
 
       let textVbox = document.createElement("vbox");
       newRow.appendChild(textVbox);
 
       let openInTab = (task.taskType == TaskConstants.TYPE_LEGACY);
 
       this.addDescription(textVbox, task.title, task.summary);
-      if (task.showMoreInfoLink) {
-        this.addXulLink(
-          textVbox, this._stringBundle.getString("testpilot.moreInfo"),
-          task.defaultUrl, openInTab);
-      }
+      this.addXulLink(
+        textVbox, this._stringBundle.getString("testpilot.moreInfo"),
+        task.defaultUrl, openInTab);
 
       // Create the rightmost status area, depending on status:
       let statusVbox = document.createElement("vbox");
       if (task.status == TaskConstants.STATUS_FINISHED) {
         this.addLabel(
           statusVbox,
           this._stringBundle.getFormattedString(
             "testpilot.studiesWindow.finishedOn",
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/browser.css
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/browser.css
@@ -2,18 +2,20 @@
 
 #feedback-menu-button {
   -moz-box-orient: horizontal;
 }
 
 #feedback-menu-button .toolbarbutton-text {
   display: -moz-box;
   margin: 0;
+  color: -moz-dialogtext;
+  text-shadow: none;
 }
- 
+
 #feedback-menu-button .toolbarbutton-icon {
   display: none;
 }
 
 #feedback-menu-button .toolbarbutton-menu-dropmarker {
   -moz-padding-start: 5px;
 }
 
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js
@@ -161,20 +161,16 @@ var TestPilotTask = {
     return this.infoPageUrl;
   },
 
   get uploadUrl() {
     let url = Application.prefs.getValue(DATA_UPLOAD_PREF, "");
     return url + this._id;
   },
 
-  get showMoreInfoLink() {
-    return true;
-  },
-
   // event handlers:
 
   onExperimentStartup: function TestPilotTask_onExperimentStartup() {
   },
 
   onExperimentShutdown: function TestPilotTask_onExperimentShutdown() {
   },
 
@@ -1016,20 +1012,16 @@ TestPilotWebSurvey.prototype = {
   get taskType() {
     return TaskConstants.TYPE_SURVEY;
   },
 
   get defaultUrl() {
     return this.infoPageUrl;
   },
 
-  get showMoreInfoLink() {
-    return false;
-  },
-
   onDetailPageOpened: function TPWS_onDetailPageOpened() {
     /* Once you view the URL of the survey, we'll assume you've taken it.
      * There's no reliable way to tell whether you have or not, so let's
      * default to not bugging the user about it again.
      */
     if (this._status < TaskConstants.STATUS_SUBMITTED) {
       this.changeStatus( TaskConstants.STATUS_SUBMITTED, true );
     }
--- a/browser/base/Makefile.in
+++ b/browser/base/Makefile.in
@@ -78,8 +78,11 @@ ifneq (,$(filter windows cocoa gtk2, $(M
 ifneq ($(OS_ARCH),WINCE)
 DEFINES += -DCONTEXT_COPY_IMAGE_CONTENTS=1
 endif
 endif
 
 ifneq (,$(filter windows, $(MOZ_WIDGET_TOOLKIT)))
 DEFINES += -DMENUBAR_CAN_AUTOHIDE=1
 endif
+
+libs::
+	$(NSINSTALL) $(srcdir)/content/tabview/modules/* $(FINAL_TARGET)/modules/tabview
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -203,16 +203,20 @@
 #endif
 #endif
               </menupopup>
             </menu>
 
             <menu id="view-menu" label="&viewMenu.label;"
                   accesskey="&viewMenu.accesskey;">
               <menupopup id="menu_viewPopup">
+                <menuitem id="menu_tabview"
+                          label="&showTabView.label;"
+                          accesskey="&showTabView.accesskey;"
+                          command="Browser:ToggleTabView"/>
                 <menu id="viewToolbarsMenu"
                       label="&viewToolbarsMenu.label;"
                       accesskey="&viewToolbarsMenu.accesskey;">
                   <menupopup onpopupshowing="onViewToolbarsPopupShowing(event);">
                     <menuseparator/>
                     <menuitem id="menu_tabsOnTop"
                               command="cmd_ToggleTabsOnTop"
                               type="checkbox"
@@ -585,17 +589,21 @@
       </menu>
       <menuseparator/>
     </menupopup>
   </menu>
 
             <menu id="tools-menu"
                   label="&toolsMenu.label;"
                   accesskey="&toolsMenu.accesskey;">
-              <menupopup id="menu_ToolsPopup">
+              <menupopup id="menu_ToolsPopup"
+#ifdef MOZ_SERVICES_SYNC
+                         onpopupshowing="gSyncUI.updateUI();"
+#endif
+                         >
               <menuitem id="menu_search"
                         label="&search.label;"
                         accesskey="&search.accesskey;"
                         key="key_search"
                         command="Tools:Search"/>
               <menuseparator id="browserToolsSeparator"/>
               <menuitem id="menu_openDownloads"
                         label="&downloads.label;"
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -475,19 +475,19 @@ var PlacesCommandHook = {
    * only the first instance of each URI will be returned.
    *
    * @returns a list of nsIURI objects representing unique locations open
    */
   _getUniqueTabInfo: function BATC__getUniqueTabInfo() {
     var tabList = [];
     var seenURIs = {};
 
-    var browsers = gBrowser.browsers;
-    for (var i = 0; i < browsers.length; ++i) {
-      let uri = browsers[i].currentURI;
+    let tabs = gBrowser.visibleTabs;
+    for (let i = 0; i < tabs.length; ++i) {
+      let uri = tabs[i].linkedBrowser.currentURI;
 
       // skip redundant entries
       if (uri.spec in seenURIs)
         continue;
 
       // add to the set of seen URIs
       seenURIs[uri.spec] = null;
       tabList.push(uri);
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -112,16 +112,17 @@
       <observes element="Browser:Reload" attribute="disabled"/>
     </command>
     <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true">
       <observes element="Browser:Reload" attribute="disabled"/>
     </command>
     <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);"/>
     <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);"/>
     <command id="Browser:ShowAllTabs" oncommand="allTabs.open();"/>
+    <command id="Browser:ToggleTabView" oncommand="TabView.toggle();"/>
     <command id="cmd_fullZoomReduce"  oncommand="FullZoom.reduce()"/>
     <command id="cmd_fullZoomEnlarge" oncommand="FullZoom.enlarge()"/>
     <command id="cmd_fullZoomReset"   oncommand="FullZoom.reset()"/>
     <command id="cmd_fullZoomToggle"  oncommand="ZoomManager.toggleZoom();"/>
     <command id="Browser:OpenLocation" oncommand="openLocation();"/>
 
     <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
     <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -35,57 +35,50 @@
 # 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 *****
 
 // gSyncUI handles updating the tools menu
 let gSyncUI = {
   init: function SUI_init() {
-    let obs = [["weave:service:sync:start", "onActivityStart"],
-               ["weave:service:sync:finish", "onSyncFinish"],
-               ["weave:service:sync:error", "onSyncError"],
-               ["weave:service:sync:delayed", "onSyncDelay"],
-               ["weave:service:setup-complete", "onLoginFinish"],
-               ["weave:service:login:start", "onActivityStart"],
-               ["weave:service:login:finish", "onLoginFinish"],
-               ["weave:service:login:error", "onLoginError"],
-               ["weave:service:logout:finish", "onLogout"],
-               ["weave:service:start-over", "onStartOver"]];
+    // this will be the first notification fired during init
+    // we can set up everything else later
+    Services.obs.addObserver(this, "weave:service:ready", true);
+  },
+  initUI: function SUI_initUI() {
+    let obs = ["weave:service:sync:start",
+               "weave:service:sync:finish",
+               "weave:service:sync:error",
+               "weave:service:sync:delayed",
+               "weave:service:setup-complete",
+               "weave:service:login:start",
+               "weave:service:login:finish",
+               "weave:service:login:error",
+               "weave:service:logout:finish",
+               "weave:service:start-over"];
 
     // If this is a browser window?
     if (gBrowser) {
-      obs.push(["weave:notification:added", "onNotificationAdded"],
-               ["weave:notification:removed", "onNotificationRemoved"]);
+      obs.push("weave:notification:added", "weave:notification:removed");
     }
 
-    // Add the observers now and remove them on unload
     let self = this;
-    let addRem = function(add) {
-      obs.forEach(function([topic, func]) {
-        //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
-        //        of `this`. Fix in a followup. (bug 583347)
-        if (add)
-          Weave.Svc.Obs.add(topic, self[func], self);
-        else
-          Weave.Svc.Obs.remove(topic, self[func], self);
-      });
-    };
-    addRem(true);
-    window.addEventListener("unload", function() addRem(false), false);
+    obs.forEach(function(topic) {
+      Services.obs.addObserver(self, topic, true);
+    });
 
     // Find the alltabs-popup, only if there is a gBrowser
     if (gBrowser) {
       let popup = document.getElementById("alltabs-popup");
       let self = this;
       popup.addEventListener("popupshowing", function() {
         self.alltabsPopupShowing();
       }, true);
     }
-
     this.updateUI();
   },
 
   _wasDelayed: false,
 
   _needsSetup: function SUI__needsSetup() {
     let firstSync = "";
     try {
@@ -237,17 +230,16 @@ let gSyncUI = {
       document.getElementById("sync-notifications-button").hidden = true;
     }
     else {
       // Display remaining notifications
       this.onNotificationAdded();
     }
   },
 
-
   // Commands
   doUpdateMenu: function SUI_doUpdateMenu(event) {
     this._updateLastSyncItem();
 
     let loginItem = document.getElementById("sync-loginitem");
     let logoutItem = document.getElementById("sync-logoutitem");
     let syncItem = document.getElementById("sync-syncnowitem");
 
@@ -364,17 +356,60 @@ let gSyncUI = {
     if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) {
       title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title");
       Weave.Notifications.removeAll(title);
       this._wasDelayed = false;
     }
 
     this.updateUI();
     this._updateLastSyncItem();
-  }
+  },
+  
+  observe: function SUI_observe(subject, topic, data) {
+    switch (topic) {
+      case "weave:service:sync:start":
+        this.onActivityStart();
+        break;
+      case "weave:service:sync:finish":
+        this.onSyncFinish();
+        break;
+      case "weave:service:sync:error":
+        this.onSyncError();
+        break;
+      case "weave:service:sync:delayed":
+        this.onSyncDelay();
+        break;
+      case "weave:service:setup-complete":
+        this.onLoginFinish();
+        break;
+      case "weave:service:login:start":
+        this.onActivityStart();
+        break;
+      case "weave:service:login:finish":
+        this.onLoginFinish();
+        break;
+      case "weave:service:login:error":
+        this.onLoginError();
+        break;
+      case "weave:service:logout:finish":
+        this.onLogout();
+        break;
+      case "weave:service:start-over":
+        this.onStartOver();
+        break;
+      case "weave:service:ready":
+        this.initUI();
+        break;
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference
+  ])
 };
 
 XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
   //XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
   //        but for now just make it work
   return Cc["@mozilla.org/intl/stringbundle;1"].
          getService(Ci.nsIStringBundleService).
          createBundle("chrome://weave/locale/services/sync.properties");
--- a/browser/base/content/browser-tabPreviews.js
+++ b/browser/base/content/browser-tabPreviews.js
@@ -210,22 +210,23 @@ var ctrlTab = {
   get canvasWidth () Math.min(tabPreviews.width,
                               Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)),
   get canvasHeight () Math.round(this.canvasWidth * tabPreviews.aspectRatio),
 
   get tabList () {
     if (this._tabList)
       return this._tabList;
 
-    var list = Array.slice(gBrowser.tabs);
+    let list = gBrowser.visibleTabs;
 
     if (this._closing)
       this.detachTab(this._closing, list);
 
-    for (let i = 0; i < gBrowser.tabContainer.selectedIndex; i++)
+    // Rotate the list until the selected tab is first
+    while (!list[0].selected)
       list.push(list.shift());
 
     if (this.recentlyUsedLimit != 0) {
       let recentlyUsedTabs = this._recentlyUsedTabs;
       if (this.recentlyUsedLimit > 0)
         recentlyUsedTabs = this._recentlyUsedTabs.slice(0, this.recentlyUsedLimit);
       for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) {
         list.splice(list.indexOf(recentlyUsedTabs[i]), 1);
@@ -457,21 +458,22 @@ var ctrlTab = {
     switch (event.keyCode) {
       case event.DOM_VK_TAB:
         if (event.ctrlKey && !event.altKey && !event.metaKey) {
           if (isOpen) {
             this.advanceFocus(!event.shiftKey);
           } else if (!event.shiftKey) {
             event.preventDefault();
             event.stopPropagation();
-            if (gBrowser.tabs.length > 2) {
+            let tabs = gBrowser.visibleTabs;
+            if (tabs.length > 2) {
               this.open();
-            } else if (gBrowser.tabs.length == 2) {
-              gBrowser.selectedTab = gBrowser.selectedTab.nextSibling ||
-                                     gBrowser.selectedTab.previousSibling;
+            } else if (tabs.length == 2) {
+              let index = gBrowser.selectedTab == tabs[0] ? 1 : 0;
+              gBrowser.selectedTab = tabs[index];
             }
           }
         }
         break;
       default:
         if (isOpen && event.ctrlKey) {
           if (event.keyCode == event.DOM_VK_DELETE) {
             this.remove(this.selected);
@@ -659,26 +661,26 @@ var allTabs = {
 
     this._currentFilter = this.filterField.value;
 
     var filter = this._currentFilter.split(/\s+/g);
     this._visible = 0;
     Array.forEach(this.previews, function (preview) {
       var tab = preview._tab;
       var matches = 0;
-      if (filter.length) {
+      if (filter.length && !tab.hidden) {
         let tabstring = tab.linkedBrowser.currentURI.spec;
         try {
           tabstring = decodeURI(tabstring);
         } catch (e) {}
         tabstring = tab.label + " " + tab.label.toLocaleLowerCase() + " " + tabstring;
         for (let i = 0; i < filter.length; i++)
           matches += tabstring.indexOf(filter[i]) > -1;
       }
-      if (matches < filter.length) {
+      if (matches < filter.length || tab.hidden) {
         preview.hidden = true;
       }
       else {
         this._visible++;
         this._updatePreview(preview);
         preview.hidden = false;
       }
     }, this);
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-tabview.js
@@ -0,0 +1,222 @@
+# ***** 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 the Tab View
+#
+# The Initial Developer of the Original Code is Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Raymond Lee <raymond@appcoast.com>
+#   Ian Gilman <ian@iangilman.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# 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 *****
+
+let TabView = {
+  _deck: null,
+  _window: null,
+  _sessionstore: null,
+  _visibilityID: "tabview-visibility",
+  
+  // ----------
+  get windowTitle() {
+    delete this.windowTitle;
+    let brandBundle = document.getElementById("bundle_brand");
+    let brandShortName = brandBundle.getString("brandShortName");
+    let title = gNavigatorBundle.getFormattedString("tabView.title", [brandShortName]);
+    return this.windowTitle = title;
+  },
+
+  // ----------
+  init: function TabView_init() {    
+    // ___ keys    
+    this._setBrowserKeyHandlers();
+
+    // ___ visibility
+    this._sessionstore =
+      Cc["@mozilla.org/browser/sessionstore;1"].
+        getService(Ci.nsISessionStore);
+
+    let data = this._sessionstore.getWindowValue(window, this._visibilityID);
+    if (data && data == "true")
+      this.show();
+  },
+
+  // ----------
+  // Creates the frame and calls the callback once it's loaded. 
+  // If the frame already exists, calls the callback immediately. 
+  _initFrame: function TabView__initFrame(callback) {
+    if (this._window) {
+      if (typeof callback == "function")
+        callback();
+    } else {
+      // ___ find the deck
+      this._deck = document.getElementById("tab-view-deck");
+      
+      // ___ create the frame
+      let iframe = document.createElement("iframe");
+      iframe.id = "tab-view";
+      iframe.setAttribute("transparent", "true");
+      iframe.flex = 1;
+              
+      if (typeof callback == "function")
+        iframe.addEventListener("DOMContentLoaded", callback, false);
+      
+      iframe.setAttribute("src", "chrome://browser/content/tabview.html");
+      this._deck.appendChild(iframe);
+      this._window = iframe.contentWindow;
+
+      // ___ visibility storage handler
+      let self = this;
+      function observer(subject, topic, data) {
+        if (topic == "quit-application-requested") {
+          let data = (self.isVisible() ? "true" : "false");
+          self._sessionstore.setWindowValue(window, self._visibilityID, data);
+        }
+      }
+      
+      Services.obs.addObserver(observer, "quit-application-requested", false);
+    }
+  },
+
+  // ----------
+  isVisible: function() {
+    return (this._deck ? this._deck.selectedIndex == 1 : false);
+  },
+
+  // ----------
+  show: function() {
+    if (this.isVisible())
+      return;
+    
+    this._initFrame(function() {
+      let event = document.createEvent("Events");
+      event.initEvent("tabviewshow", false, false);
+      dispatchEvent(event);
+    });
+  },
+
+  // ----------
+  hide: function() {
+    if (!this.isVisible())
+      return;
+
+    let event = document.createEvent("Events");
+    event.initEvent("tabviewhide", false, false);
+    dispatchEvent(event);
+  },
+
+  // ----------
+  toggle: function() {
+    if (this.isVisible())
+      this.hide();
+    else 
+      this.show();
+  },
+
+  // ----------
+  updateContextMenu: function(tab, popup) {
+    let isEmpty = true;
+
+    while(popup.lastChild && popup.lastChild.id != "context_namedGroups")
+      popup.removeChild(popup.lastChild);
+
+    let self = this;
+    this._initFrame(function() {
+      let activeGroup = tab.tabItem.parent;
+      let groupItems = self._window.GroupItems.groupItems;
+  
+      groupItems.forEach(function(groupItem) { 
+        if (groupItem.getTitle().length > 0 && 
+            (!activeGroup || activeGroup.id != groupItem.id)) {
+          let menuItem = self._createGroupMenuItem(groupItem);
+          popup.appendChild(menuItem);
+          isEmpty = false;
+        }
+      });
+      document.getElementById("context_namedGroups").hidden = isEmpty;
+    });
+  },
+
+  // ----------
+  _createGroupMenuItem : function(groupItem) {
+    let menuItem = document.createElement("menuitem")
+    menuItem.setAttribute("class", "group");
+    menuItem.setAttribute("label", groupItem.getTitle());
+    menuItem.setAttribute(
+      "oncommand", 
+      "TabView.moveTabTo(TabContextMenu.contextTab,'" + groupItem.id + "')");
+
+    return menuItem;
+  },
+
+  // ----------
+  moveTabTo: function(tab, groupItemId) {
+    if (this._window)
+      this._window.GroupItems.moveTabToGroupItem(tab, groupItemId);
+  },
+
+  // ----------
+  // Adds new key commands to the browser, for invoking the Tab Candy UI
+  // and for switching between groups of tabs when outside of the Tab Candy UI.
+  _setBrowserKeyHandlers : function() {
+    let self = this;
+
+    window.addEventListener("keypress", function(event) {
+      if (self.isVisible())
+        return;
+
+      let charCode = event.charCode;
+#ifdef XP_MACOSX
+      // if a text box in a webpage has the focus, the event.altKey would
+      // return false so we are depending on the charCode here.
+      if (!event.ctrlKey && !event.metaKey && !event.shiftKey &&
+          charCode == 160) { // alt + space
+#else
+      if (event.ctrlKey && !event.metaKey && !event.shiftKey &&
+          event.altKey && charCode == 32) { // ctrl + alt + space
+#endif
+        event.stopPropagation();
+        event.preventDefault();
+        self.show();
+        return;
+      }
+
+      // Control (+ Shift) + `
+      if (event.ctrlKey && !event.metaKey && !event.altKey &&
+          (charCode == 96 || charCode == 126)) {
+        event.stopPropagation();
+        event.preventDefault();
+
+        self._initFrame(function() {
+          let tabItem = self._window.GroupItems.getNextGroupItemTab(event.shiftKey);
+          if (tabItem)
+            window.gBrowser.selectedTab = tabItem.tab;
+        });
+      }
+    }, true);
+  }
+};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -173,16 +173,17 @@ let gInitialPages = [
   "about:privatebrowsing",
   "about:sessionrestore"
 ];
 
 #include browser-fullZoom.js
 #include inspector.js
 #include browser-places.js
 #include browser-tabPreviews.js
+#include browser-tabview.js
 
 #ifdef MOZ_SERVICES_SYNC
 #include browser-syncui.js
 #endif
 
 XPCOMUtils.defineLazyGetter(this, "Win7Features", function () {
 #ifdef XP_WIN
 #ifndef WINCE
@@ -1519,16 +1520,18 @@ function delayedStartup(isLoadingBlank, 
   if (Win7Features)
     Win7Features.onOpenWindow();
 
 #ifdef MOZ_SERVICES_SYNC
   // initialize the sync UI
   gSyncUI.init();
 #endif
 
+  TabView.init();
+
   Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
 }
 
 function BrowserShutdown()
 {
   if (Win7Features)
     Win7Features.onCloseWindow();
 
@@ -6764,39 +6767,39 @@ function formatURL(aFormat, aIsPref) {
  * This also takes care of updating the command enabled-state when tabs are
  * created or removed.
  */
 var gBookmarkAllTabsHandler = {
   init: function () {
     this._command = document.getElementById("Browser:BookmarkAllTabs");
     gBrowser.tabContainer.addEventListener("TabOpen", this, true);
     gBrowser.tabContainer.addEventListener("TabClose", this, true);
+    gBrowser.tabContainer.addEventListener("TabSelect", this, true);
+    gBrowser.tabContainer.addEventListener("TabMove", this, true);
     this._updateCommandState();
   },
 
-  _updateCommandState: function BATH__updateCommandState(aTabClose) {
-    var numTabs = gBrowser.tabs.length;
-
-    // The TabClose event is fired before the tab is removed from the DOM
-    if (aTabClose)
-      numTabs--;
-
-    if (numTabs > 1)
+  _updateCommandState: function BATH__updateCommandState() {
+    let remainingTabs = gBrowser.visibleTabs.filter(function(tab) {
+      return gBrowser._removingTabs.indexOf(tab) == -1;
+    });
+
+    if (remainingTabs.length > 1)
       this._command.removeAttribute("disabled");
     else
       this._command.setAttribute("disabled", "true");
   },
 
   doCommand: function BATH_doCommand() {
     PlacesCommandHook.bookmarkCurrentPages();
   },
 
   // nsIDOMEventListener
   handleEvent: function(aEvent) {
-    this._updateCommandState(aEvent.type == "TabClose");
+    this._updateCommandState();
   }
 };
 
 /**
  * Utility object to handle manipulations of the identity indicators in the UI
  */
 var gIdentityHandler = {
   // Mode strings used to control CSS display
@@ -7794,17 +7797,17 @@ function switchToTabHavingURI(aURI, aOpe
   return false;
 }
 
 var TabContextMenu = {
   contextTab: null,
   updateContextMenu: function updateContextMenu(aPopupMenu) {
     this.contextTab = document.popupNode.localName == "tab" ?
                       document.popupNode : gBrowser.selectedTab;
-    var disabled = gBrowser.tabs.length == 1;
+    let disabled = gBrowser.visibleTabs.length == 1;
 
     // Enable the "Close Tab" menuitem when the window doesn't close with the last tab.
     document.getElementById("context_closeTab").disabled =
       disabled && gBrowser.tabContainer._closeWindowWithLastTab;
 
     var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
     for (var i = 0; i < menuItems.length; i++)
       menuItems[i].disabled = disabled;
@@ -7816,17 +7819,17 @@ var TabContextMenu = {
       getClosedTabCount(window) == 0;
 
     // Only one of pin/unpin should be visible
     document.getElementById("context_pinTab").hidden = this.contextTab.pinned;
     document.getElementById("context_unpinTab").hidden = !this.contextTab.pinned;
 
     // Disable "Close other Tabs" if there is only one unpinned tab and
     // hide it when the user rightclicked on a pinned tab.
-    var unpinnedTabs = gBrowser.tabs.length - gBrowser._numPinnedTabs;
+    let unpinnedTabs = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs;
     document.getElementById("context_closeOtherTabs").disabled = unpinnedTabs <= 1;
     document.getElementById("context_closeOtherTabs").hidden = this.contextTab.pinned;
   }
 };
 
 XPCOMUtils.defineLazyGetter(this, "HUDConsoleUI", function () {
   Cu.import("resource://gre/modules/HUDService.jsm");
   try {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -97,24 +97,39 @@
 
 #ifdef MOZ_SAFE_BROWSING
 <script type="application/javascript" src="chrome://browser/content/safebrowsing/sb-loader.js"/>
 #endif
 <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
 
 <script type="application/javascript" src="chrome://browser/content/places/editBookmarkOverlay.js"/>
 
+<deck flex="1" id="tab-view-deck">
+<vbox flex="1">
+
 # All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the 
 # browser-sets.inc file for sharing with hiddenWindow.xul.
 #include browser-sets.inc
 
   <popupset id="mainPopupSet">
     <menupopup id="tabContextMenu"
                onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);"
                onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;">
+      <menu id="context_tabViewMenu" class="menu-iconic" label="&moveTabTo.label;"
+            accesskey="&moveTabTo.accesskey;">
+        <menupopup id="context_tabViewMenuPopup" 
+                   onpopupshowing="if (event.target == this) TabView.updateContextMenu(TabContextMenu.contextTab, this);">
+          <menuitem label="&createNewGroup.label;" 
+                    accesskey="&createNewGroup.accesskey;" 
+                    oncommand="TabView.moveTabTo(TabContextMenu.contextTab, null);" />
+          <menuitem id="context_namedGroups" label="&namedGroups.label;" 
+                    disabled="true" />
+        </menupopup>
+      </menu>
+      <menuseparator/>
       <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;"
                 oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.reloadAllTabs();"/>
       <menuseparator/>
       <menuitem id="context_openTabInWindow" label="&openTabInNewWindow.label;"
                 accesskey="&openTabInNewWindow.accesskey;"
@@ -888,17 +903,17 @@
 
     <toolbar id="TabsToolbar"
              fullscreentoolbar="true"
              customizable="true"
              mode="icons" lockmode="true"
              iconsize="small" defaulticonsize="small" lockiconsize="true"
              aria-label="&tabsToolbar.label;"
              context="toolbar-context-menu"
-             defaultset="tabbrowser-tabs,new-tab-button,alltabs-button,tabs-closebutton"
+             defaultset="tabbrowser-tabs,new-tab-button,tabview-button,alltabs-button,tabs-closebutton"
              collapsed="true">
 
       <tabs id="tabbrowser-tabs"
             class="tabbrowser-tabs"
             tabbrowser="content"
             flex="1"
             setfocus="false"
             tooltip="tabbrowser-tab-tooltip">
@@ -922,16 +937,22 @@
                      type="menu"
                      label="&listAllTabs.label;"
                      tooltiptext="&listAllTabs.label;"
                      removable="true">
         <menupopup id="alltabs-popup"
                    position="after_end"/>
       </toolbarbutton>
 
+      <toolbarbutton id="tabview-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+                     label="&tabViewButton.label;"
+                     command="Browser:ToggleTabView"
+                     tooltiptext="&tabViewButton.tooltip;"
+                     removable="true"/>
+
       <toolbarbutton id="tabs-closebutton"
                      class="close-button tabs-closebutton"
                      command="cmd_close"
                      label="&closeTab.label;"
                      tooltiptext="&closeTab.label;"/>
 
     </toolbar>
 
@@ -1035,17 +1056,17 @@
       <statusbarpanel id="download-monitor" class="statusbarpanel-iconic-text"
                       tooltiptext="&downloadMonitor2.tooltip;" hidden="true"
                       command="Tools:Downloads"/>
       <statusbarpanel id="security-button" class="statusbarpanel-iconic"
                       hidden="true"
                       onclick="if (event.button == 0 &amp;&amp; event.detail == 1) displaySecurityInfo();"/>
 #ifdef MOZ_SERVICES_SYNC
       <statusbarpanel id="sync-status-button"
-                      class="statusbarpanel-iconic-text"
+                      class="statusbarpanel-iconic"
                       image="chrome://browser/skin/sync-16.png"
                       label="&syncLogInItem.label;"
                       oncommand="gSyncUI.handleStatusbarButton();"
                       onmousedown="event.preventDefault();">
       </statusbarpanel>
       <separator class="thin"/>
       <statusbarpanel id="sync-notifications-button"
                       class="statusbarpanel-iconic-text"
@@ -1076,9 +1097,16 @@
       <svg:circle cx="-0.46" cy="0.5" r="0.63"/>
     </svg:mask>
     <svg:mask id="winstripe-keyhole-forward-mask-hover" maskContentUnits="objectBoundingBox">
       <svg:rect x="0" y="0" width="1" height="1" fill="white"/>
       <svg:circle cx="-0.35" cy="0.5" r="0.58"/>
     </svg:mask>
   </svg:svg>
 #endif
+
+</vbox>
+# <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck.
+#     Introducing the iframe dynamically, as needed, was found to be better than
+#     starting with an empty iframe here in browser.xul from a Ts standpoint.
+</deck>
+
 </window>
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -83,16 +83,18 @@
                 onget="return this.tabContainer.contextMenu;"/>
 
       <field name="tabContainer" readonly="true">
         document.getElementById(this.getAttribute("tabcontainer"));
       </field>
       <field name="tabs" readonly="true">
         this.tabContainer.childNodes;
       </field>
+      <property name="visibleTabs" readonly="true"
+                onget="return Array.filter(this.tabs, function(tab) !tab.hidden);"/>
       <field name="mURIFixup" readonly="true">
         Components.classes["@mozilla.org/docshell/urifixup;1"]
                   .getService(Components.interfaces.nsIURIFixup);
       </field>
       <field name="mFaviconService" readonly="true">
         Components.classes["@mozilla.org/browser/favicon-service;1"]
                   .getService(Components.interfaces.nsIFaviconService);
       </field>
@@ -716,17 +718,23 @@
             return newTitle;
           ]]>
         </body>
       </method>
 
       <method name="updateTitlebar">
         <body>
           <![CDATA[
-            this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
+            if (TabView && TabView.isVisible()) {
+              // ToDo: this will be removed when we gain ability to draw to the menu bar.
+              // Bug 586175
+              this.ownerDocument.title = TabView.windowTitle;
+            } else {
+              this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
+            }
           ]]>
         </body>
       </method>
 
       <method name="updateCurrentBrowser">
         <parameter name="aForceUpdate"/>
         <body>
           <![CDATA[
@@ -753,16 +761,17 @@
                 (oldBrowser.pageReport && !newBrowser.pageReport) ||
                 (!oldBrowser.pageReport && newBrowser.pageReport))
               updatePageReport = true;
 
             newBrowser.setAttribute("type", "content-primary");
             newBrowser.docShell.isActive = true;
             this.mCurrentBrowser = newBrowser;
             this.mCurrentTab = this.selectedTab;
+            this.mCurrentTab.hidden = false;
 
             if (updatePageReport)
               this.mCurrentBrowser.updatePageReport();
 
             // Update the URL bar.
             var loc = this.mCurrentBrowser.currentURI;
 
             var webProgress = this.mCurrentBrowser.webProgress;
@@ -1011,29 +1020,30 @@
           // the next of many URLs opened) or if the pref to have UI links opened in
           // the background is set (i.e. the link is not being opened modally)
           //
           // i.e.
           //    Number of URLs    Load UI Links in BG       Focus Last Viewed?
           //    == 1              false                     YES
           //    == 1              true                      NO
           //    > 1               false/true                NO
-          var owner = (aURIs.length > 1) || aLoadInBackground ? null : this.selectedTab;
+          var multiple = aURIs.length > 1;
+          var owner = multiple || aLoadInBackground ? null : this.selectedTab;
           var firstTabAdded = null;
 
           if (aReplace) {
             try {
               this.loadURI(aURIs[0], null, null);
             } catch (e) {
               // Ignore failure in case a URI is wrong, so we can continue
               // opening the next ones.
             }
           }
           else
-            firstTabAdded = this.addTab(aURIs[0], {ownerTab: owner, skipAnimation: true});
+            firstTabAdded = this.addTab(aURIs[0], {ownerTab: owner, skipAnimation: multiple});
 
           var tabNum = this.tabContainer.selectedIndex;
           for (let i = 1; i < aURIs.length; ++i) {
             let tab = this.addTab(aURIs[i], {skipAnimation: true});
             if (aReplace)
               this.moveTabTo(tab, ++tabNum);
           }
 
@@ -1238,17 +1248,17 @@
 
       <method name="warnAboutClosingTabs">
       <parameter name="aAll"/>
       <body>
         <![CDATA[
           var tabsToClose = this.tabs.length;
 
           if (!aAll)
-            tabsToClose -= 1 + gBrowser._numPinnedTabs;
+            tabsToClose = this.visibleTabs.length - (1 + this._numPinnedTabs);
           if (tabsToClose <= 1)
             return true;
 
           const pref = "browser.tabs.warnOnClose";
           var shouldPrompt = Services.prefs.getBoolPref(pref);
 
           if (!shouldPrompt)
             return true;
@@ -1289,21 +1299,22 @@
       <method name="removeAllTabsBut">
         <parameter name="aTab"/>
         <body>
           <![CDATA[
             if (aTab.pinned)
               return;
 
             if (this.warnAboutClosingTabs(false)) {
+              let tabs = this.visibleTabs;
               this.selectedTab = aTab;
 
-              for (let i = this.tabs.length - 1; i >= 0; --i) {
-                if (this.tabs[i] != aTab && !this.tabs[i].pinned)
-                  this.removeTab(this.tabs[i]);
+              for (let i = tabs.length - 1; i >= 0; --i) {
+                if (tabs[i] != aTab && !tabs[i].pinned)
+                  this.removeTab(tabs[i]);
               }
             }
           ]]>
         </body>
       </method>
 
       <method name="removeCurrentTab">
         <parameter name="aParams"/>
@@ -1318,20 +1329,29 @@
         []
       </field>
 
       <method name="removeTab">
         <parameter name="aTab"/>
         <parameter name="aParams"/>
         <body>
           <![CDATA[
-            var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
             if (aParams)
               var animate = aParams.animate;
 
+            // Handle requests for synchronously removing an already
+            // asynchronously closing tab.
+            if (!animate &&
+                this._removingTabs.indexOf(aTab) > -1) {
+              this._endRemoveTab(aTab);
+              return;
+            }
+
+            var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
+
             if (!this._beginRemoveTab(aTab, false, null, true))
               return;
 
             /* Don't animate if:
                 - the caller didn't opt in
                 - this is the last tab in the window
                 - this is a pinned tab
                 - a bunch of other tabs are already closing (arbitrary threshold)
@@ -1375,17 +1395,18 @@
             if (!aTabWillBeMoved) {
               let ds = browser.docShell;
               if (ds && ds.contentViewer && !ds.contentViewer.permitUnload())
                 return false;
             }
 
             var closeWindow = false;
             var newTab = false;
-            if (this.tabs.length - this._removingTabs.length == 1) {
+            if (this.tabs.length - this._removingTabs.length == 1 && 
+                (!TabView || !TabView.isVisible())) {
               closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
                             !window.toolbar.visible ||
                               this.tabContainer._closeWindowWithLastTab;
 
               // Closing the tab and replacing it with a blank one is notably slower
               // than closing the window right away. If the caller opts in, take
               // the fast path.
               if (closeWindow &&
@@ -1559,28 +1580,39 @@
 
             if (aTab.owner &&
                 this._removingTabs.indexOf(aTab.owner) == -1 &&
                 Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
               this.selectedTab = aTab.owner;
               return;
             }
 
+            let removing = this._removingTabs;
+            function keepRemaining(tab) {
+              // A tab remains only if it's not being removed nor blurred
+              return removing.indexOf(tab) == -1 && tab != aTab;
+            }
+
+            // Switch to a visible tab unless there aren't any remaining
+            let remainingTabs = this.visibleTabs.filter(keepRemaining);
+            if (remainingTabs.length == 0)
+              remainingTabs = Array.filter(this.tabs, keepRemaining);
+
+            // Try to find a remaining tab that comes after the given tab
             var tab = aTab;
-
             do {
               tab = tab.nextSibling;
-            } while (tab && this._removingTabs.indexOf(tab) != -1);
+            } while (tab && remainingTabs.indexOf(tab) == -1);
 
             if (!tab) {
               tab = aTab;
 
               do {
                 tab = tab.previousSibling;
-              } while (tab && this._removingTabs.indexOf(tab) != -1);
+              } while (tab && remainingTabs.indexOf(tab) == -1);
             }
 
             this.selectedTab = tab;
           ]]>
         </body>
       </method>
 
       <method name="swapBrowsersAndCloseOther">
@@ -1646,20 +1678,21 @@
               this.updateCurrentBrowser(true);
           ]]>
         </body>
       </method>
 
       <method name="reloadAllTabs">
         <body>
           <![CDATA[
-            var l = this.mPanelContainer.childNodes.length;
+            let tabs = this.visibleTabs;
+            let l = tabs.length;
             for (var i = 0; i < l; i++) {
               try {
-                this.getBrowserAtIndex(i).reload();
+                this.getBrowserForTab(tabs[i]).reload();
               } catch (e) {
                 // ignore failure to reload so others will be reloaded
               }
             }
           ]]>
         </body>
       </method>
 
@@ -1746,29 +1779,40 @@
         <parameter name="aTab"/>
         <body>
         <![CDATA[
           return aTab.linkedBrowser;
         ]]>
         </body>
       </method>
 
+      <method name="showOnlyTheseTabs">
+        <parameter name="aTabs"/>
+        <body>
+        <![CDATA[
+          Array.forEach(this.tabs, function(tab) {
+            tab.hidden = aTabs.indexOf(tab) == -1 && !tab.pinned && !tab.selected;
+          });
+        ]]>
+        </body>
+      </method>
+
       <method name="selectTabAtIndex">
         <parameter name="aIndex"/>
         <parameter name="aEvent"/>
         <body>
         <![CDATA[
+          let tabs = this.visibleTabs;
+
           // count backwards for aIndex < 0
           if (aIndex < 0)
-            aIndex += this.tabs.length;
-
-          if (aIndex >= 0 &&
-              aIndex < this.tabs.length &&
-              aIndex != this.tabContainer.selectedIndex)
-            this.selectedTab = this.tabs[aIndex];
+            aIndex += tabs.length;
+
+          if (aIndex >= 0 && aIndex < tabs.length)
+            this.selectedTab = tabs[aIndex];
 
           if (aEvent) {
             aEvent.preventDefault();
             aEvent.stopPropagation();
           }
         ]]>
         </body>
       </method>
@@ -1800,17 +1844,17 @@
       </property>
 
       <!-- Moves a tab to a new browser window, unless it's already the only tab
            in the current window, in which case this will do nothing. -->
       <method name="replaceTabWithWindow">
         <parameter name="aTab"/>
         <body>
           <![CDATA[
-            if (this.tabs.length == 1)
+            if (this.visibleTabs.length == 1)
               return null;
 
             // tell a new window to take the "dropped" tab
             return Services.ww.openWindow(window,
                                           getBrowserURL(),
                                           null,
                                           "chrome,dialog=no,all",
                                           aTab);
@@ -2385,31 +2429,31 @@
       <method name="_getScrollableElements">
         <body><![CDATA[
           return Array.filter(document.getBindingParent(this).childNodes,
                               this._canScrollToElement, this);
         ]]></body>
       </method>
       <method name="_canScrollToElement">
         <parameter name="tab"/>
-        <body>
-          return !tab.pinned;
-        </body>
+        <body><![CDATA[
+          return !tab.pinned && !tab.hidden;
+        ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="underflow"><![CDATA[
          if (event.detail == 0)
            return; // Ignore vertical events
 
          var tabs = document.getBindingParent(this);
          tabs.removeAttribute("overflow");
 
-         tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser._endRemoveTab,
+         tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser.removeTab,
                                                tabs.tabbrowser);
 
          tabs._positionPinnedTabs();
       ]]></handler>
       <handler event="overflow"><![CDATA[
          if (event.detail == 0)
            return; // Ignore vertical events
 
@@ -2564,17 +2608,18 @@
               this.setAttribute("closebuttons", "noclose");
             else
               this.setAttribute("closebuttons", "activetab");
             break;
           case 1:
             if (this.childNodes.length == 1 && this._closeWindowWithLastTab)
               this.setAttribute("closebuttons", "noclose");
             else {
-              let tab = this.childNodes.item(this.tabbrowser._numPinnedTabs);
+              // Grab the last tab for size comparison
+              let tab = this.tabbrowser.visibleTabs.pop();
               if (tab && tab.getBoundingClientRect().width > this.mTabClipWidth)
                 this.setAttribute("closebuttons", "alltabs");
               else
                 this.setAttribute("closebuttons", "activetab");
             }
             break;
           case 2:
           case 3:
@@ -3361,17 +3406,17 @@
       </method>
     </implementation>
 
     <handlers>
       <handler event="popupshowing">
       <![CDATA[
         // set up the menu popup
         var tabcontainer = gBrowser.tabContainer;
-        var tabs = tabcontainer.childNodes;
+        let tabs = gBrowser.visibleTabs;
 
         // Listen for changes in the tab bar.
         tabcontainer.addEventListener("TabOpen", this, false);
         tabcontainer.addEventListener("TabAttrModified", this, false);
         tabcontainer.addEventListener("TabClose", this, false);
         tabcontainer.mTabstrip.addEventListener("scroll", this, false);
 
         for (var i = 0; i < tabs.length; i++) {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/drag.js
@@ -0,0 +1,296 @@
+/* ***** 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 drag.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: drag.js
+
+// ----------
+// Variable: drag
+// The Drag that's currently in process.
+var drag = {
+  info: null,
+  zIndex: 100
+};
+
+
+// ##########
+// Class: Drag (formerly DragInfo)
+// Helper class for dragging <Item>s
+//
+// ----------
+// Constructor: Drag
+// Called to create a Drag in response to an <Item> draggable "start" event.
+// Note that it is also used partially during <Item>'s resizable method as well.
+//
+// Parameters:
+//   item - The <Item> being dragged
+//   event - The DOM event that kicks off the drag
+//   isResizing - (boolean) is this a resizing instance? or (if false) dragging?
+//   isFauxDrag - (boolean) true if a faux drag, which is used when simply snapping.
+var Drag = function(item, event, isResizing, isFauxDrag) {
+  Utils.assert(item && (item.isAnItem || item.isAFauxItem), 
+      'must be an item, or at least a faux item');
+
+  this.isResizing = isResizing || false;
+  this.item = item;
+  this.el = item.container;
+  this.$el = iQ(this.el);
+  this.parent = this.item.parent;
+  this.startPosition = new Point(event.clientX, event.clientY);
+  this.startTime = Date.now();
+
+  this.item.isDragging = true;
+  this.item.setZ(999999);
+
+  this.safeWindowBounds = Items.getSafeWindowBounds();
+
+  Trenches.activateOthersTrenches(this.el);
+
+  if (!isFauxDrag) {
+    // When a tab drag starts, make it the focused tab.
+    if (this.item.isAGroupItem) {
+      var tab = UI.getActiveTab();
+      if (!tab || tab.parent != this.item) {
+        if (this.item._children.length)
+          UI.setActiveTab(this.item._children[0]);
+      }
+    } else if (this.item.isATabItem) {
+      UI.setActiveTab(this.item);
+    }
+  }
+};
+
+Drag.prototype = {
+  // ----------
+  // Function: snapBounds
+  // Adjusts the given bounds according to the currently active trenches. Used by <Drag.snap>
+  //
+  // Parameters:
+  //   bounds             - (<Rect>) bounds
+  //   stationaryCorner   - which corner is stationary? by default, the top left.
+  //                        "topleft", "bottomleft", "topright", "bottomright"
+  //   assumeConstantSize - (boolean) whether the bounds' dimensions are sacred or not.
+  //   keepProportional   - (boolean) if assumeConstantSize is false, whether we should resize
+  //                        proportionally or not
+  //   checkItemStatus    - (boolean) make sure this is a valid item which should be snapped
+  snapBounds: function Drag_snapBounds(bounds, stationaryCorner, assumeConstantSize, keepProportional, checkItemStatus) {
+		if (!stationaryCorner)
+			stationaryCorner || 'topleft';
+    var update = false; // need to update
+    var updateX = false;
+    var updateY = false;
+    var newRect;
+    var snappedTrenches = {};
+
+    // OH SNAP!
+
+    // if we aren't holding down the meta key...
+    if (!Keys.meta) {
+      // snappable = true if we aren't a tab on top of something else, and
+      // there's no active drop site...
+      let snappable = !(this.item.isATabItem &&
+                       this.item.overlapsWithOtherItems()) &&
+                       !iQ(".acceptsDrop").length;
+      if (!checkItemStatus || snappable) {
+        newRect = Trenches.snap(bounds, stationaryCorner, assumeConstantSize,
+                                keepProportional);
+        if (newRect) { // might be false if no changes were made
+          update = true;
+          snappedTrenches = newRect.snappedTrenches || {};
+          bounds = newRect;
+        }
+      }
+    }
+
+    // make sure the bounds are in the window.
+    newRect = this.snapToEdge(bounds, stationaryCorner, assumeConstantSize,
+                              keepProportional);
+    if (newRect) {
+      update = true;
+      bounds = newRect;
+      Utils.extend(snappedTrenches, newRect.snappedTrenches);
+    }
+
+    Trenches.hideGuides();
+    for (var edge in snappedTrenches) {
+      var trench = snappedTrenches[edge];
+      if (typeof trench == 'object') {
+        trench.showGuide = true;
+        trench.show();
+      }
+    }
+
+    return update ? bounds : false;
+  },
+
+  // ----------
+  // Function: snap
+  // Called when a drag or mousemove occurs. Set the bounds based on the mouse move first, then
+  // call snap and it will adjust the item's bounds if appropriate. Also triggers the display of
+  // trenches that it snapped to.
+  //
+  // Parameters:
+  //   stationaryCorner   - which corner is stationary? by default, the top left.
+  //                        "topleft", "bottomleft", "topright", "bottomright"
+  //   assumeConstantSize - (boolean) whether the bounds' dimensions are sacred or not.
+  //   keepProportional   - (boolean) if assumeConstantSize is false, whether we should resize
+  //                        proportionally or not
+  snap: function Drag_snap(stationaryCorner, assumeConstantSize, keepProportional) {
+    var bounds = this.item.getBounds();
+    bounds = this.snapBounds(bounds, stationaryCorner, assumeConstantSize, keepProportional, true);
+    if (bounds) {
+      this.item.setBounds(bounds, true);
+      return true;
+    }
+    return false;
+  },
+
+  // --------
+  // Function: snapToEdge
+  // Returns a version of the bounds snapped to the edge if it is close enough. If not,
+  // returns false. If <Keys.meta> is true, this function will simply enforce the
+  // window edges.
+  //
+  // Parameters:
+  //   rect - (<Rect>) current bounds of the object
+  //   stationaryCorner   - which corner is stationary? by default, the top left.
+  //                        "topleft", "bottomleft", "topright", "bottomright"
+  //   assumeConstantSize - (boolean) whether the rect's dimensions are sacred or not
+  //   keepProportional   - (boolean) if we are allowed to change the rect's size, whether the
+  //                                  dimensions should scaled proportionally or not.
+  snapToEdge: function Drag_snapToEdge(rect, stationaryCorner, assumeConstantSize, keepProportional) {
+
+    var swb = this.safeWindowBounds;
+    var update = false;
+    var updateX = false;
+    var updateY = false;
+    var snappedTrenches = {};
+
+    var snapRadius = (Keys.meta ? 0 : Trenches.defaultRadius);
+    if (rect.left < swb.left + snapRadius ) {
+      if (stationaryCorner.indexOf('right') > -1)
+        rect.width = rect.right - swb.left;
+      rect.left = swb.left;
+      update = true;
+      updateX = true;
+      snappedTrenches.left = 'edge';
+    }
+
+    if (rect.right > swb.right - snapRadius) {
+      if (updateX || !assumeConstantSize) {
+        var newWidth = swb.right - rect.left;
+        if (keepProportional)
+          rect.height = rect.height * newWidth / rect.width;
+        rect.width = newWidth;
+        update = true;
+      } else if (!updateX || !Trenches.preferLeft) {
+        rect.left = swb.right - rect.width;
+        update = true;
+      }
+      snappedTrenches.right = 'edge';
+      delete snappedTrenches.left;
+    }
+    if (rect.top < swb.top + snapRadius) {
+      if (stationaryCorner.indexOf('bottom') > -1)
+        rect.height = rect.bottom - swb.top;
+      rect.top = swb.top;
+      update = true;
+      updateY = true;
+      snappedTrenches.top = 'edge';
+    }
+    if (rect.bottom > swb.bottom - snapRadius) {
+      if (updateY || !assumeConstantSize) {
+        var newHeight = swb.bottom - rect.top;
+        if (keepProportional)
+          rect.width = rect.width * newHeight / rect.height;
+        rect.height = newHeight;
+        update = true;
+      } else if (!updateY || !Trenches.preferTop) {
+        rect.top = swb.bottom - rect.height;
+        update = true;
+      }
+      snappedTrenches.top = 'edge';
+      delete snappedTrenches.bottom;
+    }
+
+    if (update) {
+      rect.snappedTrenches = snappedTrenches;
+      return rect;
+    }
+    return false;
+  },
+
+  // ----------
+  // Function: drag
+  // Called in response to an <Item> draggable "drag" event.
+  drag: function(event) {
+    this.snap('topleft', true);
+
+    if (this.parent && this.parent.expanded) {
+      var distance = this.startPosition.distance(new Point(event.clientX, event.clientY));
+      if (distance > 100) {
+        this.parent.remove(this.item);
+        this.parent.collapse();
+      }
+    }
+  },
+
+  // ----------
+  // Function: stop
+  // Called in response to an <Item> draggable "stop" event.
+  stop: function() {
+    Trenches.hideGuides();
+    this.item.isDragging = false;
+
+    if (this.parent && !this.parent.locked.close && this.parent != this.item.parent &&
+       this.parent.isEmpty()) {
+      this.parent.close();
+    }
+
+    if (this.parent && this.parent.expanded)
+      this.parent.arrange();
+
+    if (this.item && !this.item.parent) {
+      this.item.setZ(drag.zIndex);
+      drag.zIndex++;
+
+      this.item.pushAway();
+    }
+
+    Trenches.disactivate();
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/groupitems.js
@@ -0,0 +1,1807 @@
+/* ***** 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 groupItems.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ * Ehsan Akhgari <ehsan@mozilla.com>
+ * Raymond Lee <raymond@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: groupItems.js
+
+// ##########
+// Class: GroupItem
+// A single groupItem in the TabView window. Descended from <Item>.
+// Note that it implements the <Subscribable> interface.
+//
+// ----------
+// Constructor: GroupItem
+//
+// Parameters:
+//   listOfEls - an array of DOM elements for tabs to be added to this groupItem
+//   options - various options for this groupItem (see below). In addition, gets passed
+//     to <add> along with the elements provided.
+//
+// Possible options:
+//   id - specifies the groupItem's id; otherwise automatically generated
+//   locked - see <Item.locked>; default is {}
+//   userSize - see <Item.userSize>; default is null
+//   bounds - a <Rect>; otherwise based on the locations of the provided elements
+//   container - a DOM element to use as the container for this groupItem; otherwise will create
+//   title - the title for the groupItem; otherwise blank
+//   dontPush - true if this groupItem shouldn't push away on creation; default is false
+let GroupItem = function GroupItem(listOfEls, options) {
+  try {
+  if (typeof options == 'undefined')
+    options = {};
+
+  this._inited = false;
+  this._children = []; // an array of Items
+  this.defaultSize = new Point(TabItems.tabWidth * 1.5, TabItems.tabHeight * 1.5);
+  this.isAGroupItem = true;
+  this.id = options.id || GroupItems.getNextID();
+  this._isStacked = false;
+  this._stackAngles = [0];
+  this.expanded = null;
+  this.locked = (options.locked ? Utils.copy(options.locked) : {});
+  this.topChild = null;
+
+  this.keepProportional = false;
+
+  // Variable: _activeTab
+  // The <TabItem> for the groupItem's active tab.
+  this._activeTab = null;
+
+  // Variables: xDensity, yDensity
+  // "density" ranges from 0 to 1, with 0 being "not dense" = "squishable" and 1 being "dense"
+  // = "not squishable". For example, if there is extra space in the vertical direction,
+  // yDensity will be < 1. These are set by <GroupItem.arrange>, as it is dependent on the tab items
+  // inside the groupItem.
+  this.xDensity = 0;
+  this.yDensity = 0;
+
+  if (Utils.isPoint(options.userSize))
+    this.userSize = new Point(options.userSize);
+
+  var self = this;
+
+  var rectToBe;
+  if (options.bounds) {
+    Utils.assert(Utils.isRect(options.bounds), "options.bounds must be a Rect");
+    rectToBe = new Rect(options.bounds);
+  }
+
+  if (!rectToBe) {
+    rectToBe = GroupItems.getBoundingBox(listOfEls);
+    rectToBe.inset(-30, -30);
+  }
+
+  var $container = options.container;
+  if (!$container) {
+    $container = iQ('<div>')
+      .addClass('groupItem')
+      .css({position: 'absolute'})
+      .css(rectToBe);
+  }
+
+  this.bounds = $container.bounds();
+
+  this.isDragging = false;
+  $container
+    .css({zIndex: -100})
+    .appendTo("body");
+
+  // ___ New Tab Button
+  this.$ntb = iQ("<div>")
+    .appendTo($container);
+
+  this.$ntb
+    .addClass('newTabButton')
+    .click(function() {
+      self.newTab();
+    });
+
+  (this.$ntb)[0].title = 'New tab';
+
+  // ___ Resizer
+  this.$resizer = iQ("<div>")
+    .addClass('resizer')
+    .appendTo($container)
+    .hide();
+
+  // ___ Titlebar
+  var html =
+    "<div class='title-container'>" +
+      "<input class='name'/>" +
+      "<div class='title-shield' />" +
+    "</div>";
+
+  this.$titlebar = iQ('<div>')
+    .addClass('titlebar')
+    .html(html)
+    .appendTo($container);
+
+  this.$titlebar.css({
+      position: "absolute",
+    });
+
+  var $close = iQ('<div>')
+    .addClass('close')
+    .click(function() {
+      self.closeAll();
+    })
+    .appendTo($container);
+
+  // ___ Title
+  this.$titleContainer = iQ('.title-container', this.$titlebar);
+  this.$title = iQ('.name', this.$titlebar);
+  this.$titleShield = iQ('.title-shield', this.$titlebar);
+  this.setTitle(options.title || "");
+
+  var titleUnfocus = function() {
+    self.$titleShield.show();
+    if (!self.getTitle()) {
+      self.$title
+        .addClass("defaultName")
+        .val(self.defaultName);
+    } else {
+      self.$title
+        .css({"background":"none"})
+        .animate({
+          "padding-left": "1px"
+        }, {
+          duration: 200,
+          easing: "tabviewBounce"
+        });
+    }
+  };
+
+  var handleKeyPress = function(e) {
+    if (e.which == 13 || e.which == 27) { // return & escape
+      (self.$title)[0].blur();
+      self.$title
+        .addClass("transparentBorder")
+        .one("mouseout", function() {
+          self.$title.removeClass("transparentBorder");
+        });
+    } else
+      self.adjustTitleSize();
+
+    self.save();
+  };
+
+  this.$title
+    .css({backgroundRepeat: 'no-repeat'})
+    .blur(titleUnfocus)
+    .focus(function() {
+      if (self.locked.title) {
+        (self.$title)[0].blur();
+        return;
+      }
+      (self.$title)[0].select();
+      if (!self.getTitle()) {
+        self.$title
+          .removeClass("defaultName")
+          .val('');
+      }
+    })
+    .keyup(handleKeyPress);
+
+  titleUnfocus();
+
+  if (this.locked.title)
+    this.$title.addClass('name-locked');
+  else {
+    this.$titleShield
+      .mousedown(function(e) {
+        self.lastMouseDownTarget = (Utils.isRightClick(e) ? null : e.target);
+      })
+      .mouseup(function(e) {
+        var same = (e.target == self.lastMouseDownTarget);
+        self.lastMouseDownTarget = null;
+        if (!same)
+          return;
+
+        if (!self.isDragging) {
+          self.$titleShield.hide();
+          (self.$title)[0].focus();
+        }
+      });
+  }
+
+  // ___ Stack Expander
+  this.$expander = iQ("<div/>")
+    .addClass("stackExpander")
+    .appendTo($container)
+    .hide();
+
+  // ___ locking
+  if (this.locked.bounds)
+    $container.css({cursor: 'default'});
+
+  if (this.locked.close)
+    $close.hide();
+
+  // ___ Superclass initialization
+  this._init($container[0]);
+
+  if (this.$debug)
+    this.$debug.css({zIndex: -1000});
+
+  // ___ Children
+  Array.prototype.forEach.call(listOfEls, function(el) {
+    self.add(el, null, options);
+  });
+
+  // ___ Finish Up
+  this._addHandlers($container);
+
+  if (!this.locked.bounds)
+    this.setResizable(true);
+
+  GroupItems.register(this);
+
+  // ___ Position
+  var immediately = $container ? true : false;
+  this.setBounds(rectToBe, immediately);
+  this.snap();
+  if ($container)
+    this.setBounds(rectToBe, immediately);
+
+  // ___ Push other objects away
+  if (!options.dontPush)
+    this.pushAway();
+
+  this._inited = true;
+  this.save();
+  } catch(e) {
+    Utils.log("Error in GroupItem()");
+    Utils.log(e.stack);
+  }
+};
+
+// ----------
+window.GroupItem.prototype = Utils.extend(new Item(), new Subscribable(), {
+  // ----------
+  // Variable: defaultName
+  // The prompt text for the title field.
+  defaultName: "name this groupItem...",
+
+  // -----------
+  // Function: setActiveTab
+  // Sets the active <TabItem> for this groupItem
+  setActiveTab: function(tab) {
+    Utils.assert(tab && tab.isATabItem, 'tab must be a TabItem');
+    this._activeTab = tab;
+  },
+
+  // -----------
+  // Function: getActiveTab
+  // Gets the active <TabItem> for this groupItem
+  getActiveTab: function() {
+    return this._activeTab;
+  },
+
+  // ----------
+  // Function: getStorageData
+  // Returns all of the info worth storing about this groupItem.
+  getStorageData: function() {
+    var data = {
+      bounds: this.getBounds(),
+      userSize: null,
+      locked: Utils.copy(this.locked),
+      title: this.getTitle(),
+      id: this.id
+    };
+
+    if (Utils.isPoint(this.userSize))
+      data.userSize = new Point(this.userSize);
+
+    return data;
+  },
+
+  // ----------
+  // Function: isEmpty
+  // Returns true if the tab groupItem is empty and unnamed.
+  isEmpty: function() {
+    return !this._children.length && !this.getTitle();
+  },
+
+  // ----------
+  // Function: save
+  // Saves this groupItem to persistent storage.
+  save: function() {
+    if (!this._inited) // too soon to save now
+      return;
+
+    var data = this.getStorageData();
+    if (GroupItems.groupItemStorageSanity(data))
+      Storage.saveGroupItem(gWindow, data);
+  },
+
+  // ----------
+  // Function: getTitle
+  // Returns the title of this groupItem as a string.
+  getTitle: function() {
+    var value = (this.$title ? this.$title.val() : '');
+    return (value == this.defaultName ? '' : value);
+  },
+
+  // ----------
+  // Function: setTitle
+  // Sets the title of this groupItem with the given string
+  setTitle: function(value) {
+    this.$title.val(value);
+    this.save();
+  },
+
+  // ----------
+  // Function: adjustTitleSize
+  // Used to adjust the width of the title box depending on groupItem width and title size.
+  adjustTitleSize: function() {
+    Utils.assert(this.bounds, 'bounds needs to have been set');
+    let closeButton = iQ('.close', this.container);
+    var w = Math.min(this.bounds.width - closeButton.width() - closeButton.css('right'),
+                     Math.max(150, this.getTitle().length * 6));
+    // The * 6 multiplier calculation is assuming that characters in the title
+    // are approximately 6 pixels wide. Bug 586545
+    var css = {width: w};
+    this.$title.css(css);
+    this.$titleShield.css(css);
+  },
+
+  // ----------
+  // Function: getContentBounds
+  // Returns a <Rect> for the groupItem's content area (which doesn't include the title, etc).
+  getContentBounds: function() {
+    var box = this.getBounds();
+    var titleHeight = this.$titlebar.height();
+    box.top += titleHeight;
+    box.height -= titleHeight;
+
+    // Make the computed bounds' "padding" and new tab button margin actually be
+    // themeable --OR-- compute this from actual bounds. Bug 586546
+    box.inset(6, 6);
+    box.height -= 33; // For new tab button
+
+    return box;
+  },
+
+  // ----------
+  // Function: setBounds
+  // Sets the bounds with the given <Rect>, animating unless "immediately" is false.
+  //
+  // Parameters:
+  //   rect - a <Rect> giving the new bounds
+  //   immediately - true if it should not animate; default false
+  //   options - an object with additional parameters, see below
+  //
+  // Possible options:
+  //   force - true to always update the DOM even if the bounds haven't changed; default false
+  setBounds: function(rect, immediately, options) {
+    if (!Utils.isRect(rect)) {
+      Utils.trace('GroupItem.setBounds: rect is not a real rectangle!', rect);
+      return;
+    }
+
+    if (!options)
+      options = {};
+
+    rect.width = Math.max(110, rect.width);
+    rect.height = Math.max(125, rect.height);
+
+    var titleHeight = this.$titlebar.height();
+
+    // ___ Determine what has changed
+    var css = {};
+    var titlebarCSS = {};
+    var contentCSS = {};
+
+    if (rect.left != this.bounds.left || options.force)
+      css.left = rect.left;
+
+    if (rect.top != this.bounds.top || options.force)
+      css.top = rect.top;
+
+    if (rect.width != this.bounds.width || options.force) {
+      css.width = rect.width;
+      titlebarCSS.width = rect.width;
+      contentCSS.width = rect.width;
+    }
+
+    if (rect.height != this.bounds.height || options.force) {
+      css.height = rect.height;
+      contentCSS.height = rect.height - titleHeight;
+    }
+
+    if (Utils.isEmptyObject(css))
+      return;
+
+    var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top);
+    this.bounds = new Rect(rect);
+
+    // ___ Deal with children
+    if (css.width || css.height) {
+      this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)});
+    } else if (css.left || css.top) {
+      this._children.forEach(function(child) {
+        var box = child.getBounds();
+        child.setPosition(box.left + offset.x, box.top + offset.y, immediately);
+      });
+    }
+
+    // ___ Update our representation
+    if (immediately) {
+      iQ(this.container).css(css);
+      this.$titlebar.css(titlebarCSS);
+    } else {
+      TabItems.pausePainting();
+      iQ(this.container).animate(css, {
+        duration: 350,
+        easing: "tabviewBounce",
+        complete: function() {
+          TabItems.resumePainting();
+        }
+      });
+
+      this.$titlebar.animate(titlebarCSS, {
+        duration: 350
+      });
+    }
+
+    this.adjustTitleSize();
+
+    this._updateDebugBounds();
+    this.setTrenches(rect);
+
+    this.save();
+  },
+
+  // ----------
+  // Function: setZ
+  // Set the Z order for the groupItem's container, as well as its children.
+  setZ: function(value) {
+    this.zIndex = value;
+
+    iQ(this.container).css({zIndex: value});
+
+    if (this.$debug)
+      this.$debug.css({zIndex: value + 1});
+
+    var count = this._children.length;
+    if (count) {
+      var topZIndex = value + count + 1;
+      var zIndex = topZIndex;
+      var self = this;
+      this._children.forEach(function(child) {
+        if (child == self.topChild)
+          child.setZ(topZIndex + 1);
+        else {
+          child.setZ(zIndex);
+          zIndex--;
+        }
+      });
+    }
+  },
+
+  // ----------
+  // Function: close
+  // Closes the groupItem, removing (but not closing) all of its children.
+  close: function() {
+    this.removeAll();
+    GroupItems.unregister(this);
+    this._sendToSubscribers("close");
+    this.removeTrenches();
+    iQ(this.container).fadeOut(function() {
+      iQ(this).remove();
+      Items.unsquish();
+    });
+
+    Storage.deleteGroupItem(gWindow, this.id);
+  },
+
+  // ----------
+  // Function: closeAll
+  // Closes the groupItem and all of its children.
+  closeAll: function() {
+    var self = this;
+    if (this._children.length) {
+      var toClose = this._children.concat();
+      toClose.forEach(function(child) {
+        child.removeSubscriber(self, "close");
+        child.close();
+      });
+    }
+
+    if (!this.locked.close)
+      this.close();
+  },
+
+  // ----------
+  // Function: add
+  // Adds an item to the groupItem.
+  // Parameters:
+  //
+  //   a - The item to add. Can be an <Item>, a DOM element or an iQ object.
+  //       The latter two must refer to the container of an <Item>.
+  //   dropPos - An object with left and top properties referring to the location dropped at.  Optional.
+  //   options - An object with optional settings for this call. Currently the only one is dontArrange.
+  add: function(a, dropPos, options) {
+    try {
+      var item;
+      var $el;
+      if (a.isAnItem) {
+        item = a;
+        $el = iQ(a.container);
+      } else {
+        $el = iQ(a);
+        item = Items.item($el);
+      }
+      Utils.assertThrow(!item.parent || item.parent == this,
+          "shouldn't already be in another groupItem");
+
+      item.removeTrenches();
+
+      if (!dropPos)
+        dropPos = {top:window.innerWidth, left:window.innerHeight};
+
+      if (typeof options == 'undefined')
+        options = {};
+
+      var self = this;
+
+      var wasAlreadyInThisGroupItem = false;
+      var oldIndex = this._children.indexOf(item);
+      if (oldIndex != -1) {
+        this._children.splice(oldIndex, 1);
+        wasAlreadyInThisGroupItem = true;
+      }
+
+      // TODO: You should be allowed to drop in the white space at the bottom
+      // and have it go to the end (right now it can match the thumbnail above
+      // it and go there)
+      // Bug 586548
+      function findInsertionPoint(dropPos) {
+        if (self.shouldStack(self._children.length + 1))
+          return 0;
+
+        var best = {dist: Infinity, item: null};
+        var index = 0;
+        var box;
+        self._children.forEach(function(child) {
+          box = child.getBounds();
+          if (box.bottom < dropPos.top || box.top > dropPos.top)
+            return;
+
+          var dist = Math.sqrt(Math.pow((box.top+box.height/2)-dropPos.top,2)
+              + Math.pow((box.left+box.width/2)-dropPos.left,2));
+
+          if (dist <= best.dist) {
+            best.item = child;
+            best.dist = dist;
+            best.index = index;
+          }
+        });
+
+        if (self._children.length) {
+          if (best.item) {
+            box = best.item.getBounds();
+            var insertLeft = dropPos.left <= box.left + box.width/2;
+            if (!insertLeft)
+              return best.index+1;
+            return best.index;
+          }
+          return self._children.length;
+        }
+
+        return 0;
+      }
+
+      // Insert the tab into the right position.
+      var index = findInsertionPoint(dropPos);
+      this._children.splice(index, 0, item);
+
+      item.setZ(this.getZ() + 1);
+      $el.addClass("tabInGroupItem");
+
+      if (!wasAlreadyInThisGroupItem) {
+        item.droppable(false);
+        item.groupItemData = {};
+
+        item.addSubscriber(this, "close", function() {
+          self.remove(item);
+        });
+
+        item.setParent(this);
+
+        if (typeof item.setResizable == 'function')
+          item.setResizable(false);
+
+        if (item.tab == gBrowser.selectedTab)
+          GroupItems.setActiveGroupItem(this);
+      }
+
+      if (!options.dontArrange) {
+        this.arrange();
+      }
+      UI.setReorderTabsOnHide(this);
+
+      if (this._nextNewTabCallback) {
+        this._nextNewTabCallback.apply(this, [item])
+        this._nextNewTabCallback = null;
+      }
+    } catch(e) {
+      Utils.log('GroupItem.add error', e);
+    }
+  },
+
+  // ----------
+  // Function: remove
+  // Removes an item from the groupItem.
+  // Parameters:
+  //
+  //   a - The item to remove. Can be an <Item>, a DOM element or an iQ object.
+  //       The latter two must refer to the container of an <Item>.
+  //   options - An object with optional settings for this call. Currently the only one is dontArrange.
+  remove: function(a, options) {
+    try {
+      var $el;
+      var item;
+
+      if (a.isAnItem) {
+        item = a;
+        $el = iQ(item.container);
+      } else {
+        $el = iQ(a);
+        item = Items.item($el);
+      }
+
+      if (typeof options == 'undefined')
+        options = {};
+
+      var index = this._children.indexOf(item);
+      if (index != -1)
+        this._children.splice(index, 1);
+
+      item.setParent(null);
+      item.removeClass("tabInGroupItem");
+      item.removeClass("stacked");
+      item.removeClass("stack-trayed");
+      item.setRotation(0);
+
+      item.droppable(true);
+      item.removeSubscriber(this, "close");
+
+      if (typeof item.setResizable == 'function')
+        item.setResizable(true);
+
+      if (!this._children.length && !this.locked.close && !this.getTitle() && !options.dontClose) {
+        this.close();
+      } else if (!options.dontArrange) {
+        this.arrange();
+      }
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: removeAll
+  // Removes all of the groupItem's children.
+  removeAll: function() {
+    var self = this;
+    var toRemove = this._children.concat();
+    toRemove.forEach(function(child) {
+      self.remove(child, {dontArrange: true});
+    });
+  },
+
+  // ----------
+  // Function: setNewTabButtonBounds
+  // Used for positioning the "new tab" button in the "new tabs" groupItem.
+  setNewTabButtonBounds: function(box, immediately) {
+    if (!immediately)
+      this.$ntb.animate(box.css(), {
+        duration: 320,
+        easing: "tabviewBounce"
+      });
+    else
+      this.$ntb.css(box.css());
+  },
+
+  // ----------
+  // Function: hideExpandControl
+  // Hide the control which expands a stacked groupItem into a quick-look view.
+  hideExpandControl: function() {
+    this.$expander.hide();
+  },
+
+  // ----------
+  // Function: showExpandControl
+  // Show the control which expands a stacked groupItem into a quick-look view.
+  showExpandControl: function() {
+    var childBB = this.getChild(0).getBounds();
+    var dT = childBB.top - this.getBounds().top;
+    var dL = childBB.left - this.getBounds().left;
+
+    this.$expander
+        .show()
+        .css({
+          opacity: .2,
+          top: dT + childBB.height + Math.min(7, (this.getBounds().bottom-childBB.bottom)/2),
+          // TODO: Why the magic -6? because the childBB.width seems to be over-sizing itself.
+          // But who can blame an object for being a bit optimistic when self-reporting size.
+          // It has to impress the ladies somehow. Bug 586549
+          left: dL + childBB.width/2 - this.$expander.width()/2 - 6,
+        });
+  },
+
+  // ----------
+  // Function: shouldStack
+  // Returns true if the groupItem, given "count", should stack (instead of grid).
+  shouldStack: function(count) {
+    if (count <= 1)
+      return false;
+
+    var bb = this.getContentBounds();
+    var options = {
+      pretend: true,
+      count: count
+    };
+
+    var rects = Items.arrange(null, bb, options);
+    return (rects[0].width < TabItems.minTabWidth * 1.35);
+  },
+
+  // ----------
+  // Function: arrange
+  // Lays out all of the children.
+  //
+  // Parameters:
+  //   options - passed to <Items.arrange> or <_stackArrange>
+  arrange: function(options) {
+    if (this.expanded) {
+      this.topChild = null;
+      var box = new Rect(this.expanded.bounds);
+      box.inset(8, 8);
+      Items.arrange(this._children, box, Utils.extend({}, options, {padding: 8, z: 99999}));
+    } else {
+      var bb = this.getContentBounds();
+      var count = this._children.length;
+      if (!this.shouldStack(count)) {
+        var animate;
+        if (!options || typeof options.animate == 'undefined')
+          animate = true;
+        else
+          animate = options.animate;
+
+        if (typeof options == 'undefined')
+          options = {};
+
+        this._children.forEach(function(child) {
+            child.removeClass("stacked")
+        });
+
+        this.topChild = null;
+
+        var arrangeOptions = Utils.copy(options);
+        Utils.extend(arrangeOptions, {
+          pretend: true,
+          count: count
+        });
+
+        if (!count) {
+          this.xDensity = 0;
+          this.yDensity = 0;
+          return;
+        }
+
+        var rects = Items.arrange(this._children, bb, arrangeOptions);
+
+        // yDensity = (the distance of the bottom of the last tab to the top of the content area)
+        // / (the total available content height)
+        this.yDensity = (rects[rects.length - 1].bottom - bb.top) / (bb.height);
+
+        // xDensity = (the distance from the left of the content area to the right of the rightmost
+        // tab) / (the total available content width)
+
+        // first, find the right of the rightmost tab! luckily, they're in order.
+        // TODO: does this change for rtl?
+        var rightMostRight = 0;
+        for each (var rect in rects) {
+          if (rect.right > rightMostRight)
+            rightMostRight = rect.right;
+          else
+            break;
+        }
+        this.xDensity = (rightMostRight - bb.left) / (bb.width);
+
+        this._children.forEach(function(child, index) {
+          if (!child.locked.bounds) {
+            child.setBounds(rects[index], !animate);
+            child.setRotation(0);
+            if (options.z)
+              child.setZ(options.z);
+          }
+        });
+
+        this._isStacked = false;
+      } else
+        this._stackArrange(bb, options);
+    }
+
+    if (this._isStacked && !this.expanded) this.showExpandControl();
+    else this.hideExpandControl();
+  },
+
+  // ----------
+  // Function: _stackArrange
+  // Arranges the children in a stack.
+  //
+  // Parameters:
+  //   bb - <Rect> to arrange within
+  //   options - see below
+  //
+  // Possible "options" properties:
+  //   animate - whether to animate; default: true.
+  _stackArrange: function(bb, options) {
+    var animate;
+    if (!options || typeof options.animate == 'undefined')
+      animate = true;
+    else
+      animate = options.animate;
+
+    if (typeof options == 'undefined')
+      options = {};
+
+    var count = this._children.length;
+    if (!count)
+      return;
+
+    var zIndex = this.getZ() + count + 1;
+
+    var maxRotation = 35; // degress
+    var scale = 0.8;
+    var newTabsPad = 10;
+    var w;
+    var h;
+    var itemAspect = TabItems.tabHeight / TabItems.tabWidth;
+    var bbAspect = bb.height / bb.width;
+
+    // compute h and w. h and w are the dimensions of each of the tabs... in other words, the
+    // height and width of the entire stack, modulo rotation.
+    if (bbAspect > itemAspect) { // Tall, thin groupItem
+      w = bb.width * scale;
+      h = w * itemAspect;
+      // let's say one, because, even though there's more space, we're enforcing that with scale.
+      this.xDensity = 1;
+      this.yDensity = h / (bb.height * scale);
+    } else { // Short, wide groupItem
+      h = bb.height * scale;
+      w = h * (1 / itemAspect);
+      this.yDensity = 1;
+      this.xDensity = h / (bb.width * scale);
+    }
+
+    // x is the left margin that the stack will have, within the content area (bb)
+    // y is the vertical margin
+    var x = (bb.width - w) / 2;
+
+    var y = Math.min(x, (bb.height - h) / 2);
+    var box = new Rect(bb.left + x, bb.top + y, w, h);
+
+    var self = this;
+    var children = [];
+    this._children.forEach(function(child) {
+      if (child == self.topChild)
+        children.unshift(child);
+      else
+        children.push(child);
+    });
+
+    children.forEach(function(child, index) {
+      if (!child.locked.bounds) {
+        child.setZ(zIndex);
+        zIndex--;
+
+        child.addClass("stacked");
+        child.setBounds(box, !animate);
+        child.setRotation(self._randRotate(maxRotation, index));
+      }
+    });
+
+    self._isStacked = true;
+  },
+
+  // ----------
+  // Function: _randRotate
+  // Random rotation generator for <_stackArrange>
+  _randRotate: function(spread, index) {
+    if (index >= this._stackAngles.length) {
+      var randAngle = 5*index + parseInt((Math.random()-.5)*1);
+      this._stackAngles.push(randAngle);
+      return randAngle;
+    }
+
+    if (index > 5) index = 5;
+
+    return this._stackAngles[index];
+  },
+
+  // ----------
+  // Function: childHit
+  // Called by one of the groupItem's children when the child is clicked on.
+  //
+  // Returns an object:
+  //   shouldZoom - true if the browser should launch into the tab represented by the child
+  //   callback - called after the zoom animation is complete
+  childHit: function(child) {
+    var self = this;
+
+    // ___ normal click
+    if (!this._isStacked || this.expanded) {
+      return {
+        shouldZoom: true,
+        callback: function() {
+          self.collapse();
+        }
+      };
+    }
+
+    GroupItems.setActiveGroupItem(self);
+    return { shouldZoom: true };
+  },
+
+  expand: function() {
+    var self = this;
+    // ___ we're stacked, and command is held down so expand
+    GroupItems.setActiveGroupItem(self);
+    var startBounds = this.getChild(0).getBounds();
+    var $tray = iQ("<div>").css({
+      top: startBounds.top,
+      left: startBounds.left,
+      width: startBounds.width,
+      height: startBounds.height,
+      position: "absolute",
+      zIndex: 99998
+    }).appendTo("body");
+
+
+    var w = 180;
+    var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1;
+    var padding = 20;
+    var col = Math.ceil(Math.sqrt(this._children.length));
+    var row = Math.ceil(this._children.length/col);
+
+    var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1));
+    var overlayHeight = Math.min(window.innerHeight - (padding * 2), h*row + padding*(row+1));
+
+    var pos = {left: startBounds.left, top: startBounds.top};
+    pos.left -= overlayWidth / 3;
+    pos.top  -= overlayHeight / 3;
+
+    if (pos.top < 0)
+      pos.top = 20;
+    if (pos.left < 0)
+      pos.left = 20;
+    if (pos.top + overlayHeight > window.innerHeight)
+      pos.top = window.innerHeight - overlayHeight - 20;
+    if (pos.left + overlayWidth > window.innerWidth)
+      pos.left = window.innerWidth - overlayWidth - 20;
+
+    $tray
+      .animate({
+        width:  overlayWidth,
+        height: overlayHeight,
+        top: pos.top,
+        left: pos.left
+      }, {
+        duration: 200,
+        easing: "tabviewBounce"
+      })
+      .addClass("overlay");
+
+    this._children.forEach(function(child) {
+      child.addClass("stack-trayed");
+    });
+
+    var $shield = iQ('<div>')
+      .addClass('shield')
+      .css({
+        zIndex: 99997
+      })
+      .appendTo('body')
+      .click(function() { // just in case
+        self.collapse();
+      });
+
+    // There is a race-condition here. If there is
+    // a mouse-move while the shield is coming up
+    // it will collapse, which we don't want. Thus,
+    // we wait a little bit before adding this event
+    // handler.
+    setTimeout(function() {
+      $shield.mouseover(function() {
+        self.collapse();
+      });
+    }, 200);
+
+    this.expanded = {
+      $tray: $tray,
+      $shield: $shield,
+      bounds: new Rect(pos.left, pos.top, overlayWidth, overlayHeight)
+    };
+
+    this.arrange();
+  },
+
+  // ----------
+  // Function: collapse
+  // Collapses the groupItem from the expanded "tray" mode.
+  collapse: function() {
+    if (this.expanded) {
+      var z = this.getZ();
+      var box = this.getBounds();
+      this.expanded.$tray
+        .css({
+          zIndex: z + 1
+        })
+        .animate({
+          width:  box.width,
+          height: box.height,
+          top: box.top,
+          left: box.left,
+          opacity: 0
+        }, {
+          duration: 350,
+          easing: "tabviewBounce",
+          complete: function() {
+            iQ(this).remove();
+          }
+        });
+
+      this.expanded.$shield.remove();
+      this.expanded = null;
+
+      this._children.forEach(function(child) {
+        child.removeClass("stack-trayed");
+      });
+
+      this.arrange({z: z + 2});
+    }
+  },
+
+  // ----------
+  // Function: _addHandlers
+  // Helper routine for the constructor; adds various event handlers to the container.
+  _addHandlers: function(container) {
+    var self = this;
+
+    this.dropOptions.over = function() {
+      iQ(this.container).addClass("acceptsDrop");
+    };
+    this.dropOptions.drop = function(event) {
+      iQ(this.container).removeClass("acceptsDrop");
+      this.add(drag.info.$el, {left:event.pageX, top:event.pageY});
+      GroupItems.setActiveGroupItem(this);
+    };
+
+    if (!this.locked.bounds)
+      this.draggable();
+
+    iQ(container)
+      .mousedown(function(e) {
+        self._mouseDown = {
+          location: new Point(e.clientX, e.clientY),
+          className: e.target.className
+        };
+      })
+      .mouseup(function(e) {
+        if (!self._mouseDown || !self._mouseDown.location || !self._mouseDown.className)
+          return;
+
+        // Don't zoom in on clicks inside of the controls.
+        var className = self._mouseDown.className;
+        if (className.indexOf('title-shield') != -1 ||
+           className.indexOf('name') != -1 ||
+           className.indexOf('close') != -1 ||
+           className.indexOf('newTabButton') != -1 ||
+           className.indexOf('stackExpander') != -1) {
+          return;
+        }
+
+        var location = new Point(e.clientX, e.clientY);
+
+        if (location.distance(self._mouseDown.location) > 1.0)
+          return;
+
+        // Zoom into the last-active tab when the groupItem
+        // is clicked, but only for non-stacked groupItems.
+        var activeTab = self.getActiveTab();
+        if (!self._isStacked) {
+          if (activeTab)
+            activeTab.zoomIn();
+          else if (self.getChild(0))
+            self.getChild(0).zoomIn();
+        }
+
+        self._mouseDown = null;
+    });
+
+    this.droppable(true);
+
+    this.$expander.click(function() {
+      self.expand();
+    });
+  },
+
+  // ----------
+  // Function: setResizable
+  // Sets whether the groupItem is resizable and updates the UI accordingly.
+  setResizable: function(value) {
+    this.resizeOptions.minWidth = 90;
+    this.resizeOptions.minHeight = 90;
+
+    if (value) {
+      this.$resizer.fadeIn();
+      this.resizable(true);
+    } else {
+      this.$resizer.fadeOut();
+      this.resizable(false);
+    }
+  },
+
+  // ----------
+  // Function: newTab
+  // Creates a new tab within this groupItem.
+  newTab: function(url) {
+    GroupItems.setActiveGroupItem(this);
+    let newTab = gBrowser.loadOneTab(url || "about:blank", {inBackground: true});
+
+    var self = this;
+    var doNextTab = function(tab) {
+      var groupItem = GroupItems.getActiveGroupItem();
+
+      iQ(tab.container).css({opacity: 0});
+      var $anim = iQ("<div>")
+        .addClass('newTabAnimatee')
+        .css({
+          top: tab.bounds.top+5,
+          left: tab.bounds.left+5,
+          width: tab.bounds.width-10,
+          height: tab.bounds.height-10,
+          zIndex: 999,
+          opacity: 0
+        })
+        .appendTo("body")
+        .animate({
+          opacity: 1.0
+        }, {
+          duration: 500,
+          complete: function() {
+            $anim.animate({
+              top: 0,
+              left: 0,
+              width: window.innerWidth,
+              height: window.innerHeight
+            }, {
+              duration: 270,
+              complete: function() {
+                iQ(tab.container).css({opacity: 1});
+                newTab.tabItem.zoomIn(!url);
+                $anim.remove();
+                // We need a timeout here so that there is a chance for the
+                // new tab to get made! Otherwise it won't appear in the list
+                // of the groupItem's tab.
+                // TODO: This is probably a terrible hack that sets up a race
+                // condition. We need a better solution.
+                // Bug 586551
+                setTimeout(function() {
+                  self._sendToSubscribers("tabAdded", { groupItemId: self.id });
+                }, 1);
+              }
+            });
+          }
+        });
+    }
+
+    // TODO: Because this happens as a callback, there is
+    // sometimes a long delay before the animation occurs.
+    // We need to fix this--immediate response to a users
+    // actions is necessary for a good user experience.
+    // Bug 586552
+    self.onNextNewTab(doNextTab);
+  },
+
+  // ----------
+  // Function: reorderTabItemsBasedOnTabOrder
+  // Reorders the tabs in a groupItem based on the arrangment of the tabs
+  // shown in the tab bar. It does it by sorting the children
+  // of the groupItem by the positions of their respective tabs in the
+  // tab bar.
+  reorderTabItemsBasedOnTabOrder: function() {
+    this._children.sort(function(a,b) a.tab._tPos - b.tab._tPos);
+
+    this.arrange({animate: false});
+    // this.arrange calls this.save for us
+  },
+
+  // Function: reorderTabsBasedOnTabItemOrder
+  // Reorders the tabs in the tab bar based on the arrangment of the tabs
+  // shown in the groupItem.
+  reorderTabsBasedOnTabItemOrder: function() {
+    var tabBarTabs = Array.slice(gBrowser.tabs);
+    var currentIndex;
+
+    // ToDo: optimisation is needed to further reduce the tab move.
+    // Bug 586553
+    this._children.forEach(function(tabItem) {
+      tabBarTabs.some(function(tab, i) {
+        if (tabItem.tab == tab) {
+          if (!currentIndex)
+            currentIndex = i;
+          else if (tab.pinned)
+            currentIndex++;
+          else {
+            var removed;
+            if (currentIndex < i)
+              currentIndex = i;
+            else if (currentIndex > i) {
+              removed = tabBarTabs.splice(i, 1);
+              tabBarTabs.splice(currentIndex, 0, removed);
+              gBrowser.moveTabTo(tabItem.tab, currentIndex);
+            }
+          }
+          return true;
+        }
+        return false;
+      });
+    });
+  },
+
+  // ----------
+  // Function: setTopChild
+  // Sets the <Item> that should be displayed on top when in stack mode.
+  setTopChild: function(topChild) {
+    this.topChild = topChild;
+
+    this.arrange({animate: false});
+    // this.arrange calls this.save for us
+  },
+
+  // ----------
+  // Function: getChild
+  // Returns the nth child tab or null if index is out of range.
+  //
+  // Parameters:
+  //  index - the index of the child tab to return, use negative
+  //          numbers to index from the end (-1 is the last child)
+  getChild: function(index) {
+    if (index < 0)
+      index = this._children.length + index;
+    if (index >= this._children.length || index < 0)
+      return null;
+    return this._children[index];
+  },
+
+  // ----------
+  // Function: getChildren
+  // Returns all children.
+  getChildren: function() {
+    return this._children;
+  },
+
+  // ---------
+  // Function: onNextNewTab
+  // Sets up a one-time handler that gets called the next time a
+  // tab is added to the groupItem.
+  //
+  // Parameters:
+  //  callback - the one-time callback that is fired when the next
+  //             time a tab is added to a groupItem; it gets passed the
+  //             new tab
+  onNextNewTab: function(callback) {
+    this._nextNewTabCallback = callback;
+  }
+});
+
+// ##########
+// Class: GroupItems
+// Singelton for managing all <GroupItem>s.
+window.GroupItems = {
+  groupItems: [],
+  nextID: 1,
+  _inited: false,
+  _activeGroupItem: null,
+  _activeOrphanTab: null,
+
+  // ----------
+  // Function: init
+  init: function() {
+  },
+
+  // ----------
+  // Function: uninit
+  uninit : function() {
+    this.groupItems = null;
+  },
+
+  // ----------
+  // Function: getNextID
+  // Returns the next unused groupItem ID.
+  getNextID: function() {
+    var result = this.nextID;
+    this.nextID++;
+    this.save();
+    return result;
+  },
+
+  // ----------
+  // Function: getStorageData
+  // Returns an object for saving GroupItems state to persistent storage.
+  getStorageData: function() {
+    var data = {nextID: this.nextID, groupItems: []};
+    this.groupItems.forEach(function(groupItem) {
+      data.groupItems.push(groupItem.getStorageData());
+    });
+
+    return data;
+  },
+
+  // ----------
+  // Function: saveAll
+  // Saves GroupItems state, as well as the state of all of the groupItems.
+  saveAll: function() {
+    this.save();
+    this.groupItems.forEach(function(groupItem) {
+      groupItem.save();
+    });
+  },
+
+  // ----------
+  // Function: save
+  // Saves GroupItems state.
+  save: function() {
+    if (!this._inited) // too soon to save now
+      return;
+
+    Storage.saveGroupItemsData(gWindow, {nextID:this.nextID});
+  },
+
+  // ----------
+  // Function: getBoundingBox
+  // Given an array of DOM elements, returns a <Rect> with (roughly) the union of their locations.
+  getBoundingBox: function GroupItems_getBoundingBox(els) {
+    var bounds = [iQ(el).bounds() for each (el in els)];
+    var left   = Math.min.apply({},[ b.left   for each (b in bounds) ]);
+    var top    = Math.min.apply({},[ b.top    for each (b in bounds) ]);
+    var right  = Math.max.apply({},[ b.right  for each (b in bounds) ]);
+    var bottom = Math.max.apply({},[ b.bottom for each (b in bounds) ]);
+
+    return new Rect(left, top, right-left, bottom-top);
+  },
+
+  // ----------
+  // Function: reconstitute
+  // Restores to stored state, creating groupItems as needed.
+  // If no data, sets up blank slate (including "new tabs" groupItem).
+  reconstitute: function(groupItemsData, groupItemData) {
+    try {
+      if (groupItemsData && groupItemsData.nextID)
+        this.nextID = groupItemsData.nextID;
+
+      if (groupItemData) {
+        for (var id in groupItemData) {
+          var groupItem = groupItemData[id];
+          if (this.groupItemStorageSanity(groupItem)) {
+            var options = {
+              dontPush: true
+            };
+
+            new GroupItem([], Utils.extend({}, groupItem, options));
+          }
+        }
+      }
+
+      this._inited = true;
+      this.save(); // for nextID
+    } catch(e) {
+      Utils.log("error in recons: "+e);
+    }
+  },
+
+  // ----------
+  // Function: groupItemStorageSanity
+  // Given persistent storage data for a groupItem, returns true if it appears to not be damaged.
+  groupItemStorageSanity: function(groupItemData) {
+    // TODO: check everything
+    // Bug 586555
+    var sane = true;
+    if (!Utils.isRect(groupItemData.bounds)) {
+      Utils.log('GroupItems.groupItemStorageSanity: bad bounds', groupItemData.bounds);
+      sane = false;
+    }
+
+    return sane;
+  },
+
+  // ----------
+  // Function: getGroupItemWithTitle
+  // Returns the <GroupItem> that has the given title, or null if none found.
+  // TODO: what if there are multiple groupItems with the same title??
+  //       Right now, looks like it'll return the last one. Bug 586557
+  getGroupItemWithTitle: function(title) {
+    var result = null;
+    this.groupItems.forEach(function(groupItem) {
+      if (groupItem.getTitle() == title)
+        result = groupItem;
+    });
+
+    return result;
+  },
+
+  // ----------
+  // Function: register
+  // Adds the given <GroupItem> to the list of groupItems we're tracking.
+  register: function(groupItem) {
+    Utils.assert(groupItem, 'groupItem');
+    Utils.assert(this.groupItems.indexOf(groupItem) == -1, 'only register once per groupItem');
+    this.groupItems.push(groupItem);
+  },
+
+  // ----------
+  // Function: unregister
+  // Removes the given <GroupItem> from the list of groupItems we're tracking.
+  unregister: function(groupItem) {
+    var index = this.groupItems.indexOf(groupItem);
+    if (index != -1)
+      this.groupItems.splice(index, 1);
+
+    if (groupItem == this._activeGroupItem)
+      this._activeGroupItem = null;
+  },
+
+  // ----------
+  // Function: groupItem
+  // Given some sort of identifier, returns the appropriate groupItem.
+  // Currently only supports groupItem ids.
+  groupItem: function(a) {
+    var result = null;
+    this.groupItems.forEach(function(candidate) {
+      if (candidate.id == a)
+        result = candidate;
+    });
+
+    return result;
+  },
+
+  // ----------
+  // Function: arrange
+  // Arranges all of the groupItems into a grid.
+  arrange: function() {
+    var bounds = Items.getPageBounds();
+    bounds.bottom -= 20; // for the dev menu
+
+    var count = this.groupItems.length - 1;
+    var columns = Math.ceil(Math.sqrt(count));
+    var rows = ((columns * columns) - count >= columns ? columns - 1 : columns);
+    var padding = 12;
+    var startX = bounds.left + padding;
+    var startY = bounds.top + padding;
+    var totalWidth = bounds.width - padding;
+    var totalHeight = bounds.height - padding;
+    var box = new Rect(startX, startY,
+        (totalWidth / columns) - padding,
+        (totalHeight / rows) - padding);
+
+    var i = 0;
+    this.groupItems.forEach(function(groupItem) {
+      if (groupItem.locked.bounds)
+        return;
+
+      groupItem.setBounds(box, true);
+
+      box.left += box.width + padding;
+      i++;
+      if (i % columns == 0) {
+        box.left = startX;
+        box.top += box.height + padding;
+      }
+    });
+  },
+
+  // ----------
+  // Function: removeAll
+  // Removes all tabs from all groupItems (which automatically closes all unnamed groupItems).
+  removeAll: function() {
+    var toRemove = this.groupItems.concat();
+    toRemove.forEach(function(groupItem) {
+      groupItem.removeAll();
+    });
+  },
+
+  // ----------
+  // Function: newTab
+  // Given a <TabItem>, files it in the appropriate groupItem.
+  newTab: function(tabItem) {
+    let activeGroupItem = this.getActiveGroupItem();
+    let orphanTab = this.getActiveOrphanTab();
+//    Utils.log('newTab', activeGroupItem, orphanTab);
+    if (activeGroupItem) {
+      activeGroupItem.add(tabItem);
+    } else if (orphanTab) {
+      let newGroupItemBounds = orphanTab.getBoundsWithTitle();
+      newGroupItemBounds.inset(-40,-40);
+      let newGroupItem = new GroupItem([orphanTab, tabItem], {bounds: newGroupItemBounds});
+      newGroupItem.snap();
+      this.setActiveGroupItem(newGroupItem);
+    } else {
+      this.positionNewTabAtBottom(tabItem);
+    }
+  },
+
+  // ----------
+  // Function: positionNewTabAtBottom
+  // Does what it says on the tin.
+  // TODO: Make more robust and improve documentation,
+  // Also, this probably belongs in tabitems.js
+  // Bug 586558
+  positionNewTabAtBottom: function(tabItem) {
+    let windowBounds = Items.getSafeWindowBounds();
+
+    let itemBounds = new Rect(
+      windowBounds.right - TabItems.tabWidth,
+      windowBounds.bottom - TabItems.tabHeight,
+      TabItems.tabWidth,
+      TabItems.tabHeight
+    );
+
+    tabItem.setBounds(itemBounds);
+  },
+
+  // ----------
+  // Function: getActiveGroupItem
+  // Returns the active groupItem. Active means its tabs are
+  // shown in the tab bar when not in the TabView interface.
+  getActiveGroupItem: function() {
+    return this._activeGroupItem;
+  },
+
+  // ----------
+  // Function: setActiveGroupItem
+  // Sets the active groupItem, thereby showing only the relevent tabs, and
+  // setting the groupItem which will receive new tabs.
+  //
+  // Paramaters:
+  //  groupItem - the active <GroupItem> or <null> if no groupItem is active
+  //          (which means we have an orphaned tab selected)
+  setActiveGroupItem: function(groupItem) {
+
+    if (this._activeGroupItem)
+      iQ(this._activeGroupItem.container).removeClass('activeGroupItem');
+
+    if (groupItem !== null) {
+      if (groupItem)
+        iQ(groupItem.container).addClass('activeGroupItem');
+      // if a groupItem is active, we surely are not in an orphaned tab.
+      this.setActiveOrphanTab(null);
+    }
+
+    this._activeGroupItem = groupItem;
+  },
+
+  // ----------
+  // Function: getActiveOrphanTab
+  // Returns the active orphan tab, in cases when there is no active groupItem.
+  getActiveOrphanTab: function() {
+    return this._activeOrphanTab;
+  },
+
+  // ----------
+  // Function: setActiveOrphanTab
+  // In cases where an orphan tab (not in a groupItem) is active by itself,
+  // this function is called and the "active orphan tab" is set.
+  //
+  // Paramaters:
+  //  groupItem - the active <TabItem> or <null>
+  setActiveOrphanTab: function(tabItem) {
+    this._activeOrphanTab = tabItem;
+  },
+
+  // ----------
+  // Function: updateTabBar
+  // Hides and shows tabs in the tab bar based on the active groupItem or
+  // currently active orphan tabItem
+  updateTabBar: function() {
+    if (!window.UI)
+      return; // called too soon
+
+//    Utils.log('updateTabBar', this._activeGroupItem, this._activeOrphanTab);
+
+    if (!this._activeGroupItem && !this._activeOrphanTab) {
+      Utils.assert(false, "There must be something to show in the tab bar!");
+      return;
+    }
+
+    let tabItems = this._activeGroupItem == null ?
+      [this._activeOrphanTab] : this._activeGroupItem._children;
+    gBrowser.showOnlyTheseTabs(tabItems.map(function(item) item.tab));
+  },
+
+  // ----------
+  // Function: getOrphanedTabs
+  // Returns an array of all tabs that aren't in a groupItem.
+  getOrphanedTabs: function() {
+    var tabs = TabItems.getItems();
+    tabs = tabs.filter(function(tab) {
+      return tab.parent == null;
+    });
+    return tabs;
+  },
+
+  // ----------
+  // Function: getNextGroupItemTab
+  // Paramaters:
+  //  reverse - the boolean indicates the direction to look for the next groupItem.
+  // Returns the <tabItem>. If nothing is found, return null.
+  getNextGroupItemTab: function(reverse) {
+    var groupItems = Utils.copy(GroupItems.groupItems);
+    if (reverse)
+      groupItems = groupItems.reverse();
+    var activeGroupItem = GroupItems.getActiveGroupItem();
+    var activeOrphanTab = GroupItems.getActiveOrphanTab();
+    var tabItem = null;
+
+    if (!activeGroupItem) {
+      if (groupItems.length > 0) {
+
+        groupItems.some(function(groupItem) {
+          var child = groupItem.getChild(0);
+          if (child) {
+            tabItem = child;
+            return true;
+          }
+          return false;
+        });
+      }
+    } else {
+      if (reverse)
+        groupItems = groupItems.reverse();
+
+      var currentIndex;
+      groupItems.some(function(groupItem, index) {
+        if (groupItem == activeGroupItem) {
+          currentIndex = index;
+          return true;
+        }
+        return false;
+      });
+      var firstGroupItems = groupItems.slice(currentIndex + 1);
+      firstGroupItems.some(function(groupItem) {
+        var child = groupItem.getChild(0);
+        if (child) {
+          tabItem = child;
+          return true;
+        }
+        return false;
+      });
+      if (!tabItem) {
+        var orphanedTabs = GroupItems.getOrphanedTabs();
+        if (orphanedTabs.length > 0)
+          tabItem = orphanedTabs[0];
+      }
+      if (!tabItem) {
+        var secondGroupItems = groupItems.slice(0, currentIndex);
+        secondGroupItems.some(function(groupItem) {
+          var child = groupItem.getChild(0);
+          if (child) {
+            tabItem = child;
+            return true;
+          }
+          return false;
+        });
+      }
+    }
+    return tabItem;
+  },
+
+  // ----------
+  // Function: moveTabToGroupItem
+  // Paramaters:
+  //  tab - the <xul:tab>.
+  //  groupItemId - the <groupItem>'s id.  If nothing, create a new <groupItem>.
+  moveTabToGroupItem : function(tab, groupItemId) {
+    let shouldUpdateTabBar = false;
+    let shouldShowTabView = false;
+    let groupItem;
+
+    // switch to the appropriate tab first.
+    if (gBrowser.selectedTab == tab) {
+      let list = gBrowser.visibleTabs;
+      let listLength = list.length;
+
+      if (listLength > 1) {
+        let index = list.indexOf(tab);
+        if (index == 0 || (index + 1) < listLength)
+          gBrowser.selectTabAtIndex(index + 1);
+        else
+          gBrowser.selectTabAtIndex(index - 1);
+      } else {
+        shouldShowTabView = true;
+      }
+    } else
+      shouldUpdateTabBar = true
+
+    // remove tab item from a groupItem
+    if (tab.tabItem.parent)
+      tab.tabItem.parent.remove(tab.tabItem);
+
+    // add tab item to a groupItem
+    if (groupItemId) {
+      groupItem = GroupItems.groupItem(groupItemId);
+      groupItem.add(tab.tabItem);
+      UI.setReorderTabItemsOnShow(groupItem);
+    } else {
+      let pageBounds = Items.getPageBounds();
+      pageBounds.inset(20, 20);
+
+      let box = new Rect(pageBounds);
+      box.width = 250;
+      box.height = 200;
+
+      new GroupItem([ tab.tabItem ], { bounds: box });
+    }
+
+    if (shouldUpdateTabBar)
+      this.updateTabBar();
+    else if (shouldShowTabView) {
+      tab.tabItem.setZoomPrep(false);
+      UI.showTabView();
+    }
+  },
+
+  // ----------
+  // Function: killNewTabGroup
+  // Removes the New Tab Group, which is now defunct. See bug 575851 and comments therein.
+  killNewTabGroup: function() {
+    this.groupItems.forEach(function(groupItem) {
+      if (groupItem.getTitle() == 'New Tabs' && groupItem.locked.title) {
+        groupItem.removeAll();
+        groupItem.close();
+      }
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/infoitems.js
@@ -0,0 +1,258 @@
+/* ***** 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 infoitems.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ * Ehsan Akhgari <ehsan@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: infoitems.js
+
+(function() {
+
+// ##########
+// Class: InfoItem
+// An <Item> in TabView used for displaying information, such as the welcome video.
+// Note that it implements the <Subscribable> interface.
+//
+// ----------
+// Constructor: InfoItem
+//
+// Parameters:
+//   bounds - a <Rect> for where the item should be located
+//   options - various options for this infoItem (see below)
+//
+// Possible options:
+//   locked - see <Item.locked>; default is {}
+//   dontPush - true if this infoItem shouldn't push away on creation; default is false
+window.InfoItem = function(bounds, options) {
+  try {
+    Utils.assertThrow(Utils.isRect(bounds), 'bounds');
+
+    if (typeof options == 'undefined')
+      options = {};
+
+    this._inited = false;
+    this.isAnInfoItem = true;
+    this.defaultSize = bounds.size();
+    this.locked = (options.locked ? Utils.copy(options.locked) : {});
+    this.bounds = new Rect(bounds);
+    this.isDragging = false;
+
+    var self = this;
+
+    var $container = iQ('<div>')
+      .addClass('info-item')
+      .css(this.bounds)
+      .appendTo('body');
+
+    this.$contents = iQ('<div>')
+      .appendTo($container);
+
+    var $close = iQ('<div>')
+      .addClass('close')
+      .click(function() {
+        self.close();
+      })
+      .appendTo($container);
+
+    // ___ locking
+    if (this.locked.bounds)
+      $container.css({cursor: 'default'});
+
+    if (this.locked.close)
+      $close.hide();
+
+    // ___ Superclass initialization
+    this._init($container[0]);
+
+    if (this.$debug)
+      this.$debug.css({zIndex: -1000});
+
+    // ___ Finish Up
+    if (!this.locked.bounds)
+      this.draggable();
+
+    // ___ Position
+    this.snap();
+
+    // ___ Push other objects away
+    if (!options.dontPush)
+      this.pushAway();
+
+    this._inited = true;
+    this.save();
+  } catch(e) {
+    Utils.log(e);
+  }
+};
+
+// ----------
+window.InfoItem.prototype = Utils.extend(new Item(), new Subscribable(), {
+
+  // ----------
+  // Function: getStorageData
+  // Returns all of the info worth storing about this item.
+  getStorageData: function() {
+    var data = null;
+
+    try {
+      data = {
+        bounds: this.getBounds(),
+        locked: Utils.copy(this.locked)
+      };
+    } catch(e) {
+      Utils.log(e);
+    }
+
+    return data;
+  },
+
+  // ----------
+  // Function: save
+  // Saves this item to persistent storage.
+  save: function() {
+    try {
+      if (!this._inited) // too soon to save now
+        return;
+
+      var data = this.getStorageData();
+
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: setBounds
+  // Sets the bounds with the given <Rect>, animating unless "immediately" is false.
+  setBounds: function(rect, immediately) {
+    try {
+      Utils.assertThrow(Utils.isRect(rect), 'InfoItem.setBounds: rect must be a real rectangle!');
+
+      // ___ Determine what has changed
+      var css = {};
+
+      if (rect.left != this.bounds.left)
+        css.left = rect.left;
+
+      if (rect.top != this.bounds.top)
+        css.top = rect.top;
+
+      if (rect.width != this.bounds.width)
+        css.width = rect.width;
+
+      if (rect.height != this.bounds.height)
+        css.height = rect.height;
+
+      if (Utils.isEmptyObject(css))
+        return;
+
+      this.bounds = new Rect(rect);
+      Utils.assertThrow(Utils.isRect(this.bounds), 
+          'InfoItem.setBounds: this.bounds must be a real rectangle!');
+
+      // ___ Update our representation
+      if (immediately) {
+        iQ(this.container).css(css);
+      } else {
+        TabItems.pausePainting();
+        iQ(this.container).animate(css, {
+          duration: 350,
+          easing: "tabviewBounce",
+          complete: function() {
+            TabItems.resumePainting();
+          }
+        });
+      }
+
+      this._updateDebugBounds();
+      this.setTrenches(rect);
+      this.save();
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: setZ
+  // Set the Z order for the item's container.
+  setZ: function(value) {
+    try {
+      Utils.assertThrow(typeof value == 'number', 'value must be a number');
+
+      this.zIndex = value;
+
+      iQ(this.container).css({zIndex: value});
+
+      if (this.$debug)
+        this.$debug.css({zIndex: value + 1});
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: close
+  // Closes the item.
+  close: function() {
+    try {
+      this._sendToSubscribers("close");
+      this.removeTrenches();
+      iQ(this.container).fadeOut(function() {
+        iQ(this).remove();
+        Items.unsquish();
+      });
+
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: html
+  // Sets the item's container's html to the specified value.
+  html: function(value) {
+    try {
+      Utils.assertThrow(typeof value == 'string', 'value must be a string');
+      this.$contents.html(value);
+    } catch(e) {
+      Utils.log(e);
+    }
+  }
+});
+
+})();
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/iq.js
@@ -0,0 +1,720 @@
+/* ***** 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 iq.js.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ *
+ * This file incorporates work from:
+ * jQuery JavaScript Library v1.4.2: http://code.jquery.com/jquery-1.4.2.js
+ * This incorporated work is covered by the following copyright and
+ * permission notice:
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * 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 ***** */
+
+// **********
+// Title: iq.js
+// Various helper functions, in the vein of jQuery.
+
+// ----------
+// Function: iQ
+// Returns an iQClass object which represents an individual element or a group
+// of elements. It works pretty much like jQuery(), with a few exceptions,
+// most notably that you can't use strings with complex html,
+// just simple tags like '<div>'.
+function iQ(selector, context) {
+  // The iQ object is actually just the init constructor 'enhanced'
+  return new iQClass(selector, context);
+};
+
+// A simple way to check for HTML strings or ID strings
+// (both of which we optimize for)
+let quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/;
+
+// Match a standalone tag
+let rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/;
+
+// ##########
+// Class: iQClass
+// The actual class of iQ result objects, representing an individual element
+// or a group of elements.
+//
+// ----------
+// Function: iQClass
+// You don't call this directly; this is what's called by iQ().
+let iQClass = function(selector, context) {
+
+  // Handle $(""), $(null), or $(undefined)
+  if (!selector) {
+    return this;
+  }
+
+  // Handle $(DOMElement)
+  if (selector.nodeType) {
+    this.context = selector;
+    this[0] = selector;
+    this.length = 1;
+    return this;
+  }
+
+  // The body element only exists once, optimize finding it
+  if (selector === "body" && !context) {
+    this.context = document;
+    this[0] = document.body;
+    this.selector = "body";
+    this.length = 1;
+    return this;
+  }
+
+  // Handle HTML strings
+  if (typeof selector === "string") {
+    // Are we dealing with HTML string or an ID?
+
+    let match = quickExpr.exec(selector);
+
+    // Verify a match, and that no context was specified for #id
+    if (match && (match[1] || !context)) {
+
+      // HANDLE $(html) -> $(array)
+      if (match[1]) {
+        let doc = (context ? context.ownerDocument || context : document);
+
+        // If a single string is passed in and it's a single tag
+        // just do a createElement and skip the rest
+        let ret = rsingleTag.exec(selector);
+
+        if (ret) {
+          if (Utils.isPlainObject(context)) {
+            Utils.assert(false, 'does not support HTML creation with context');
+          } else {
+            selector = [doc.createElement(ret[1])];
+          }
+
+        } else {
+          Utils.assert(false, 'does not support complex HTML creation');
+        }
+
+        return Utils.merge(this, selector);
+
+      // HANDLE $("#id")
+      } else {
+        let elem = document.getElementById(match[2]);
+
+        if (elem) {
+          this.length = 1;
+          this[0] = elem;
+        }
+
+        this.context = document;
+        this.selector = selector;
+        return this;
+      }
+
+    // HANDLE $("TAG")
+    } else if (!context && /^\w+$/.test(selector)) {
+      this.selector = selector;
+      this.context = document;
+      selector = document.getElementsByTagName(selector);
+      return Utils.merge(this, selector);
+
+    // HANDLE $(expr, $(...))
+    } else if (!context || context.iq) {
+      return (context || iQ(document)).find(selector);
+
+    // HANDLE $(expr, context)
+    // (which is just equivalent to: $(context).find(expr)
+    } else {
+      return iQ(context).find(selector);
+    }
+
+  // HANDLE $(function)
+  // Shortcut for document ready
+  } else if (typeof selector == "function") {
+    Utils.log('iQ does not support ready functions');
+    return null;
+  }
+
+  if (typeof selector.selector !== "undefined") {
+    this.selector = selector.selector;
+    this.context = selector.context;
+  }
+
+  let ret = this || [];
+  if (selector != null) {
+    // The window, strings (and functions) also have 'length'
+    if (selector.length == null || typeof selector == "string" || selector.setInterval) {
+      Array.push(ret, selector);
+    } else {
+      Utils.merge(ret, selector);
+    }
+  }
+  return ret;
+};
+  
+iQClass.prototype = {
+
+  // Start with an empty selector
+  selector: "",
+
+  // The default length of a iQ object is 0
+  length: 0,
+
+  // ----------
+  // Function: each
+  // Execute a callback for every element in the matched set.
+  each: function(callback) {
+    if (typeof callback != "function") {
+      Utils.assert(false, "each's argument must be a function");
+      return null;
+    }
+    for (let i = 0; this[i] != null; i++) {
+      callback(this[i]);
+    }
+    return this;
+  },
+
+  // ----------
+  // Function: addClass
+  // Adds the given class(es) to the receiver.
+  addClass: function(value) {
+    Utils.assertThrow(typeof value == "string" && value,
+                      'requires a valid string argument');
+
+    let length = this.length;
+    for (let i = 0; i < length; i++) {
+      let elem = this[i];
+      if (elem.nodeType === 1) {
+        value.split(/\s+/).forEach(function(className) {
+          elem.classList.add(className);
+        });
+      }
+    }
+
+    return this;
+  },
+
+  // ----------
+  // Function: removeClass
+  // Removes the given class(es) from the receiver.
+  removeClass: function(value) {
+    if (typeof value != "string" || !value) {
+      Utils.assert(false, 'does not support function argument');
+      return null;
+    }
+
+    let length = this.length;
+    for (let i = 0; i < length; i++) {
+      let elem = this[i];
+      if (elem.nodeType === 1 && elem.className) {
+        value.split(/\s+/).forEach(function(className) {
+          elem.classList.remove(className);
+        });
+      }
+    }
+
+    return this;
+  },
+
+  // ----------
+  // Function: hasClass
+  // Returns true is the receiver has the given css class.
+  hasClass: function(singleClassName) {
+    let length = this.length;
+    for (let i = 0; i < length; i++) {
+      if (this[i].classList.contains(singleClassName)) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  // ----------
+  // Function: find
+  // Searches the receiver and its children, returning a new iQ object with
+  // elements that match the given selector.
+  find: function(selector) {
+    let ret = [];
+    let length = 0;
+
+    let l = this.length;
+    for (let i = 0; i < l; i++) {
+      length = ret.length;
+      try {
+        Utils.merge(ret, this[i].querySelectorAll(selector));
+      } catch(e) {
+        Utils.log('iQ.find error (bad selector)', e);
+      }
+
+      if (i > 0) {
+        // Make sure that the results are unique
+        for (let n = length; n < ret.length; n++) {
+          for (let r = 0; r < length; r++) {
+            if (ret[r] === ret[n]) {
+              ret.splice(n--, 1);
+              break;
+            }
+          }
+        }
+      }
+    }
+
+    return iQ(ret);
+  },
+
+  // ----------
+  // Function: remove
+  // Removes the receiver from the DOM.
+  remove: function() {
+    for (let i = 0; this[i] != null; i++) {
+      let elem = this[i];
+      if (elem.parentNode) {
+        elem.parentNode.removeChild(elem);
+      }
+    }
+    return this;
+  },
+
+  // ----------
+  // Function: empty
+  // Removes all of the reciever's children and HTML content from the DOM.
+  empty: function() {
+    for (let i = 0; this[i] != null; i++) {
+      let elem = this[i];
+      while (elem.firstChild) {
+        elem.removeChild(elem.firstChild);
+      }
+    }
+    return this;
+  },
+
+  // ----------
+  // Function: width
+  // Returns the width of the receiver.
+  width: function() {
+    let bounds = this.bounds();
+    return bounds.width;
+  },
+
+  // ----------
+  // Function: height
+  // Returns the height of the receiver.
+  height: function() {
+    let bounds = this.bounds();
+    return bounds.height;
+  },
+
+  // ----------
+  // Function: position
+  // Returns an object with the receiver's position in left and top
+  // properties.
+  position: function() {
+    let bounds = this.bounds();
+    return new Point(bounds.left, bounds.top);
+  },
+
+  // ----------
+  // Function: bounds
+  // Returns a <Rect> with the receiver's bounds.
+  bounds: function() {
+    Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+    let rect = this[0].getBoundingClientRect();
+    return new Rect(Math.floor(rect.left), Math.floor(rect.top),
+                    Math.floor(rect.width), Math.floor(rect.height));
+  },
+
+  // ----------
+  // Function: data
+  // Pass in both key and value to attach some data to the receiver;
+  // pass in just key to retrieve it.
+  data: function(key, value) {
+    let data = null;
+    if (typeof value === "undefined") {
+      Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+      data = this[0].iQData;
+      if (data)
+        return data[key];
+      else
+        return null;
+    }
+
+    for (let i = 0; this[i] != null; i++) {
+      let elem = this[i];
+      data = elem.iQData;
+
+      if (!data)
+        data = elem.iQData = {};
+
+      data[key] = value;
+    }
+
+    return this;
+  },
+
+  // ----------
+  // Function: html
+  // Given a value, sets the receiver's innerHTML to it; otherwise returns
+  // what's already there.
+  html: function(value) {
+    Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+    if (typeof value === "undefined")
+      return this[0].innerHTML;
+
+    this[0].innerHTML = value;
+    return this;
+  },
+
+  // ----------
+  // Function: text
+  // Given a value, sets the receiver's textContent to it; otherwise returns
+  // what's already there.
+  text: function(value) {
+    Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+    if (typeof value === "undefined") {
+      return this[0].textContent;
+    }
+
+    return this.empty().append((this[0] && this[0].ownerDocument || document).createTextNode(value));
+  },
+
+  // ----------
+  // Function: val
+  // Given a value, sets the receiver's value to it; otherwise returns what's already there.
+  val: function(value) {
+    Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+    if (typeof value === "undefined") {
+      return this[0].value;
+    }
+
+    this[0].value = value;
+    return this;
+  },
+
+  // ----------
+  // Function: appendTo
+  // Appends the receiver to the result of iQ(selector).
+  appendTo: function(selector) {
+    Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+    iQ(selector).append(this);
+    return this;
+  },
+
+  // ----------
+  // Function: append
+  // Appends the result of iQ(selector) to the receiver.
+  append: function(selector) {
+    let object = iQ(selector);
+    Utils.assert(object.length == 1 && this.length == 1, 
+        'does not yet support multi-objects (or null objects)');
+    this[0].appendChild(object[0]);
+    return this;
+  },
+
+  // ----------
+  // Function: attr
+  // Sets or gets an attribute on the element(s).
+  attr: function(key, value) {
+    Utils.assert(typeof key === 'string', 'string key');
+    if (typeof value === "undefined") {
+      Utils.assert(this.length == 1, 'retrieval does not support multi-objects (or null objects)');
+      return this[0].getAttribute(key);
+    }
+
+    for (let i = 0; this[i] != null; i++)
+      this[i].setAttribute(key, value);
+
+    return this;
+  },
+
+  // ----------
+  // Function: css
+  // Sets or gets CSS properties on the receiver. When setting certain numerical properties,
+  // will automatically add "px". A property can be removed by setting it to null.
+  //
+  // Possible call patterns:
+  //   a: object, b: undefined - sets with properties from a
+  //   a: string, b: undefined - gets property specified by a
+  //   a: string, b: string/number - sets property specified by a to b
+  css: function(a, b) {
+    let properties = null;
+
+    if (typeof a === 'string') {
+      let key = a;
+      if (typeof b === "undefined") {
+        Utils.assert(this.length == 1, 'retrieval does not support multi-objects (or null objects)');
+
+        return window.getComputedStyle(this[0], null).getPropertyValue(key);
+      }
+      properties = {};
+      properties[key] = b;
+    } else {
+      properties = a;
+    }
+
+    let pixels = {
+      'left': true,
+      'top': true,
+      'right': true,
+      'bottom': true,
+      'width': true,
+      'height': true
+    };
+
+    for (let i = 0; this[i] != null; i++) {
+      let elem = this[i];
+      for (let key in properties) {
+        let value = properties[key];
+        if (pixels[key] && typeof value != 'string')
+          value += 'px';
+
+        if (value == null) {
+          elem.style.removeProperty(key);
+        } else if (key.indexOf('-') != -1)
+          elem.style.setProperty(key, value, '');
+        else
+          elem.style[key] = value;
+      }
+    }
+
+    return this;
+  },
+
+  // ----------
+  // Function: animate
+  // Uses CSS transitions to animate the element.
+  //
+  // Parameters:
+  //   css - an object map of the CSS properties to change
+  //   options - an object with various properites (see below)
+  //
+  // Possible "options" properties:
+  //   duration - how long to animate, in milliseconds
+  //   easing - easing function to use. Possibilities include
+  //     "tabviewBounce", "easeInQuad". Default is "ease".
+  //   complete - function to call once the animation is done, takes nothing
+  //     in, but "this" is set to the element that was animated.
+  animate: function(css, options) {
+    Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
+
+    if (!options)
+      options = {};
+
+    let easings = {
+      tabviewBounce: "cubic-bezier(0.0, 0.63, .6, 1.0)", 
+      // TODO: change 1.0 above to 1.29 after bug 575672 is fixed
+      
+      easeInQuad: 'ease-in', // TODO: make it a real easeInQuad, or decide we don't care
+      fast: 'cubic-bezier(0.7,0,1,1)'
+    };
+
+    let duration = (options.duration || 400);
+    let easing = (easings[options.easing] || 'ease');
+
+    // The latest versions of Firefox do not animate from a non-explicitly
+    // set css properties. So for each element to be animated, go through
+    // and explicitly define 'em.
+    let rupper = /([A-Z])/g;
+    this.each(function(elem) {
+      let cStyle = window.getComputedStyle(elem, null);
+      for (let prop in css) {
+        prop = prop.replace(rupper, "-$1").toLowerCase();
+        iQ(elem).css(prop, cStyle.getPropertyValue(prop));
+      }
+    });
+
+    this.css({
+      '-moz-transition-property': 'all', // TODO: just animate the properties we're changing
+      '-moz-transition-duration': (duration / 1000) + 's',
+      '-moz-transition-timing-function': easing
+    });
+
+    this.css(css);
+
+    let self = this;
+    setTimeout(function() {
+      self.css({
+        '-moz-transition-property': 'none',
+        '-moz-transition-duration': '',
+        '-moz-transition-timing-function': ''
+      });
+
+      if (typeof options.complete == "function")
+        options.complete.apply(self);
+    }, duration);
+
+    return this;
+  },
+
+  // ----------
+  // Function: fadeOut
+  // Animates the receiver to full transparency. Calls callback on completion.
+  fadeOut: function(callback) {
+    Utils.assert(typeof callback == "function" || typeof callback === "undefined", 
+        'does not yet support duration');
+
+    this.animate({
+      opacity: 0
+    }, {
+      duration: 400,
+      complete: function() {
+        iQ(this).css({display: 'none'});
+        if (typeof callback == "function")
+          callback.apply(this);
+      }
+    });
+
+    return this;
+  },
+
+  // ----------
+  // Function: fadeIn
+  // Animates the receiver to full opacity.
+  fadeIn: function() {
+    this.css({display: ''});
+    this.animate({
+      opacity: 1
+    }, {
+      duration: 400
+    });
+
+    return this;
+  },
+
+  // ----------
+  // Function: hide
+  // Hides the receiver.
+  hide: function() {
+    this.css({display: 'none', opacity: 0});
+    return this;
+  },
+
+  // ----------
+  // Function: show
+  // Shows the receiver.
+  show: function() {
+    this.css({display: '', opacity: 1});
+    return this;
+  },
+
+  // ----------
+  // Function: bind
+  // Binds the given function to the given event type. Also wraps the function
+  // in a try/catch block that does a Utils.log on any errors.
+  bind: function(type, func) {
+    let handler = function(event) func.apply(this, [event]);
+
+    for (let i = 0; this[i] != null; i++) {
+      let elem = this[i];
+      if (!elem.iQEventData)
+        elem.iQEventData = {};
+
+      if (!elem.iQEventData[type])
+        elem.iQEventData[type] = [];
+
+      elem.iQEventData[type].push({
+        original: func,
+        modified: handler
+      });
+
+      elem.addEventListener(type, handler, false);
+    }
+
+    return this;
+  },
+
+  // ----------
+  // Function: one
+  // Binds the given function to the given event type, but only for one call;
+  // automatically unbinds after the event fires once.
+  one: function(type, func) {
+    Utils.assert(typeof func == "function", 'does not support eventData argument');
+
+    let handler = function(e) {
+      iQ(this).unbind(type, handler);
+      return func.apply(this, [e]);
+    };
+
+    return this.bind(type, handler);
+  },
+
+  // ----------
+  // Function: unbind
+  // Unbinds the given function from the given event type.
+  unbind: function(type, func) {
+    Utils.assert(typeof func == "function", 'Must provide a function');
+
+    for (let i = 0; this[i] != null; i++) {
+      let elem = this[i];
+      let handler = func;
+      if (elem.iQEventData && elem.iQEventData[type]) {
+        let count = elem.iQEventData[type].length;
+        for (let a = 0; a < count; a++) {
+          let pair = elem.iQEventData[type][a];
+          if (pair.original == func) {
+            handler = pair.modified;
+            elem.iQEventData[type].splice(a, 1);
+            break;
+          }
+        }
+      }
+
+      elem.removeEventListener(type, handler, false);
+    }
+
+    return this;
+  }
+};
+
+// ----------
+// Create various event aliases
+let events = [
+  'keyup',
+  'keydown',
+  'mouseup',
+  'mousedown',
+  'mouseover',
+  'mouseout',
+  'mousemove',
+  'click',
+  'resize',
+  'change',
+  'blur',
+  'focus'
+];
+
+events.forEach(function(event) {
+  iQClass.prototype[event] = function(func) {
+    return this.bind(event, func);
+  };
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/items.js
@@ -0,0 +1,1067 @@
+/* ***** 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 items.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: items.js
+
+// ##########
+// Class: Item
+// Superclass for all visible objects (<TabItem>s and <GroupItem>s).
+//
+// If you subclass, in addition to the things Item provides, you need to also provide these methods:
+//   setBounds - function(rect, immediately)
+//   setZ - function(value)
+//   close - function()
+//   save - function()
+//
+// Subclasses of Item must also provide the <Subscribable> interface.
+//
+// ... and this property:
+//   defaultSize - a Point
+//   locked - an object (see below)
+//
+// Make sure to call _init() from your subclass's constructor.
+window.Item = function() {
+  // Variable: isAnItem
+  // Always true for Items
+  this.isAnItem = true;
+
+  // Variable: bounds
+  // The position and size of this Item, represented as a <Rect>.
+  this.bounds = null;
+
+  // Variable: zIndex
+  // The z-index for this item.
+  this.zIndex = 0;
+
+  // Variable: debug
+  // When set to true, displays a rectangle on the screen that corresponds with bounds.
+  // May be used for additional debugging features in the future.
+  this.debug = false;
+
+  // Variable: $debug
+  // If <debug> is true, this will be the iQ object for the visible rectangle.
+  this.$debug = null;
+
+  // Variable: container
+  // The outermost DOM element that describes this item on screen.
+  this.container = null;
+
+  // Variable: locked
+  // Affects whether an item can be pushed, closed, renamed, etc
+  //
+  // The object may have properties to specify what can't be changed:
+  //   .bounds - true if it can't be pushed, dragged, resized, etc
+  //   .close - true if it can't be closed
+  //   .title - true if it can't be renamed
+  this.locked = null;
+
+  // Variable: parent
+  // The groupItem that this item is a child of
+  this.parent = null;
+
+  // Variable: userSize
+  // A <Point> that describes the last size specifically chosen by the user.
+  // Used by unsquish.
+  this.userSize = null;
+
+  // Variable: dragOptions
+  // Used by <draggable>
+  //
+  // Possible properties:
+  //   cancelClass - A space-delimited list of classes that should cancel a drag
+  //   start - A function to be called when a drag starts
+  //   drag - A function to be called each time the mouse moves during drag
+  //   stop - A function to be called when the drag is done
+  this.dragOptions = null;
+
+  // Variable: dropOptions
+  // Used by <draggable> if the item is set to droppable.
+  //
+  // Possible properties:
+  //   accept - A function to determine if a particular item should be accepted for dropping
+  //   over - A function to be called when an item is over this item
+  //   out - A function to be called when an item leaves this item
+  //   drop - A function to be called when an item is dropped in this item
+  this.dropOptions = null;
+
+  // Variable: resizeOptions
+  // Used by <resizable>
+  //
+  // Possible properties:
+  //   minWidth - Minimum width allowable during resize
+  //   minHeight - Minimum height allowable during resize
+  //   aspectRatio - true if we should respect aspect ratio; default false
+  //   start - A function to be called when resizing starts
+  //   resize - A function to be called each time the mouse moves during resize
+  //   stop - A function to be called when the resize is done
+  this.resizeOptions = null;
+
+  // Variable: isDragging
+  // Boolean for whether the item is currently being dragged or not.
+  this.isDragging = false;
+};
+
+window.Item.prototype = {
+  // ----------
+  // Function: _init
+  // Initializes the object. To be called from the subclass's intialization function.
+  //
+  // Parameters:
+  //   container - the outermost DOM element that describes this item onscreen.
+  _init: function(container) {
+    Utils.assert(typeof this.addSubscriber == 'function' && 
+        typeof this.removeSubscriber == 'function' && 
+        typeof this._sendToSubscribers == 'function',
+        'Subclass must implement the Subscribable interface');
+    Utils.assert(Utils.isDOMElement(container), 'container must be a DOM element');
+    Utils.assert(typeof this.setBounds == 'function', 'Subclass must provide setBounds');
+    Utils.assert(typeof this.setZ == 'function', 'Subclass must provide setZ');
+    Utils.assert(typeof this.close == 'function', 'Subclass must provide close');
+    Utils.assert(typeof this.save == 'function', 'Subclass must provide save');
+    Utils.assert(Utils.isPoint(this.defaultSize), 'Subclass must provide defaultSize');
+    Utils.assert(this.locked, 'Subclass must provide locked');
+    Utils.assert(Utils.isRect(this.bounds), 'Subclass must provide bounds');
+
+    this.container = container;
+
+    if (this.debug) {
+      this.$debug = iQ('<div>')
+        .css({
+          border: '2px solid green',
+          zIndex: -10,
+          position: 'absolute'
+        })
+        .appendTo('body');
+    }
+
+    iQ(this.container).data('item', this);
+
+    // ___ drag
+    this.dragOptions = {
+      cancelClass: 'close stackExpander',
+      start: function(e, ui) {
+        if (this.isAGroupItem)
+          GroupItems.setActiveGroupItem(this);
+        drag.info = new Drag(this, e);
+      },
+      drag: function(e) {
+        drag.info.drag(e);
+      },
+      stop: function() {
+        drag.info.stop();
+        drag.info = null;
+      }
+    };
+
+    // ___ drop
+    this.dropOptions = {
+      over: function() {},
+      out: function() {
+        var groupItem = drag.info.item.parent;
+        if (groupItem)
+          groupItem.remove(drag.info.$el, {dontClose: true});
+
+        iQ(this.container).removeClass("acceptsDrop");
+      },
+      drop: function(event) {
+        iQ(this.container).removeClass("acceptsDrop");
+      },
+      // Function: dropAcceptFunction
+      // Given a DOM element, returns true if it should accept tabs being dropped on it.
+      // Private to this file.
+      accept: function dropAcceptFunction(item) {
+        return (item && item.isATabItem && (!item.parent || !item.parent.expanded));
+      }
+    };
+
+    // ___ resize
+    var self = this;
+    var resizeInfo = null;
+    this.resizeOptions = {
+      aspectRatio: self.keepProportional,
+      minWidth: 90,
+      minHeight: 90,
+      start: function(e,ui) {
+        if (this.isAGroupItem)
+          GroupItems.setActiveGroupItem(this);
+        resizeInfo = new Drag(this, e, true); // true = isResizing
+      },
+      resize: function(e,ui) {
+        // TODO: maybe the stationaryCorner should be topright for rtl langs?
+        resizeInfo.snap('topleft', false, self.keepProportional);
+      },
+      stop: function() {
+        self.setUserSize();
+        self.pushAway();
+        resizeInfo.stop();
+        resizeInfo = null;
+      }
+    };
+  },
+
+  // ----------
+  // Function: getBounds
+  // Returns a copy of the Item's bounds as a <Rect>.
+  getBounds: function() {
+    Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
+    return new Rect(this.bounds);
+  },
+
+  // ----------
+  // Function: overlapsWithOtherItems
+  // Returns true if this Item overlaps with any other Item on the screen.
+  overlapsWithOtherItems: function() {
+    var self = this;
+    var items = Items.getTopLevelItems();
+    var bounds = this.getBounds();
+    return items.some(function(item) {
+      if (item == self) // can't overlap with yourself.
+        return false;
+      var myBounds = item.getBounds();
+      return myBounds.intersects(bounds);
+    } );
+  },
+
+  // ----------
+  // Function: setPosition
+  // Moves the Item to the specified location.
+  //
+  // Parameters:
+  //   left - the new left coordinate relative to the window
+  //   top - the new top coordinate relative to the window
+  //   immediately - if false or omitted, animates to the new position;
+  //   otherwise goes there immediately
+  setPosition: function(left, top, immediately) {
+    Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
+    this.setBounds(new Rect(left, top, this.bounds.width, this.bounds.height), immediately);
+  },
+
+  // ----------
+  // Function: setSize
+  // Resizes the Item to the specified size.
+  //
+  // Parameters:
+  //   width - the new width in pixels
+  //   height - the new height in pixels
+  //   immediately - if false or omitted, animates to the new size;
+  //   otherwise resizes immediately
+  setSize: function(width, height, immediately) {
+    Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
+    this.setBounds(new Rect(this.bounds.left, this.bounds.top, width, height), immediately);
+  },
+
+  // ----------
+  // Function: setUserSize
+  // Remembers the current size as one the user has chosen.
+  setUserSize: function() {
+    Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
+    this.userSize = new Point(this.bounds.width, this.bounds.height);
+    this.save();
+  },
+
+  // ----------
+  // Function: getZ
+  // Returns the zIndex of the Item.
+  getZ: function() {
+    return this.zIndex;
+  },
+
+  // ----------
+  // Function: setRotation
+  // Rotates the object to the given number of degrees.
+  setRotation: function(degrees) {
+    var value = degrees ? "rotate(%deg)".replace(/%/, degrees) : null;
+    iQ(this.container).css({"-moz-transform": value});
+  },
+
+  // ----------
+  // Function: setParent
+  // Sets the receiver's parent to the given <Item>.
+  setParent: function(parent) {
+    this.parent = parent;
+    this.removeTrenches();
+    this.save();
+  },
+
+  // ----------
+  // Function: pushAway
+  // Pushes all other items away so none overlap this Item.
+  pushAway: function() {
+    var buffer = Math.floor(Items.defaultGutter / 2);
+
+    var items = Items.getTopLevelItems();
+    // setup each Item's pushAwayData attribute:
+    items.forEach(function pushAway_setupPushAwayData(item) {
+      var data = {};
+      data.bounds = item.getBounds();
+      data.startBounds = new Rect(data.bounds);
+      // Infinity = (as yet) unaffected
+      data.generation = Infinity;
+      item.pushAwayData = data;
+    });
+
+    // The first item is a 0-generation pushed item. It all starts here.
+    var itemsToPush = [this];
+    this.pushAwayData.generation = 0;
+
+    var pushOne = function(baseItem) {
+      // the baseItem is an n-generation pushed item. (n could be 0)
+      var baseData = baseItem.pushAwayData;
+      var bb = new Rect(baseData.bounds);
+
+      // make the bounds larger, adding a +buffer margin to each side.
+      bb.inset(-buffer, -buffer);
+      // bbc = center of the base's bounds
+      var bbc = bb.center();
+
+      items.forEach(function(item) {
+        if (item == baseItem || item.locked.bounds)
+          return;
+
+        var data = item.pushAwayData;
+        // if the item under consideration has already been pushed, or has a lower
+        // "generation" (and thus an implictly greater placement priority) then don't move it.
+        if (data.generation <= baseData.generation)
+          return;
+
+        // box = this item's current bounds, with a +buffer margin.
+        var bounds = data.bounds;
+        var box = new Rect(bounds);
+        box.inset(-buffer, -buffer);
+
+        // if the item under consideration overlaps with the base item...
+        if (box.intersects(bb)) {
+
+          // Let's push it a little.
+
+          // First, decide in which direction and how far to push. This is the offset.
+          var offset = new Point();
+          // center = the current item's center.
+          var center = box.center();
+
+          // Consider the relationship between the current item (box) + the base item.
+          // If it's more vertically stacked than "side by side"...
+          if (Math.abs(center.x - bbc.x) < Math.abs(center.y - bbc.y)) {
+            // push vertically.
+            if (center.y > bbc.y)
+              offset.y = bb.bottom - box.top;
+            else
+              offset.y = bb.top - box.bottom;
+          } else { // if they're more "side by side" than stacked vertically...
+            // push horizontally.
+            if (center.x > bbc.x)
+              offset.x = bb.right - box.left;
+            else
+              offset.x = bb.left - box.right;
+          }
+
+          // Actually push the Item.
+          bounds.offset(offset);
+
+          // This item now becomes an (n+1)-generation pushed item.
+          data.generation = baseData.generation + 1;
+          // keep track of who pushed this item.
+          data.pusher = baseItem;
+          // add this item to the queue, so that it, in turn, can push some other things.
+          itemsToPush.push(item);
+        }
+      });
+    };
+
+    // push each of the itemsToPush, one at a time.
+    // itemsToPush starts with just [this], but pushOne can add more items to the stack.
+    // Maximally, this could run through all Items on the screen.
+    while (itemsToPush.length)
+      pushOne(itemsToPush.shift());
+
+    // ___ Squish!
+    var pageBounds = Items.getSafeWindowBounds();
+    items.forEach(function(item) {
+      var data = item.pushAwayData;
+      if (data.generation == 0 || item.locked.bounds)
+        return;
+
+      function apply(item, posStep, posStep2, sizeStep) {
+        var data = item.pushAwayData;
+        if (data.generation == 0)
+          return;
+
+        var bounds = data.bounds;
+        bounds.width -= sizeStep.x;
+        bounds.height -= sizeStep.y;
+        bounds.left += posStep.x;
+        bounds.top += posStep.y;
+
+        if (!item.isAGroupItem) {
+          if (sizeStep.y > sizeStep.x) {
+            var newWidth = bounds.height * (TabItems.tabWidth / TabItems.tabHeight);
+            bounds.left += (bounds.width - newWidth) / 2;
+            bounds.width = newWidth;
+          } else {
+            var newHeight = bounds.width * (TabItems.tabHeight / TabItems.tabWidth);
+            bounds.top += (bounds.height - newHeight) / 2;
+            bounds.height = newHeight;
+          }
+        }
+
+        var pusher = data.pusher;
+        if (pusher) {
+          var newPosStep = new Point(posStep.x + posStep2.x, posStep.y + posStep2.y);
+          apply(pusher, newPosStep, posStep2, sizeStep);
+        }
+      }
+
+      var bounds = data.bounds;
+      var posStep = new Point();
+      var posStep2 = new Point();
+      var sizeStep = new Point();
+
+      if (bounds.left < pageBounds.left) {
+        posStep.x = pageBounds.left - bounds.left;
+        sizeStep.x = posStep.x / data.generation;
+        posStep2.x = -sizeStep.x;
+      } else if (bounds.right > pageBounds.right) {
+        posStep.x = pageBounds.right - bounds.right;
+        sizeStep.x = -posStep.x / data.generation;
+        posStep.x += sizeStep.x;
+        posStep2.x = sizeStep.x;
+      }
+
+      if (bounds.top < pageBounds.top) {
+        posStep.y = pageBounds.top - bounds.top;
+        sizeStep.y = posStep.y / data.generation;
+        posStep2.y = -sizeStep.y;
+      } else if (bounds.bottom > pageBounds.bottom) {
+        posStep.y = pageBounds.bottom - bounds.bottom;
+        sizeStep.y = -posStep.y / data.generation;
+        posStep.y += sizeStep.y;
+        posStep2.y = sizeStep.y;
+      }
+
+      if (posStep.x || posStep.y || sizeStep.x || sizeStep.y)
+        apply(item, posStep, posStep2, sizeStep);
+    });
+
+    // ___ Unsquish
+    var pairs = [];
+    items.forEach(function(item) {
+      var data = item.pushAwayData;
+      pairs.push({
+        item: item,
+        bounds: data.bounds
+      });
+    });
+
+    Items.unsquish(pairs);
+
+    // ___ Apply changes
+    items.forEach(function(item) {
+      var data = item.pushAwayData;
+      var bounds = data.bounds;
+      if (!bounds.equals(data.startBounds)) {
+        item.setBounds(bounds);
+      }
+    });
+  },
+
+  // ----------
+  // Function: _updateDebugBounds
+  // Called by a subclass when its bounds change, to update the debugging rectangles on screen.
+  // This functionality is enabled only by the debug property.
+  _updateDebugBounds: function() {
+    if (this.$debug) {
+      this.$debug.css(this.bounds.css());
+    }
+  },
+
+  // ----------
+  // Function: setTrenches
+  // Sets up/moves the trenches for snapping to this item.
+  setTrenches: function(rect) {
+    if (this.parent !== null)
+      return;
+
+    if (!this.borderTrenches)
+      this.borderTrenches = Trenches.registerWithItem(this,"border");
+
+    var bT = this.borderTrenches;
+    Trenches.getById(bT.left).setWithRect(rect);
+    Trenches.getById(bT.right).setWithRect(rect);
+    Trenches.getById(bT.top).setWithRect(rect);
+    Trenches.getById(bT.bottom).setWithRect(rect);
+
+    if (!this.guideTrenches)
+      this.guideTrenches = Trenches.registerWithItem(this,"guide");
+
+    var gT = this.guideTrenches;
+    Trenches.getById(gT.left).setWithRect(rect);
+    Trenches.getById(gT.right).setWithRect(rect);
+    Trenches.getById(gT.top).setWithRect(rect);
+    Trenches.getById(gT.bottom).setWithRect(rect);
+
+  },
+
+  // ----------
+  // Function: removeTrenches
+  // Removes the trenches for snapping to this item.
+  removeTrenches: function() {
+    for (var edge in this.borderTrenches) {
+      Trenches.unregister(this.borderTrenches[edge]); // unregister can take an array
+    }
+    this.borderTrenches = null;
+    for (var edge in this.guideTrenches) {
+      Trenches.unregister(this.guideTrenches[edge]); // unregister can take an array
+    }
+    this.guideTrenches = null;
+  },
+
+  // ----------
+  // Function: snap
+  // The snap function used during groupItem creation via drag-out
+  snap: function Item_snap() {
+    // make the snapping work with a wider range!
+    var defaultRadius = Trenches.defaultRadius;
+    Trenches.defaultRadius = 2 * defaultRadius; // bump up from 10 to 20!
+
+    var event = {startPosition:{}}; // faux event
+    var FauxDragInfo = new Drag(this,event,false,true);
+    // false == isDragging, true == isFauxDrag
+    FauxDragInfo.snap('none',false);
+    FauxDragInfo.stop();
+
+    Trenches.defaultRadius = defaultRadius;
+  },
+
+  // ----------
+  // Function: draggable
+  // Enables dragging on this item. Note: not to be called multiple times on the same item!
+  draggable: function() {
+    try {
+      Utils.assert(this.dragOptions, 'dragOptions');
+
+      var cancelClasses = [];
+      if (typeof this.dragOptions.cancelClass == 'string')
+        cancelClasses = this.dragOptions.cancelClass.split(' ');
+
+      var self = this;
+      var $container = iQ(this.container);
+      var startMouse;
+      var startPos;
+      var startSent;
+      var startEvent;
+      var droppables;
+      var dropTarget;
+
+      // ___ mousemove
+      var handleMouseMove = function(e) {
+        // positioning
+        var mouse = new Point(e.pageX, e.pageY);
+        var box = self.getBounds();
+        box.left = startPos.x + (mouse.x - startMouse.x);
+        box.top = startPos.y + (mouse.y - startMouse.y);
+
+        self.setBounds(box, true);
+
+        // drag events
+        if (!startSent) {
+          if (typeof self.dragOptions.start == "function")
+            self.dragOptions.start.apply(self,
+                [startEvent, {position: {left: startPos.x, top: startPos.y}}]);
+
+          startSent = true;
+        }
+
+        if (typeof self.dragOptions.drag == "function")
+          self.dragOptions.drag.apply(self, [e]);
+
+        // drop events
+        var best = {
+          dropTarget: null,
+          score: 0
+        };
+
+        droppables.forEach(function(droppable) {
+          var intersection = box.intersection(droppable.bounds);
+          if (intersection && intersection.area() > best.score) {
+            var possibleDropTarget = droppable.item;
+            var accept = true;
+            if (possibleDropTarget != dropTarget) {
+              var dropOptions = possibleDropTarget.dropOptions;
+              if (dropOptions && typeof dropOptions.accept == "function")
+                accept = dropOptions.accept.apply(possibleDropTarget, [self]);
+            }
+
+            if (accept) {
+              best.dropTarget = possibleDropTarget;
+              best.score = intersection.area();
+            }
+          }
+        });
+
+        if (best.dropTarget != dropTarget) {
+          var dropOptions;
+          if (dropTarget) {
+            dropOptions = dropTarget.dropOptions;
+            if (dropOptions && typeof dropOptions.out == "function")
+              dropOptions.out.apply(dropTarget, [e]);
+          }
+
+          dropTarget = best.dropTarget;
+
+          if (dropTarget) {
+            dropOptions = dropTarget.dropOptions;
+            if (dropOptions && typeof dropOptions.over == "function")
+              dropOptions.over.apply(dropTarget, [e]);
+          }
+        }
+
+        e.preventDefault();
+      };
+
+      // ___ mouseup
+      var handleMouseUp = function(e) {
+        iQ(gWindow)
+          .unbind('mousemove', handleMouseMove)
+          .unbind('mouseup', handleMouseUp);
+
+        if (dropTarget) {
+          var dropOptions = dropTarget.dropOptions;
+          if (dropOptions && typeof dropOptions.drop == "function")
+            dropOptions.drop.apply(dropTarget, [e]);
+        }
+
+        if (startSent && typeof self.dragOptions.stop == "function")
+          self.dragOptions.stop.apply(self, [e]);
+
+        e.preventDefault();
+      };
+
+      // ___ mousedown
+      $container.mousedown(function(e) {
+        if (Utils.isRightClick(e))
+          return;
+
+        var cancel = false;
+        var $target = iQ(e.target);
+        cancelClasses.forEach(function(className) {
+          if ($target.hasClass(className))
+            cancel = true;
+        });
+
+        if (cancel) {
+          e.preventDefault();
+          return;
+        }
+
+        startMouse = new Point(e.pageX, e.pageY);
+        startPos = self.getBounds().position();
+        startEvent = e;
+        startSent = false;
+        dropTarget = null;
+
+        droppables = [];
+        iQ('.iq-droppable').each(function(elem) {
+          if (elem != self.container) {
+            var item = Items.item(elem);
+            droppables.push({
+              item: item,
+              bounds: item.getBounds()
+            });
+          }
+        });
+
+        iQ(gWindow)
+          .mousemove(handleMouseMove)
+          .mouseup(handleMouseUp);
+
+        e.preventDefault();
+      });
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: droppable
+  // Enables or disables dropping on this item.
+  droppable: function(value) {
+    try {
+      var $container = iQ(this.container);
+      if (value)
+        $container.addClass('iq-droppable');
+      else {
+        Utils.assert(this.dropOptions, 'dropOptions');
+
+        $container.removeClass('iq-droppable');
+      }
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: resizable
+  // Enables or disables resizing of this item.
+  resizable: function(value) {
+    try {
+      var $container = iQ(this.container);
+      iQ('.iq-resizable-handle', $container).remove();
+
+      if (!value) {
+        $container.removeClass('iq-resizable');
+      } else {
+        Utils.assert(this.resizeOptions, 'resizeOptions');
+
+        $container.addClass('iq-resizable');
+
+        var self = this;
+        var startMouse;
+        var startSize;
+
+        // ___ mousemove
+        var handleMouseMove = function(e) {
+          var mouse = new Point(e.pageX, e.pageY);
+          var box = self.getBounds();
+          box.width = Math.max(self.resizeOptions.minWidth || 0, startSize.x + (mouse.x - startMouse.x));
+          box.height = Math.max(self.resizeOptions.minHeight || 0, startSize.y + (mouse.y - startMouse.y));
+
+          if (self.resizeOptions.aspectRatio) {
+            if (startAspect < 1)
+              box.height = box.width * startAspect;
+            else
+              box.width = box.height / startAspect;
+          }
+
+          self.setBounds(box, true);
+
+          if (typeof self.resizeOptions.resize == "function")
+            self.resizeOptions.resize.apply(self, [e]);
+
+          e.preventDefault();
+          e.stopPropagation();
+        };
+
+        // ___ mouseup
+        var handleMouseUp = function(e) {
+          iQ(gWindow)
+            .unbind('mousemove', handleMouseMove)
+            .unbind('mouseup', handleMouseUp);
+
+          if (typeof self.resizeOptions.stop == "function")
+            self.resizeOptions.stop.apply(self, [e]);
+
+          e.preventDefault();
+          e.stopPropagation();
+        };
+
+        // ___ handle + mousedown
+        iQ('<div>')
+          .addClass('iq-resizable-handle iq-resizable-se')
+          .appendTo($container)
+          .mousedown(function(e) {
+            if (Utils.isRightClick(e))
+              return;
+
+            startMouse = new Point(e.pageX, e.pageY);
+            startSize = self.getBounds().size();
+            startAspect = startSize.y / startSize.x;
+
+            if (typeof self.resizeOptions.start == "function")
+              self.resizeOptions.start.apply(self, [e]);
+
+            iQ(gWindow)
+              .mousemove(handleMouseMove)
+              .mouseup(handleMouseUp);
+
+            e.preventDefault();
+            e.stopPropagation();
+          });
+        }
+    } catch(e) {
+      Utils.log(e);
+    }
+  }
+};
+
+// ##########
+// Class: Items
+// Keeps track of all Items.
+window.Items = {
+  // ----------
+  // Variable: defaultGutter
+  // How far apart Items should be from each other and from bounds
+  defaultGutter: 15,
+
+  // ----------
+  // Function: item
+  // Given a DOM element representing an Item, returns the Item.
+  item: function(el) {
+    return iQ(el).data('item');
+  },
+
+  // ----------
+  // Function: getTopLevelItems
+  // Returns an array of all Items not grouped into groupItems.
+  getTopLevelItems: function() {
+    var items = [];
+
+    iQ('.tab, .groupItem, .info-item').each(function(elem) {
+      var $this = iQ(elem);
+      var item = $this.data('item');
+      if (item && !item.parent && !$this.hasClass('phantom'))
+        items.push(item);
+    });
+
+    return items;
+  },
+
+  // ----------
+  // Function: getPageBounds
+  // Returns a <Rect> defining the area of the page <Item>s should stay within.
+  getPageBounds: function() {
+    var width = Math.max(100, window.innerWidth);
+    var height = Math.max(100, window.innerHeight);
+    return new Rect(0, 0, width, height);
+  },
+
+  // ----------
+  // Function: getSafeWindowBounds
+  // Returns the bounds within which it is safe to place all non-stationary <Item>s.
+  getSafeWindowBounds: function() {
+    // the safe bounds that would keep it "in the window"
+    var gutter = Items.defaultGutter;
+    // Here, I've set the top gutter separately, as the top of the window has its own
+    // extra chrome which makes a large top gutter unnecessary.
+    // TODO: set top gutter separately, elsewhere.
+    var topGutter = 5;
+    return new Rect(gutter, topGutter,
+        window.innerWidth - 2 * gutter, window.innerHeight - gutter - topGutter);
+
+  },
+
+  // ----------
+  // Function: arrange
+  // Arranges the given items in a grid within the given bounds,
+  // maximizing item size but maintaining standard tab aspect ratio for each
+  //
+  // Parameters:
+  //   items - an array of <Item>s. Can be null if the pretend and count options are set.
+  //   bounds - a <Rect> defining the space to arrange within
+  //   options - an object with various properites (see below)
+  //
+  // Possible "options" properties:
+  //   animate - whether to animate; default: true.
+  //   z - the z index to set all the items; default: don't change z.
+  //   pretend - whether to collect and return the rectangle rather than moving the items; default: false
+  //   count - overrides the item count for layout purposes; default: the actual item count
+  //   padding - pixels between each item
+  //
+  // Returns:
+  //   the list of rectangles if the pretend option is set; otherwise null
+  arrange: function(items, bounds, options) {
+    var animate;
+    if (!options || typeof options.animate == 'undefined')
+      animate = true;
+    else
+      animate = options.animate;
+
+    if (typeof options == 'undefined')
+      options = {};
+
+    var rects = null;
+    if (options.pretend)
+      rects = [];
+
+    var tabAspect = TabItems.tabHeight / TabItems.tabWidth;
+    var count = options.count || (items ? items.length : 0);
+    if (!count)
+      return rects;
+
+    var columns = 1;
+    var padding = options.padding || 0;
+    var yScale = 1.1; // to allow for titles
+    var rows;
+    var tabWidth;
+    var tabHeight;
+    var totalHeight;
+
+    function figure() {
+      rows = Math.ceil(count / columns);
+      tabWidth = (bounds.width - (padding * (columns - 1))) / columns;
+      tabHeight = tabWidth * tabAspect;
+      totalHeight = (tabHeight * yScale * rows) + (padding * (rows - 1));
+    }
+
+    figure();
+
+    while (rows > 1 && totalHeight > bounds.height) {
+      columns++;
+      figure();
+    }
+
+    if (rows == 1) {
+      var maxWidth = Math.max(TabItems.tabWidth, bounds.width / 2);
+      tabWidth = Math.min(Math.min(maxWidth, bounds.width / count), bounds.height / tabAspect);
+      tabHeight = tabWidth * tabAspect;
+    }
+
+    var box = new Rect(bounds.left, bounds.top, tabWidth, tabHeight);
+    var row = 0;
+    var column = 0;
+    var immediately;
+
+    var a;
+    for (a = 0; a < count; a++) {
+      immediately = !animate;
+
+      if (rects)
+        rects.push(new Rect(box));
+      else if (items && a < items.length) {
+        var item = items[a];
+        if (!item.locked.bounds) {
+          item.setBounds(box, immediately);
+          item.setRotation(0);
+          if (options.z)
+            item.setZ(options.z);
+        }
+      }
+
+      box.left += box.width + padding;
+      column++;
+      if (column == columns) {
+        box.left = bounds.left;
+        box.top += (box.height * yScale) + padding;
+        column = 0;
+        row++;
+      }
+    }
+
+    return rects;
+  },
+
+  // ----------
+  // Function: unsquish
+  // Checks to see which items can now be unsquished.
+  //
+  // Parameters:
+  //   pairs - an array of objects, each with two properties: item and bounds. The bounds are
+  //     modified as appropriate, but the items are not changed. If pairs is null, the
+  //     operation is performed directly on all of the top level items.
+  //   ignore - an <Item> to not include in calculations (because it's about to be closed, for instance)
+  unsquish: function(pairs, ignore) {
+    var pairsProvided = (pairs ? true : false);
+    if (!pairsProvided) {
+      var items = Items.getTopLevelItems();
+      pairs = [];
+      items.forEach(function(item) {
+        pairs.push({
+          item: item,
+          bounds: item.getBounds()
+        });
+      });
+    }
+
+    var pageBounds = Items.getSafeWindowBounds();
+    pairs.forEach(function(pair) {
+      var item = pair.item;
+      if (item.locked.bounds || item == ignore)
+        return;
+
+      var bounds = pair.bounds;
+      var newBounds = new Rect(bounds);
+
+      var newSize;
+      if (Utils.isPoint(item.userSize))
+        newSize = new Point(item.userSize);
+      else
+        newSize = new Point(TabItems.tabWidth, TabItems.tabHeight);
+
+      if (item.isAGroupItem) {
+          newBounds.width = Math.max(newBounds.width, newSize.x);
+          newBounds.height = Math.max(newBounds.height, newSize.y);
+      } else {
+        if (bounds.width < newSize.x) {
+          newBounds.width = newSize.x;
+          newBounds.height = newSize.y;
+        }
+      }
+
+      newBounds.left -= (newBounds.width - bounds.width) / 2;
+      newBounds.top -= (newBounds.height - bounds.height) / 2;
+
+      var offset = new Point();
+      if (newBounds.left < pageBounds.left)
+        offset.x = pageBounds.left - newBounds.left;
+      else if (newBounds.right > pageBounds.right)
+        offset.x = pageBounds.right - newBounds.right;
+
+      if (newBounds.top < pageBounds.top)
+        offset.y = pageBounds.top - newBounds.top;
+      else if (newBounds.bottom > pageBounds.bottom)
+        offset.y = pageBounds.bottom - newBounds.bottom;
+
+      newBounds.offset(offset);
+
+      if (!bounds.equals(newBounds)) {
+        var blocked = false;
+        pairs.forEach(function(pair2) {
+          if (pair2 == pair || pair2.item == ignore)
+            return;
+
+          var bounds2 = pair2.bounds;
+          if (bounds2.intersects(newBounds))
+            blocked = true;
+					return;
+        });
+
+        if (!blocked) {
+          pair.bounds.copy(newBounds);
+        }
+      }
+      return;
+    });
+
+    if (!pairsProvided) {
+      pairs.forEach(function(pair) {
+        pair.item.setBounds(pair.bounds);
+      });
+    }
+  }
+};
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/modules/groups.jsm
@@ -0,0 +1,70 @@
+/* ***** 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 TabView Groups.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Edward Lee <edilee@mozilla.com>
+ * Ian Gilman <ian@iangilman.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let EXPORTED_SYMBOLS = ["Groups"];
+
+let Groups = let (T = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// Public
+  //////////////////////////////////////////////////////////////////////////////
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// Private
+  //////////////////////////////////////////////////////////////////////////////
+
+  init: function init() {
+    // Only allow calling init once
+    T.init = function() T;
+    
+    // load all groups data
+    // presumably we can load from app global, not a window
+    // how do we know which window has which group?
+    // load tab data to figure out which go into which group
+    // set up interface for subscribing to our data
+
+    return T;
+  }
+}) T.init();
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/modules/utils.jsm
@@ -0,0 +1,728 @@
+/* ***** 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 utils.js.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Aza Raskin <aza@mozilla.com>
+ * Ian Gilman <ian@iangilman.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ *
+ * This file incorporates work from:
+ * jQuery JavaScript Library v1.4.2: http://code.jquery.com/jquery-1.4.2.js
+ * This incorporated work is covered by the following copyright and
+ * permission notice:
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * 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 ***** */
+
+// **********
+// Title: utils.js
+
+let EXPORTED_SYMBOLS = ["Point", "Rect", "Range", "Subscribable", "Utils"];
+
+// #########
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// ##########
+// Class: Point
+// A simple point.
+//
+// Constructor: Point
+// If a is a Point, creates a copy of it. Otherwise, expects a to be x,
+// and creates a Point with it along with y. If either a or y are omitted,
+// 0 is used in their place.
+function Point(a, y) {
+  if (Utils.isPoint(a)) {
+    this.x = a.x;
+    this.y = a.y;
+  } else {
+    this.x = (Utils.isNumber(a) ? a : 0);
+    this.y = (Utils.isNumber(y) ? y : 0);
+  }
+};
+
+Point.prototype = {
+  // ----------
+  // Function: distance
+  // Returns the distance from this point to the given <Point>.
+  distance: function(point) {
+    var ax = this.x - point.x;
+    var ay = this.y - point.y;
+    return Math.sqrt((ax * ax) + (ay * ay));
+  }
+};
+
+// ##########
+// Class: Rect
+// A simple rectangle. Note that in addition to the left and width, it also has
+// a right property; changing one affects the others appropriately. Same for the
+// vertical properties.
+//
+// Constructor: Rect
+// If a is a Rect, creates a copy of it. Otherwise, expects a to be left,
+// and creates a Rect with it along with top, width, and height.
+function Rect(a, top, width, height) {
+  // Note: perhaps 'a' should really be called 'rectOrLeft'
+  if (Utils.isRect(a)) {
+    this.left = a.left;
+    this.top = a.top;
+    this.width = a.width;
+    this.height = a.height;
+  } else {
+    this.left = a;
+    this.top = top;
+    this.width = width;
+    this.height = height;
+  }
+};
+
+Rect.prototype = {
+
+  get right() this.left + this.width,
+  set right(value) {
+    this.width = value - this.left;
+  },
+
+  get bottom() this.top + this.height,
+  set bottom(value) {
+    this.height = value - this.top;
+  },
+
+  // ----------
+  // Variable: xRange
+  // Gives you a new <Range> for the horizontal dimension.
+  get xRange() new Range(this.left, this.right),
+
+  // ----------
+  // Variable: yRange
+  // Gives you a new <Range> for the vertical dimension.
+  get yRange() new Range(this.top, this.bottom),
+
+  // ----------
+  // Function: intersects
+  // Returns true if this rectangle intersects the given <Rect>.
+  intersects: function(rect) {
+    return (rect.right > this.left &&
+            rect.left < this.right &&
+            rect.bottom > this.top &&
+            rect.top < this.bottom);
+  },
+
+  // ----------
+  // Function: intersection
+  // Returns a new <Rect> with the intersection of this rectangle and the give <Rect>,
+  // or null if they don't intersect.
+  intersection: function(rect) {
+    var box = new Rect(Math.max(rect.left, this.left), Math.max(rect.top, this.top), 0, 0);
+    box.right = Math.min(rect.right, this.right);
+    box.bottom = Math.min(rect.bottom, this.bottom);
+    if (box.width > 0 && box.height > 0)
+      return box;
+
+    return null;
+  },
+
+  // ----------
+  // Function: contains
+  // Returns a boolean denoting if the <Rect> is contained inside
+  // of the bounding rect.
+  //
+  // Paramaters
+  //  - A <Rect>
+  contains: function(rect) {
+    return (rect.left > this.left &&
+            rect.right < this.right &&
+            rect.top > this.top &&
+            rect.bottom < this.bottom);
+  },
+
+  // ----------
+  // Function: center
+  // Returns a new <Point> with the center location of this rectangle.
+  center: function() {
+    return new Point(this.left + (this.width / 2), this.top + (this.height / 2));
+  },
+
+  // ----------
+  // Function: size
+  // Returns a new <Point> with the dimensions of this rectangle.
+  size: function() {
+    return new Point(this.width, this.height);
+  },
+
+  // ----------
+  // Function: position
+  // Returns a new <Point> with the top left of this rectangle.
+  position: function() {
+    return new Point(this.left, this.top);
+  },
+
+  // ----------
+  // Function: area
+  // Returns the area of this rectangle.
+  area: function() {
+    return this.width * this.height;
+  },
+
+  // ----------
+  // Function: inset
+  // Makes the rect smaller (if the arguments are positive) as if a margin is added all around
+  // the initial rect, with the margin widths (symmetric) being specified by the arguments.
+  //
+  // Paramaters
+  //  - A <Point> or two arguments: x and y
+  inset: function(a, b) {
+    if (Utils.isPoint(a)) {
+      b = a.y;
+      a = a.x;
+    }
+
+    this.left += a;
+    this.width -= a * 2;
+    this.top += b;
+    this.height -= b * 2;
+  },
+
+  // ----------
+  // Function: offset
+  // Moves (translates) the rect by the given vector.
+  //
+  // Paramaters
+  //  - A <Point> or two arguments: x and y
+  offset: function(a, b) {
+    if (Utils.isPoint(a)) {
+      this.left += a.x;
+      this.top += a.y;
+    } else {
+      this.left += a;
+      this.top += b;
+    }
+  },
+
+  // ----------
+  // Function: equals
+  // Returns true if this rectangle is identical to the given <Rect>.
+  equals: function(rect) {
+    return (rect.left == this.left &&
+            rect.top == this.top &&
+            rect.width == this.width &&
+            rect.height == this.height);
+  },
+
+  // ----------
+  // Function: union
+  // Returns a new <Rect> with the union of this rectangle and the given <Rect>.
+  union: function(a) {
+    var newLeft = Math.min(a.left, this.left);
+    var newTop = Math.min(a.top, this.top);
+    var newWidth = Math.max(a.right, this.right) - newLeft;
+    var newHeight = Math.max(a.bottom, this.bottom) - newTop;
+    var newRect = new Rect(newLeft, newTop, newWidth, newHeight);
+
+    return newRect;
+  },
+
+  // ----------
+  // Function: copy
+  // Copies the values of the given <Rect> into this rectangle.
+  copy: function(a) {
+    this.left = a.left;
+    this.top = a.top;
+    this.width = a.width;
+    this.height = a.height;
+  },
+
+  // ----------
+  // Function: css
+  // Returns an object with the dimensions of this rectangle, suitable for
+  // passing into iQ's css method. You could of course just pass the rectangle
+  // straight in, but this is cleaner, as it removes all the extraneous
+  // properties. If you give a <Rect> to <iQClass.css> without this, it will
+  // ignore the extraneous properties, but result in CSS warnings.
+  css: function() {
+    return {
+      left: this.left,
+      top: this.top,
+      width: this.width,
+      height: this.height
+    };
+  }
+};
+
+// ##########
+// Class: Range
+// A physical interval, with a min and max.
+//
+// Constructor: Range
+// Creates a Range with the given min and max
+function Range(min, max) {
+  if (Utils.isRange(min) && !max) { // if the one variable given is a range, copy it.
+    this.min = min.min;
+    this.max = min.max;
+  } else {
+    this.min = min || 0;
+    this.max = max || 0;
+  }
+};
+
+Range.prototype = {
+  // Variable: extent
+  // Equivalent to max-min
+  get extent() {
+    return (this.max - this.min);
+  },
+
+  set extent(extent) {
+    this.max = extent - this.min;
+  },
+
+  // ----------
+  // Function: contains
+  // Whether the <Range> contains the given <Range> or value or not.
+  //
+  // Paramaters
+  //  - a number or <Range>
+  contains: function(value) {
+    if (Utils.isNumber(value))
+      return value >= this.min && value <= this.max;
+    if (Utils.isRange(value))
+      return value.min >= this.min && value.max <= this.max;
+    return false;
+  },
+
+  // ----------
+  // Function: proportion
+  // Maps the given value to the range [0,1], so that it returns 0 if the value is <= the min,
+  // returns 1 if the value >= the max, and returns an interpolated "proportion" in (min, max).
+  //
+  // Paramaters
+  //  - a number
+  //  - (bool) smooth? If true, a smooth tanh-based function will be used instead of the linear.
+  proportion: function(value, smooth) {
+    if (value <= this.min)
+      return 0;
+    if (this.max <= value)
+      return 1;
+
+    var proportion = (value - this.min) / this.extent;
+
+    if (smooth) {
+      // The ease function ".5+.5*Math.tanh(4*x-2)" is a pretty
+      // little graph. It goes from near 0 at x=0 to near 1 at x=1
+      // smoothly and beautifully.
+      // http://www.wolframalpha.com/input/?i=.5+%2B+.5+*+tanh%28%284+*+x%29+-+2%29
+      function tanh(x) {
+        var e = Math.exp(x);
+        return (e - 1/e) / (e + 1/e);
+      }
+      return .5 - .5 * tanh(2 - 4 * proportion);
+    }
+
+    return proportion;
+  },
+
+  // ----------
+  // Function: scale
+  // Takes the given value in [0,1] and maps it to the associated value on the Range.
+  //
+  // Paramaters
+  //  - a number in [0,1]
+  scale: function(value) {
+    if (value > 1)
+      value = 1;
+    if (value < 0)
+      value = 0;
+    return this.min + this.extent * value;
+  }
+};
+
+// ##########
+// Class: Subscribable
+// A mix-in for allowing objects to collect subscribers for custom events.
+function Subscribable() {
+  this.subscribers = null;
+};
+
+Subscribable.prototype = {
+  // ----------
+  // Function: addSubscriber
+  // The given callback will be called when the Subscribable fires the given event.
+  // The refObject is used to facilitate removal if necessary.
+  addSubscriber: function(refObject, eventName, callback) {
+    try {
+      Utils.assertThrow(refObject, "refObject");
+      Utils.assertThrow(typeof callback == "function", "callback must be a function");
+      Utils.assertThrow(eventName && typeof eventName == "string",
+          "eventName must be a non-empty string");
+    } catch(e) {
+      Utils.log(e);
+      return;
+    }
+
+    if (!this.subscribers)
+      this.subscribers = {};
+
+    if (!this.subscribers[eventName])
+      this.subscribers[eventName] = [];
+
+    var subs = this.subscribers[eventName];
+    var existing = subs.filter(function(element) {
+      return element.refObject == refObject;
+    });
+
+    if (existing.length) {
+      Utils.assert(existing.length == 1, 'should only ever be one');
+      existing[0].callback = callback;
+    } else {
+      subs.push({
+        refObject: refObject,
+        callback: callback
+      });
+    }
+  },
+
+  // ----------
+  // Function: removeSubscriber
+  // Removes the callback associated with refObject for the given event.
+  removeSubscriber: function(refObject, eventName) {
+    try {
+      Utils.assertThrow(refObject, "refObject");
+      Utils.assertThrow(eventName && typeof eventName == "string",
+          "eventName must be a non-empty string");
+    } catch(e) {
+      Utils.log(e);
+      return;
+    }
+
+    if (!this.subscribers || !this.subscribers[eventName])
+      return;
+
+    this.subscribers[eventName] = this.subscribers[eventName].filter(function(element) {
+      return element.refObject != refObject;
+    });
+  },
+
+  // ----------
+  // Function: _sendToSubscribers
+  // Internal routine. Used by the Subscribable to fire events.
+  _sendToSubscribers: function(eventName, eventInfo) {
+    try {
+      Utils.assertThrow(eventName && typeof eventName == "string",
+          "eventName must be a non-empty string");
+    } catch(e) {
+      Utils.log(e);
+      return;
+    }
+
+    if (!this.subscribers || !this.subscribers[eventName])
+      return;
+
+    var subsCopy = this.subscribers[eventName].concat();
+    subsCopy.forEach(function(object) {
+      try {
+        object.callback(this, eventInfo);
+      } catch(e) {
+        Utils.log(e);
+      }
+    }, this);
+  }
+};
+
+// ##########
+// Class: Utils
+// Singelton with common utility functions.
+let Utils = {
+  // ___ Logging
+
+  // ----------
+  // Function: log
+  // Prints the given arguments to the JavaScript error console as a message.
+  // Pass as many arguments as you want, it'll print them all.
+  log: function() {
+    var text = this.expandArgumentsForLog(arguments);
+    Services.console.logStringMessage(text);
+  },
+
+  // ----------
+  // Function: error
+  // Prints the given arguments to the JavaScript error console as an error.
+  // Pass as many arguments as you want, it'll print them all.
+  error: function() {
+    var text = this.expandArgumentsForLog(arguments);
+    Cu.reportError("tabview error: " + text);
+  },
+
+  // ----------
+  // Function: trace
+  // Prints the given arguments to the JavaScript error console as a message,
+  // along with a full stack trace.
+  // Pass as many arguments as you want, it'll print them all.
+  trace: function() {
+    var text = this.expandArgumentsForLog(arguments);
+    // cut off the first two lines of the stack trace, because they're just this function.
+    let stack = Error().stack.replace(/^.*?\n.*?\n/, "");
+    // if the caller was assert, cut out the line for the assert function as well.
+    if (this.trace.caller.name == 'Utils_assert')
+      stack = stack.replace(/^.*?\n/, "");
+    this.log('trace: ' + text + '\n' + stack);
+  },
+
+  // ----------
+  // Function: assert
+  // Prints a stack trace along with label (as a console message) if condition is false.
+  assert: function Utils_assert(condition, label) {
+    if (!condition) {
+      let text;
+      if (typeof label != 'string')
+        text = 'badly formed assert';
+      else
+        text = "tabview assert: " + label;
+
+      this.trace(text);
+    }
+  },
+
+  // ----------
+  // Function: assertThrow
+  // Throws label as an exception if condition is false.
+  assertThrow: function(condition, label) {
+    if (!condition) {
+      let text;
+      if (typeof label != 'string')
+        text = 'badly formed assert';
+      else
+        text = "tabview assert: " + label;
+
+      // cut off the first two lines of the stack trace, because they're just this function.
+      text += Error().stack.replace(/^.*?\n.*?\n/, "");
+
+      throw text;
+    }
+  },
+
+  // ----------
+  // Function: expandObject
+  // Prints the given object to a string, including all of its properties.
+  expandObject: function(obj) {
+    var s = obj + ' = {';
+    for (let prop in obj) {
+      let value;
+      try {
+        value = obj[prop];
+      } catch(e) {
+        value = '[!!error retrieving property]';
+      }
+
+      s += prop + ': ';
+      if (typeof value == 'string')
+        s += '\'' + value + '\'';
+      else if (typeof value == 'function')
+        s += 'function';
+      else
+        s += value;
+
+      s += ', ';
+    }
+    return s + '}';
+  },
+
+  // ----------
+  // Function: expandArgumentsForLog
+  // Expands all of the given args (an array) into a single string.
+  expandArgumentsForLog: function(args) {
+    var that = this;
+    return Array.map(args, function(arg) {
+      return typeof arg == 'object' ? that.expandObject(arg) : arg;
+    }).join('; ');
+  },
+
+  // ___ Misc
+
+  // ----------
+  // Function: isRightClick
+  // Given a DOM mouse event, returns true if it was for the right mouse button.
+  isRightClick: function(event) {
+    return event.button == 2;
+  },
+
+  // ----------
+  // Function: isDOMElement
+  // Returns true if the given object is a DOM element.
+  isDOMElement: function(object) {
+    return object instanceof Ci.nsIDOMElement;
+  },
+
+  // ----------
+  // Function: isNumber
+  // Returns true if the argument is a valid number.
+  isNumber: function(n) {
+    return typeof n == 'number' && !isNaN(n);
+  },
+
+  // ----------
+  // Function: isRect
+  // Returns true if the given object (r) looks like a <Rect>.
+  isRect: function(r) {
+    return (r &&
+            this.isNumber(r.left) &&
+            this.isNumber(r.top) &&
+            this.isNumber(r.width) &&
+            this.isNumber(r.height));
+  },
+
+  // ----------
+  // Function: isRange
+  // Returns true if the given object (r) looks like a <Range>.
+  isRange: function(r) {
+    return (r &&
+            this.isNumber(r.min) &&
+            this.isNumber(r.max));
+  },
+
+  // ----------
+  // Function: isPoint
+  // Returns true if the given object (p) looks like a <Point>.
+  isPoint: function(p) {
+    return (p && this.isNumber(p.x) && this.isNumber(p.y));
+  },
+
+  // ----------
+  // Function: isPlainObject
+  // Check to see if an object is a plain object (created using "{}" or "new Object").
+  isPlainObject: function(obj) {
+    // Must be an Object.
+    // Make sure that DOM nodes and window objects don't pass through, as well
+    if (!obj || Object.prototype.toString.call(obj) !== "[object Object]" ||
+       obj.nodeType || obj.setInterval) {
+      return false;
+    }
+
+    // Not own constructor property must be Object
+    const hasOwnProperty = Object.prototype.hasOwnProperty;
+
+    if (obj.constructor &&
+       !hasOwnProperty.call(obj, "constructor") &&
+       !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
+      return false;
+    }
+
+    // Own properties are enumerated firstly, so to speed up,
+    // if last one is own, then all properties are own.
+
+    var key;
+    for (key in obj) {}
+
+    return key === undefined || hasOwnProperty.call(obj, key);
+  },
+
+  // ----------
+  // Function: isEmptyObject
+  // Returns true if the given object has no members.
+  isEmptyObject: function(obj) {
+    for (let name in obj)
+      return false;
+    return true;
+  },
+
+  // ----------
+  // Function: copy
+  // Returns a copy of the argument. Note that this is a shallow copy; if the argument
+  // has properties that are themselves objects, those properties will be copied by reference.
+  copy: function(value) {
+    if (value && typeof value == 'object') {
+      if (Array.isArray(value))
+        return this.extend([], value);
+      return this.extend({}, value);
+    }
+    return value;
+  },
+
+  // ----------
+  // Function: merge
+  // Merge two array-like objects into the first and return it.
+  merge: function(first, second) {
+    Array.forEach(second, function(el) Array.push(first, el));
+    return first;
+  },
+
+  // ----------
+  // Function: extend
+  // Pass several objects in and it will combine them all into the first object and return it.
+  extend: function() {
+
+    // copy reference to target object
+    let target = arguments[0] || {};
+    // Deep copy is not supported
+    if (typeof target === "boolean") {
+      this.assert(false, "The first argument of extend cannot be a boolean." +
+          "Deep copy is not supported.");
+      return target;
+    }
+
+    // Back when this was in iQ + iQ.fn, so you could extend iQ objects with it.
+    // This is no longer supported.
+    let length = arguments.length;
+    if (length === 1) {
+      this.assert(false, "Extending the iQ prototype using extend is not supported.");
+      return target;
+    }
+
+    // Handle case when target is a string or something
+    if (typeof target != "object" && typeof target != "function") {
+      target = {};
+    }
+
+    for (let i = 1; i < length; i++) {
+      // Only deal with non-null/undefined values
+      let options = arguments[i];
+      if (options != null) {
+        // Extend the base object
+        for (let name in options) {
+          let copy = options[name];
+
+          // Prevent never-ending loop
+          if (target === copy)
+            continue;
+
+          if (copy !== undefined)
+            target[name] = copy;
+        }
+      }
+    }
+
+    // Return the modified object
+    return target;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/storage.js
@@ -0,0 +1,213 @@
+/* ***** 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 storage.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ehsan Akhgari <ehsan@mozilla.com>
+ * Ian Gilman <ian@iangilman.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: storage.js
+
+// ##########
+// Class: Storage
+// Singleton for permanent storage of TabView data.
+let Storage = {
+  GROUP_DATA_IDENTIFIER: "tabview-group",
+  GROUPS_DATA_IDENTIFIER: "tabview-groups",
+  TAB_DATA_IDENTIFIER: "tabview-tab",
+  UI_DATA_IDENTIFIER: "tabview-ui",
+
+  // ----------
+  // Function: init
+  // Sets up the object.
+  init: function() {
+    this._sessionStore =
+      Cc["@mozilla.org/browser/sessionstore;1"].
+        getService(Ci.nsISessionStore);
+  },
+
+  // ----------
+  // Function: uninit
+  uninit : function() {
+    this._sessionStore = null;
+  },
+
+  // ----------
+  // Function: wipe
+  // Cleans out all the stored data, leaving empty objects.
+  wipe: function() {
+    try {
+      var self = this;
+
+      // ___ Tabs
+      AllTabs.tabs.forEach(function(tab) {
+        if (tab.ownerDocument.defaultView != gWindow)
+          return;
+
+        self.saveTab(tab, null);
+      });
+
+      // ___ Other
+      this.saveGroupItemsData(gWindow, {});
+      this.saveUIData(gWindow, {});
+
+      this._sessionStore.setWindowValue(gWindow, this.GROUP_DATA_IDENTIFIER,
+        JSON.stringify({}));
+    } catch (e) {
+      Utils.log("Error in wipe: "+e);
+    }
+  },
+
+  // ----------
+  // Function: saveTab
+  // Saves the data for a single tab.
+  saveTab: function(tab, data) {
+    Utils.assert(tab, "tab");
+
+    this._sessionStore.setTabValue(tab, this.TAB_DATA_IDENTIFIER,
+      JSON.stringify(data));
+  },
+
+  // ----------
+  // Function: getTabData
+  // Returns the data object associated with a single tab.
+  getTabData: function(tab) {
+    Utils.assert(tab, "tab");
+
+    var existingData = null;
+    try {
+      var tabData = this._sessionStore.getTabValue(tab, this.TAB_DATA_IDENTIFIER);
+      if (tabData != "") {
+        existingData = JSON.parse(tabData);
+      }
+    } catch (e) {
+      // getWindowValue will fail if the property doesn't exist
+      Utils.log(e);
+    }
+
+    return existingData;
+  },
+
+  // ----------
+  // Function: saveGroupItem
+  // Saves the data for a single groupItem, associated with a specific window.
+  saveGroupItem: function(win, data) {
+    var id = data.id;
+    var existingData = this.readGroupItemData(win);
+    existingData[id] = data;
+    this._sessionStore.setWindowValue(win, this.GROUP_DATA_IDENTIFIER,
+      JSON.stringify(existingData));
+  },
+
+  // ----------
+  // Function: deleteGroupItem
+  // Deletes the data for a single groupItem from the given window.
+  deleteGroupItem: function(win, id) {
+    var existingData = this.readGroupItemData(win);
+    delete existingData[id];
+    this._sessionStore.setWindowValue(win, this.GROUP_DATA_IDENTIFIER,
+      JSON.stringify(existingData));
+  },
+
+  // ----------
+  // Function: readGroupItemData
+  // Returns the data for all groupItems associated with the given window.
+  readGroupItemData: function(win) {
+    var existingData = {};
+    try {
+      existingData = JSON.parse(
+        this._sessionStore.getWindowValue(win, this.GROUP_DATA_IDENTIFIER)
+      );
+    } catch (e) {
+      // getWindowValue will fail if the property doesn't exist
+      Utils.log("Error in readGroupItemData: "+e);
+    }
+    return existingData;
+  },
+
+  // ----------
+  // Function: saveGroupItemsData
+  // Saves the global data for the <GroupItems> singleton for the given window.
+  saveGroupItemsData: function(win, data) {
+    this.saveData(win, this.GROUPS_DATA_IDENTIFIER, data);
+  },
+
+  // ----------
+  // Function: readGroupItemsData
+  // Reads the global data for the <GroupItems> singleton for the given window.
+  readGroupItemsData: function(win) {
+    return this.readData(win, this.GROUPS_DATA_IDENTIFIER);
+  },
+
+  // ----------
+  // Function: saveUIData
+  // Saves the global data for the <UIManager> singleton for the given window.
+  saveUIData: function(win, data) {
+    this.saveData(win, this.UI_DATA_IDENTIFIER, data);
+  },
+
+  // ----------
+  // Function: readUIData
+  // Reads the global data for the <UIManager> singleton for the given window.
+  readUIData: function(win) {
+    return this.readData(win, this.UI_DATA_IDENTIFIER);
+  },
+
+  // ----------
+  // Function: saveData
+  // Generic routine for saving data to a window.
+  saveData: function(win, id, data) {
+    try {
+      this._sessionStore.setWindowValue(win, id, JSON.stringify(data));
+    } catch (e) {
+      Utils.log("Error in saveData: "+e);
+    }
+  },
+
+  // ----------
+  // Function: readData
+  // Generic routine for reading data from a window.
+  readData: function(win, id) {
+    var existingData = {};
+    try {
+      var data = this._sessionStore.getWindowValue(win, id);
+      if (data)
+        existingData = JSON.parse(data);
+    } catch (e) {
+      Utils.log("Error in readData: "+e);
+    }
+
+    return existingData;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/tabitems.js
@@ -0,0 +1,1104 @@
+/* ***** 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 tabitems.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ * Ehsan Akhgari <ehsan@mozilla.com>
+ * Raymond Lee <raymond@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: tabitems.js
+
+// ##########
+// Class: TabItem
+// An <Item> that represents a tab. Also implements the <Subscribable> interface.
+//
+// Parameters:
+//   tab - a xul:tab
+window.TabItem = function(tab) {
+
+  Utils.assert(tab, "tab");
+
+  this.tab = tab;
+  // register this as the tab's tabItem
+  this.tab.tabItem = this;
+
+  // ___ set up div
+  var $div = iQ('<div>')
+    .addClass('tab')
+    .html("<div class='thumb'><div class='thumb-shadow'></div>" +
+          "<img class='cached-thumb' style='display:none'/><canvas/></div>" +
+          "<div class='favicon'><img/></div>" +
+          "<span class='tab-title'>&nbsp;</span>"
+    )
+    .appendTo('body');
+
+  this.canvasSizeForced = false;
+  this.isShowingCachedData = false;
+  this.favEl = (iQ('.favicon>img', $div))[0];
+  this.nameEl = (iQ('.tab-title', $div))[0];
+  this.canvasEl = (iQ('.thumb canvas', $div))[0];
+  this.cachedThumbEl = (iQ('img.cached-thumb', $div))[0];
+
+  this.tabCanvas = new TabCanvas(this.tab, this.canvasEl);
+
+  this.defaultSize = new Point(TabItems.tabWidth, TabItems.tabHeight);
+  this.locked = {};
+  this.isATabItem = true;
+  this._zoomPrep = false;
+  this.sizeExtra = new Point();
+  this.keepProportional = true;
+
+  var self = this;
+
+  this.isDragging = false;
+
+  this.sizeExtra.x = parseInt($div.css('padding-left'))
+      + parseInt($div.css('padding-right'));
+
+  this.sizeExtra.y = parseInt($div.css('padding-top'))
+      + parseInt($div.css('padding-bottom'));
+
+  this.bounds = $div.bounds();
+
+  // ___ superclass setup
+  this._init($div[0]);
+
+  // ___ drag/drop
+  // override dropOptions with custom tabitem methods
+  // This is mostly to support the phantom groupItems.
+  this.dropOptions.drop = function(e) {
+    var $target = iQ(this.container);
+    this.isDropTarget = false;
+
+    var phantom = $target.data("phantomGroupItem");
+
+    var groupItem = drag.info.item.parent;
+    if (groupItem) {
+      groupItem.add(drag.info.$el);
+    } else {
+      phantom.removeClass("phantom acceptsDrop");
+      new GroupItem([$target, drag.info.$el], {container:phantom, bounds:phantom.bounds()});
+    }
+  };
+
+  this.dropOptions.over = function(e) {
+    var $target = iQ(this.container);
+    this.isDropTarget = true;
+
+    $target.removeClass("acceptsDrop");
+
+    var phantomMargin = 40;
+
+    var groupItemBounds = this.getBoundsWithTitle();
+    groupItemBounds.inset(-phantomMargin, -phantomMargin);
+
+    iQ(".phantom").remove();
+    var phantom = iQ("<div>")
+      .addClass("groupItem phantom acceptsDrop")
+      .css({
+        position: "absolute",
+        zIndex: -99
+      })
+      .css(groupItemBounds.css())
+      .hide()
+      .appendTo("body");
+
+    var defaultRadius = Trenches.defaultRadius;
+    // Extend the margin so that it covers the case where the target tab item
+    // is right next to a trench.
+    Trenches.defaultRadius = phantomMargin + 1;
+    var updatedBounds = drag.info.snapBounds(groupItemBounds,'none');
+    Trenches.defaultRadius = defaultRadius;
+
+    // Utils.log('updatedBounds:',updatedBounds);
+    if (updatedBounds)
+      phantom.css(updatedBounds.css());
+
+    phantom.fadeIn();
+
+    $target.data("phantomGroupItem", phantom);
+  };
+
+  this.dropOptions.out = function(e) {
+    this.isDropTarget = false;
+    var phantom = iQ(this.container).data("phantomGroupItem");
+    if (phantom) {
+      phantom.fadeOut(function() {
+        iQ(this).remove();
+      });
+    }
+  };
+
+  this.draggable();
+  this.droppable(true);
+
+  // ___ more div setup
+  $div.mousedown(function(e) {
+    if (!Utils.isRightClick(e))
+      self.lastMouseDownTarget = e.target;
+  });
+
+  $div.mouseup(function(e) {
+    var same = (e.target == self.lastMouseDownTarget);
+    self.lastMouseDownTarget = null;
+    if (!same)
+      return;
+
+    if (iQ(e.target).hasClass("close"))
+      self.close();
+    else {
+      if (!Items.item(this).isDragging)
+        self.zoomIn();
+    }
+  });
+
+  iQ("<div>")
+    .addClass('close')
+    .appendTo($div);
+
+  iQ("<div>")
+    .addClass('expander')
+    .appendTo($div);
+
+  // ___ additional setup
+  this.reconnected = false;
+  this._hasBeenDrawn = false;
+  this.setResizable(true);
+
+  this._updateDebugBounds();
+
+  TabItems.register(this);
+
+  if (!TabItems.reconnect(this))
+    GroupItems.newTab(this);
+};
+
+window.TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
+  // ----------
+  // Function: forceCanvasSize
+  // Repaints the thumbnail with the given resolution, and forces it
+  // to stay that resolution until unforceCanvasSize is called.
+  forceCanvasSize: function(w, h) {
+    this.canvasSizeForced = true;
+    this.canvasEl.width = w;
+    this.canvasEl.height = h;
+    this.tabCanvas.paint();
+  },
+
+  // ----------
+  // Function: unforceCanvasSize
+  // Stops holding the thumbnail resolution; allows it to shift to the
+  // size of thumbnail on screen. Note that this call does not nest, unlike
+  // <TabItems.resumePainting>; if you call forceCanvasSize multiple
+  // times, you just need a single unforce to clear them all.
+  unforceCanvasSize: function() {
+    this.canvasSizeForced = false;
+  },
+
+  // ----------
+  // Function: showCachedData
+  // Shows the cached data i.e. image and title.  Note: this method should only
+  // be called at browser startup with the cached data avaliable.
+  showCachedData: function(tabData) {
+    this.isShowingCachedData = true;
+    var $nameElement = iQ(this.nameEl);
+    var $canvasElement = iQ(this.canvasEl);
+    var $cachedThumbElement = iQ(this.cachedThumbEl);
+    $cachedThumbElement.attr("src", tabData.imageData).show();
+    $canvasElement.css({opacity: 0.0});
+    $nameElement.text(tabData.title ? tabData.title : "");
+  },
+
+  // ----------
+  // Function: hideCachedData
+  // Hides the cached data i.e. image and title and show the canvas.
+  hideCachedData: function() {
+    var $canvasElement = iQ(this.canvasEl);
+    var $cachedThumbElement = iQ(this.cachedThumbEl);
+    $cachedThumbElement.hide();
+    $canvasElement.css({opacity: 1.0});
+    this.isShowingCachedData = false;
+  },
+
+  // ----------
+  // Function: getStorageData
+  // Get data to be used for persistent storage of this object.
+  //
+  // Parameters:
+  //   getImageData - true to include thumbnail pixels (and page title as well); default false
+  getStorageData: function(getImageData) {
+    return {
+      bounds: this.getBounds(),
+      userSize: (Utils.isPoint(this.userSize) ? new Point(this.userSize) : null),
+      url: this.tab.linkedBrowser.currentURI.spec,
+      groupID: (this.parent ? this.parent.id : 0),
+      imageData: (getImageData && this.tabCanvas ?
+                  this.tabCanvas.toImageData() : null),
+      title: getImageData && this.tab.label || null
+    };
+  },
+
+  // ----------
+  // Function: save
+  // Store persistent for this object.
+  //
+  // Parameters:
+  //   saveImageData - true to include thumbnail pixels (and page title as well); default false
+  save: function(saveImageData) {
+    try{
+      if (!this.tab || this.tab.parentNode == null || !this.reconnected) // too soon/late to save
+        return;
+
+      var data = this.getStorageData(saveImageData);
+      if (TabItems.storageSanity(data))
+        Storage.saveTab(this.tab, data);
+    } catch(e) {
+      Utils.log("Error in saving tab value: "+e);
+    }
+  },
+
+  // ----------
+  // Function: setBounds
+  // Moves this item to the specified location and size.
+  //
+  // Parameters:
+  //   rect - a <Rect> giving the new bounds
+  //   immediately - true if it should not animate; default false
+  //   options - an object with additional parameters, see below
+  //
+  // Possible options:
+  //   force - true to always update the DOM even if the bounds haven't changed; default false
+  setBounds: function(rect, immediately, options) {
+    if (!Utils.isRect(rect)) {
+      Utils.trace('TabItem.setBounds: rect is not a real rectangle!', rect);
+      return;
+    }
+
+    if (!options)
+      options = {};
+
+    if (this._zoomPrep)
+      this.bounds.copy(rect);
+    else {
+      var $container = iQ(this.container);
+      var $title = iQ('.tab-title', $container);
+      var $thumb = iQ('.thumb', $container);
+      var $close = iQ('.close', $container);
+      var $fav   = iQ('.favicon', $container);
+      var css = {};
+
+      const fontSizeRange = new Range(8,15);
+
+      if (rect.left != this.bounds.left || options.force)
+        css.left = rect.left;
+
+      if (rect.top != this.bounds.top || options.force)
+        css.top = rect.top;
+
+      if (rect.width != this.bounds.width || options.force) {
+        css.width = rect.width - this.sizeExtra.x;
+        let widthRange = new Range(0,TabItems.tabWidth);
+        let proportion = widthRange.proportion(css.width, true); // in [0,1]
+
+        css.fontSize = fontSizeRange.scale(proportion); // returns a value in the fontSizeRange
+        css.fontSize += 'px';
+      }
+
+      if (rect.height != this.bounds.height || options.force)
+        css.height = rect.height - this.sizeExtra.y;
+
+      if (Utils.isEmptyObject(css))
+        return;
+
+      this.bounds.copy(rect);
+
+      // If this is a brand new tab don't animate it in from
+      // a random location (i.e., from [0,0]). Instead, just
+      // have it appear where it should be.
+      if (immediately || (!this._hasBeenDrawn)) {
+        $container.css(css);
+      } else {
+        TabItems.pausePainting();
+        $container.animate(css, {
+          duration: 200,
+          easing: "tabviewBounce",
+          complete: function() {
+            TabItems.resumePainting();
+          }
+        });
+      }
+
+      if (css.fontSize && !this.inStack()) {
+        if (css.fontSize < fontSizeRange.min)
+          $title.fadeOut();
+        else
+          $title.fadeIn();
+      }
+
+      if (css.width) {
+        TabItems.update(this.tab);
+
+        let widthRange, proportion;
+
+        if (this.inStack()) {
+          $fav.css({top:0, left:0});
+          widthRange = new Range(70, 90);
+          proportion = widthRange.proportion(css.width); // between 0 and 1
+        } else {
+          $fav.css({top:4,left:4});
+          widthRange = new Range(60, 70);
+          proportion = widthRange.proportion(css.width); // between 0 and 1
+          $close.show().css({opacity:proportion});
+          if (proportion <= .1)
+            $close.hide()
+        }
+
+        var pad = 1 + 5 * proportion;
+        var alphaRange = new Range(0.1,0.2);
+        $fav.css({
+         "padding-left": pad + "px",
+         "padding-right": pad + 2 + "px",
+         "padding-top": pad + "px",
+         "padding-bottom": pad + "px",
+         "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")",
+        });
+      }
+
+      this._hasBeenDrawn = true;
+    }
+
+    this._updateDebugBounds();
+    rect = this.getBounds(); // ensure that it's a <Rect>
+
+    if (!Utils.isRect(this.bounds))
+      Utils.trace('TabItem.setBounds: this.bounds is not a real rectangle!', this.bounds);
+
+    if (!this.parent && this.tab.parentNode != null)
+      this.setTrenches(rect);
+
+    this.save();
+  },
+
+  // ----------
+  // Function: getBoundsWithTitle
+  // Returns a <Rect> for the groupItem's bounds, including the title
+  getBoundsWithTitle: function() {
+    var b = this.getBounds();
+    var $title = iQ(this.container).find('.tab-title');
+    var height = b.height;
+    if ( Utils.isNumber($title.height()) )
+      height += $title.height();
+    return new Rect(b.left, b.top, b.width, height);
+  },
+
+  // ----------
+  // Function: inStack
+  // Returns true if this item is in a stacked groupItem.
+  inStack: function() {
+    return iQ(this.container).hasClass("stacked");
+  },
+
+  // ----------
+  // Function: setZ
+  // Sets the z-index for this item.
+  setZ: function(value) {
+    this.zIndex = value;
+    iQ(this.container).css({zIndex: value});
+  },
+
+  // ----------
+  // Function: close
+  // Closes this item (actually closes the tab associated with it, which automatically
+  // closes the item.
+  close: function() {
+    gBrowser.removeTab(this.tab);
+    this._sendToSubscribers("tabRemoved");
+
+    // No need to explicitly delete the tab data, becasue sessionstore data
+    // associated with the tab will automatically go away
+  },
+
+  // ----------
+  // Function: addClass
+  // Adds the specified CSS class to this item's container DOM element.
+  addClass: function(className) {
+    iQ(this.container).addClass(className);
+  },
+
+  // ----------
+  // Function: removeClass
+  // Removes the specified CSS class from this item's container DOM element.
+  removeClass: function(className) {
+    iQ(this.container).removeClass(className);
+  },
+
+  // ----------
+  // Function: setResizable
+  // If value is true, makes this item resizable, otherwise non-resizable.
+  // Shows/hides a visible resize handle as appropriate.
+  setResizable: function(value) {
+    var $resizer = iQ('.expander', this.container);
+
+    this.resizeOptions.minWidth = TabItems.minTabWidth;
+    this.resizeOptions.minHeight = TabItems.minTabWidth * (TabItems.tabHeight / TabItems.tabWidth);
+
+    if (value) {
+      $resizer.fadeIn();
+      this.resizable(true);
+    } else {
+      $resizer.fadeOut();
+      this.resizable(false);
+    }
+  },
+
+  // ----------
+  // Function: makeActive
+  // Updates this item to visually indicate that it's active.
+  makeActive: function() {
+   iQ(this.container).find("canvas").addClass("focus");
+   iQ(this.container).find("img.cached-thumb").addClass("focus");
+
+  },
+
+  // ----------
+  // Function: makeDeactive
+  // Updates this item to visually indicate that it's not active.
+  makeDeactive: function() {
+   iQ(this.container).find("canvas").removeClass("focus");
+   iQ(this.container).find("img.cached-thumb").removeClass("focus");
+  },
+
+  // ----------
+  // Function: zoomIn
+  // Allows you to select the tab and zoom in on it, thereby bringing you
+  // to the tab in Firefox to interact with.
+  // Parameters:
+  //   isNewBlankTab - boolean indicates whether it is a newly opened blank tab.
+  zoomIn: function(isNewBlankTab) {
+    var self = this;
+    var $tabEl = iQ(this.container);
+    var childHitResult = { shouldZoom: true };
+    if (this.parent)
+      childHitResult = this.parent.childHit(this);
+
+    if (childHitResult.shouldZoom) {
+      // Zoom in!
+      var orig = $tabEl.bounds();
+      var scale = window.innerWidth/orig.width;
+      var tab = this.tab;
+
+      function onZoomDone() {
+        TabItems.resumePainting();
+        // If it's not focused, the onFocus lsitener would handle it.
+        if (gBrowser.selectedTab == tab)
+          UI.tabOnFocus(tab);
+        else
+          gBrowser.selectedTab = tab;
+
+        $tabEl
+          .css(orig.css())
+          .removeClass("front");
+
+        // If the tab is in a groupItem set then set the active
+        // groupItem to the tab's parent.
+        if (self.parent) {
+          var gID = self.parent.id;
+          var groupItem = GroupItems.groupItem(gID);
+          GroupItems.setActiveGroupItem(groupItem);
+          groupItem.setActiveTab(self);
+        } else {
+          GroupItems.setActiveGroupItem(null);
+          GroupItems.setActiveOrphanTab(self);
+        }
+        GroupItems.updateTabBar();
+
+        if (isNewBlankTab)
+          gWindow.gURLBar.focus();
+
+        if (childHitResult.callback)
+          childHitResult.callback();
+      }
+
+      // The scaleCheat is a clever way to speed up the zoom-in code.
+      // Because image scaling is slowest on big images, we cheat and stop the image
+      // at scaled-down size and placed accordingly. Because the animation is fast, you can't
+      // see the difference but it feels a lot zippier. The only trick is choosing the
+      // right animation function so that you don't see a change in percieved
+      // animation speed.
+      var scaleCheat = 1.7;
+      TabItems.pausePainting();
+      $tabEl
+        .addClass("front")
+        .animate({
+          top:    orig.top    * (1 - 1/scaleCheat),
+          left:   orig.left   * (1 - 1/scaleCheat),
+          width:  orig.width  * scale/scaleCheat,
+          height: orig.height * scale/scaleCheat
+        }, {
+          duration: 230,
+          easing: 'fast',
+          complete: onZoomDone
+        });
+    }
+  },
+
+  // ----------
+  // Function: zoomOut
+  // Handles the zoom down animation after returning to TabView.
+  // It is expected that this routine will be called from the chrome thread
+  //
+  // Parameters:
+  //   complete - a function to call after the zoom down animation
+  zoomOut: function(complete) {
+    var $tab = iQ(this.container);
+
+    var box = this.getBounds();
+    box.width -= this.sizeExtra.x;
+    box.height -= this.sizeExtra.y;
+
+    TabItems.pausePainting();
+
+    var self = this;
+    $tab.animate({
+      left: box.left,
+      top: box.top,
+      width: box.width,
+      height: box.height
+    }, {
+      duration: 300,
+      easing: 'cubic-bezier', // note that this is legal easing, even without parameters
+      complete: function() { // note that this will happen on the DOM thread
+        $tab.removeClass('front');
+
+        GroupItems.setActiveOrphanTab(null);
+
+        TabItems.resumePainting();
+
+        self._zoomPrep = false;
+        self.setBounds(self.getBounds(), true, {force: true});
+
+        if (typeof complete == "function")
+          complete();
+      }
+    });
+  },
+
+  // ----------
+  // Function: setZoomPrep
+  // Either go into or return from (depending on <value>) "zoom prep" mode,
+  // where the tab fills a large portion of the screen in anticipation of
+  // the zoom out animation.
+  setZoomPrep: function(value) {
+    var $div = iQ(this.container);
+    var data;
+
+    var box = this.getBounds();
+    if (value) {
+      this._zoomPrep = true;
+
+      // The divide by two part here is a clever way to speed up the zoom-out code.
+      // Because image scaling is slowest on big images, we cheat and start the image
+      // at half-size and placed accordingly. Because the animation is fast, you can't
+      // see the difference but it feels a lot zippier. The only trick is choosing the
+      // right animation function so that you don't see a change in percieved
+      // animation speed from frame #1 (the tab) to frame #2 (the half-size image) to
+      // frame #3 (the first frame of real animation). Choosing an animation that starts
+      // fast is key.
+      var scaleCheat = 2;
+      $div
+        .addClass('front')
+        .css({
+          left: box.left * (1-1/scaleCheat),
+          top: box.top * (1-1/scaleCheat),
+          width: window.innerWidth/scaleCheat,
+          height: box.height * (window.innerWidth / box.width)/scaleCheat
+        });
+    } else {
+      this._zoomPrep = false;
+      $div.removeClass('front');
+
+      this.setBounds(box, true, {force: true});
+    }
+  }
+});
+
+// ##########
+// Class: TabItems
+// Singleton for managing <TabItem>s
+window.TabItems = {
+  minTabWidth: 40,
+  tabWidth: 160,
+  tabHeight: 120,
+  fontSize: 9,
+  items: [],
+  paintingPaused: 0,
+  _tabsWaitingForUpdate: [],
+  _heartbeatOn: false,
+  _heartbeatTiming: 100, // milliseconds between beats
+  _lastUpdateTime: Date.now(),
+  _eventListeners: [],
+
+  // ----------
+  // Function: init
+  // Set up the necessary tracking to maintain the <TabItems>s.
+  init: function() {
+    Utils.assert(window.AllTabs, "AllTabs must be initialized first");
+    var self = this;
+
+    // When a tab is opened, create the TabItem
+    this._eventListeners["open"] = function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      setTimeout(function() { // Marshal event from chrome thread to DOM thread
+        self.link(tab);
+      }, 1);
+    }
+    // When a tab's content is loaded, show the canvas and hide the cached data
+    // if necessary.
+    this._eventListeners["attrModified"] = function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      setTimeout(function() { // Marshal event from chrome thread to DOM thread
+        self.update(tab);
+      }, 1);
+    }
+    // When a tab is closed, unlink.
+    this._eventListeners["close"] = function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      setTimeout(function() { // Marshal event from chrome thread to DOM thread
+        self.unlink(tab);
+      }, 1);
+    }
+    for (let name in this._eventListeners) {
+      AllTabs.register(name, this._eventListeners[name]);
+    }
+
+    // For each tab, create the link.
+    AllTabs.tabs.forEach(function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      self.link(tab);
+      self.update(tab);
+    });
+  },
+
+  // ----------
+  // Function: uninit
+  uninit: function() {
+    for (let name in this._eventListeners) {
+      AllTabs.unregister(name, this._eventListeners[name]);
+    }
+    this.items.forEach(function(tabItem) {
+      for (let x in tabItem) {
+        if (typeof tabItem[x] == "object")
+          tabItem[x] = null;
+      }
+    });
+
+    this.items = null;
+    this._eventListeners = null;
+    this._lastUpdateTime = null;
+    this._tabsWaitingForUpdate = null;
+  },
+
+  // ----------
+  // Function: update
+  // Takes in a xul:tab.
+  update: function(tab) {
+    try {
+      Utils.assertThrow(tab, "tab");
+
+      let shouldDefer = (
+        this.isPaintingPaused() ||
+        this._tabsWaitingForUpdate.length ||
+        Date.now() - this._lastUpdateTime < this._heartbeatTiming
+      );
+
+      let isCurrentTab = (
+        !UI._isTabViewVisible() &&
+        tab == gBrowser.selectedTab
+      );
+
+      if (shouldDefer && !isCurrentTab) {
+        if (this._tabsWaitingForUpdate.indexOf(tab) == -1)
+          this._tabsWaitingForUpdate.push(tab);
+      } else
+        this._update(tab);
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: _update
+  // Takes in a xul:tab.
+  _update: function(tab) {
+    try {
+      Utils.assertThrow(tab, "tab");
+
+      // ___ remove from waiting list if needed
+      let index = this._tabsWaitingForUpdate.indexOf(tab);
+      if (index != -1)
+        this._tabsWaitingForUpdate.splice(index, 1);
+
+      // ___ get the TabItem
+      Utils.assertThrow(tab.tabItem, "must already be linked");
+      let tabItem = tab.tabItem;
+
+      // ___ icon
+      let iconUrl = tab.image;
+      if (iconUrl == null)
+        iconUrl = "chrome://mozapps/skin/places/defaultFavicon.png";
+
+      if (iconUrl != tabItem.favEl.src)
+        tabItem.favEl.src = iconUrl;
+
+      // ___ URL
+      let tabUrl = tab.linkedBrowser.currentURI.spec;
+      if (tabUrl != tabItem.url) {
+        let oldURL = tabItem.url;
+        tabItem.url = tabUrl;
+
+        if (!tabItem.reconnected && (oldURL == 'about:blank' || !oldURL))
+          this.reconnect(tabItem);
+
+        tabItem.save();
+      }
+
+      // ___ label
+      let label = tab.label;
+      let $name = iQ(tabItem.nameEl);
+      if (!tabItem.isShowingCachedData && $name.text() != label)
+        $name.text(label);
+
+      // ___ thumbnail
+      let $canvas = iQ(tabItem.canvasEl);
+      if (!tabItem.canvasSizeForced) {
+        let w = $canvas.width();
+        let h = $canvas.height();
+        if (w != tabItem.canvasEl.width || h != tabItem.canvasEl.height) {
+          tabItem.canvasEl.width = w;
+          tabItem.canvasEl.height = h;
+        }
+      }
+
+      tabItem.tabCanvas.paint();
+
+      // ___ cache
+      // TODO: this logic needs to be better; hiding too soon now
+      if (tabItem.isShowingCachedData && !tab.hasAttribute("busy"))
+        tabItem.hideCachedData();
+    } catch(e) {
+      Utils.log(e);
+    }
+
+    this._lastUpdateTime = Date.now();
+  },
+
+  // ----------
+  // Function: link
+  // Takes in a xul:tab.
+  link: function(tab){
+    try {
+      Utils.assertThrow(tab, "tab");
+      Utils.assertThrow(!tab.tabItem, "shouldn't already be linked");
+      new TabItem(tab); // sets tab.tabItem to itself
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: unlink
+  // Takes in a xul:tab.
+  unlink: function(tab) {
+    try {
+      Utils.assertThrow(tab, "tab");
+      Utils.assertThrow(tab.tabItem, "should already be linked");
+
+      this.unregister(tab.tabItem);
+      tab.tabItem._sendToSubscribers("close");
+      iQ(tab.tabItem.container).remove();
+      tab.tabItem.removeTrenches();
+      Items.unsquish(null, tab.tabItem);
+
+      tab.tabItem = null;
+
+      let index = this._tabsWaitingForUpdate.indexOf(tab);
+      if (index != -1)
+        this._tabsWaitingForUpdate.splice(index, 1);
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // ----------
+  // Function: heartbeat
+  // Allows us to spreadout update calls over a period of time.
+  heartbeat: function() {
+    if (!this._heartbeatOn)
+      return;
+
+    if (this._tabsWaitingForUpdate.length) {
+      this._update(this._tabsWaitingForUpdate[0]);
+      // _update will remove the tab from the waiting list
+    }
+
+    let self = this;
+    if (this._tabsWaitingForUpdate.length) {
+      setTimeout(function() {
+        self.heartbeat();
+      }, this._heartbeatTiming);
+    } else
+      this._hearbeatOn = false;
+  },
+
+  // ----------
+  // Function: pausePainting
+  // Tells TabItems to stop updating thumbnails (so you can do
+  // animations without thumbnail paints causing stutters).
+  // pausePainting can be called multiple times, but every call to
+  // pausePainting needs to be mirrored with a call to <resumePainting>.
+  pausePainting: function() {
+    this.paintingPaused++;
+
+    if (this.isPaintingPaused() && this._heartbeatOn)
+      this._heartbeatOn = false;
+  },
+
+  // ----------
+  // Function: resumePainting
+  // Undoes a call to <pausePainting>. For instance, if you called
+  // pausePainting three times in a row, you'll need to call resumePainting
+  // three times before TabItems will start updating thumbnails again.
+  resumePainting: function() {
+    this.paintingPaused--;
+
+    if (!this.isPaintingPaused() &&
+        this._tabsWaitingForUpdate.length &&
+        !this._heartbeatOn) {
+      this._heartbeatOn = true;
+      this.heartbeat();
+    }
+  },
+
+  // ----------
+  // Function: isPaintingPaused
+  // Returns a boolean indicating whether painting
+  // is paused or not.
+  isPaintingPaused: function() {
+    return this.paintingPaused > 0;
+  },
+
+  // ----------
+  // Function: register
+  // Adds the given <TabItem> to the master list.
+  register: function(item) {
+    Utils.assert(item && item.isAnItem, 'item must be a TabItem');
+    Utils.assert(this.items.indexOf(item) == -1, 'only register once per item');
+    this.items.push(item);
+  },
+
+  // ----------
+  // Function: unregister
+  // Removes the given <TabItem> from the master list.
+  unregister: function(item) {
+    var index = this.items.indexOf(item);
+    if (index != -1)
+      this.items.splice(index, 1);
+  },
+
+  // ----------
+  // Function: getItems
+  // Returns a copy of the master array of <TabItem>s.
+  getItems: function() {
+    return Utils.copy(this.items);
+  },
+
+  // ----------
+  // Function: saveAll
+  // Saves all open <TabItem>s.
+  //
+  // Parameters:
+  //   saveImageData - true to include thumbnail pixels (and page title as well); default false
+  saveAll: function(saveImageData) {
+    var items = this.getItems();
+    items.forEach(function(item) {
+      item.save(saveImageData);
+    });
+  },
+
+  // ----------
+  // Function: storageSanity
+  // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage)
+  // and returns true if it looks valid.
+  // TODO: check everything
+  storageSanity: function(data) {
+    var sane = true;
+    if (!Utils.isRect(data.bounds)) {
+      Utils.log('TabItems.storageSanity: bad bounds', data.bounds);
+      sane = false;
+    }
+
+    return sane;
+  },
+
+  // ----------
+  // Function: reconnect
+  // Given a <TabItem>, attempts to load its persistent data from storage.
+  reconnect: function(item) {
+    var found = false;
+
+    try{
+      Utils.assert(item, 'item');
+      Utils.assert(item.tab, 'item.tab');
+
+      if (item.reconnected)
+        return true;
+
+      if (!item.tab)
+        return false;
+
+      let tabData = Storage.getTabData(item.tab);
+      if (tabData && this.storageSanity(tabData)) {
+        if (item.parent)
+          item.parent.remove(item);
+
+        item.setBounds(tabData.bounds, true);
+
+        if (Utils.isPoint(tabData.userSize))
+          item.userSize = new Point(tabData.userSize);
+
+        if (tabData.groupID) {
+          var groupItem = GroupItems.groupItem(tabData.groupID);
+          if (groupItem) {
+            groupItem.add(item);
+
+            if (item.tab == gBrowser.selectedTab)
+              GroupItems.setActiveGroupItem(item.parent);
+          }
+        }
+
+        if (tabData.imageData) {
+          item.showCachedData(tabData);
+          // the code in the progress listener doesn't fire sometimes because
+          // tab is being restored so need to catch that.
+          setTimeout(function() {
+            if (item && item.isShowingCachedData) {
+              item.hideCachedData();
+            }
+          }, 15000);
+        }
+
+        item.reconnected = true;
+        found = true;
+      } else
+        item.reconnected = item.tab.linkedBrowser.currentURI.spec != 'about:blank';
+
+      item.save();
+    } catch(e) {
+      Utils.log(e);
+    }
+
+    return found;
+  }
+};
+
+// ##########
+// Class: TabCanvas
+// Takes care of the actual canvas for the tab thumbnail
+// Does not need to be accessed from outside of tabitems.js
+var TabCanvas = function(tab, canvas) {
+  this.init(tab, canvas);
+};
+
+TabCanvas.prototype = {
+  // ----------
+  // Function: init
+  init: function(tab, canvas) {
+    this.tab = tab;
+    this.canvas = canvas;
+
+    var $canvas = iQ(canvas);
+    var w = $canvas.width();
+    var h = $canvas.height();
+    canvas.width = w;
+    canvas.height = h;
+  },
+
+  // ----------
+  // Function: paint
+  paint: function(evt) {
+    var ctx = this.canvas.getContext("2d");
+
+    var w = this.canvas.width;
+    var h = this.canvas.height;
+    if (!w || !h)
+      return;
+
+    let fromWin = this.tab.linkedBrowser.contentWindow;
+    if (fromWin == null) {
+      Utils.log('null fromWin in paint');
+      return;
+    }
+
+    var scaler = w/fromWin.innerWidth;
+
+    // TODO: Potentially only redraw the dirty rect? (Is it worth it?)
+
+    ctx.save();
+    ctx.scale(scaler, scaler);
+    try{
+      ctx.drawWindow(fromWin, fromWin.scrollX, fromWin.scrollY, w/scaler, h/scaler, "#fff");
+    } catch(e) {
+      Utils.error('paint', e);
+    }
+
+    ctx.restore();
+  },
+
+  // ----------
+  // Function: toImageData
+  toImageData: function() {
+    return this.canvas.toDataURL("image/png", "");
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/tabview.css
@@ -0,0 +1,479 @@
+html {
+  overflow: hidden;
+/*   image-rendering: -moz-crisp-edges; */
+}
+
+body {
+  background-color: transparent;  
+  font-family: Tahoma, sans-serif !important;
+  padding: 0px;
+  color: rgba(0,0,0,0.4);
+  font-size: 12px;
+  line-height: 16px;
+  margin: 0 auto;
+}
+
+#content {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+#bg {
+  background: -moz-linear-gradient(top,#C4C4C4,#9E9E9E);
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: -999999;
+}
+
+/* Tab Styling
+----------------------------------*/
+
+
+.tab {
+  position: absolute;
+  padding: 4px 6px 6px 4px;
+  border: 1px solid rgba(230,230,230,1);
+  background-color: rgba(245,245,245,1);
+  overflow: visible !important;
+  -moz-border-radius: 0.4em;
+  -moz-box-shadow: inset rgba(255, 255, 255, 0.6) 0 0 0 2px;
+  cursor: pointer;
+}
+
+.tab canvas,
+.cached-thumb {
+  border: 1px solid rgba(0,0,0,0.2);
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0px;
+  left: 0px;
+}
+
+.thumb {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+.thumb-shadow {
+  position: absolute;
+  border-bottom: 5px solid rgba(0,0,0,0.05);
+  margin-right: -12px;
+  bottom: 2px;
+  width: 94.5%;
+}
+
+.favicon {
+  position: absolute;
+  background-color: rgba(245,245,245,1);
+  -moz-border-radius-bottomright: 0.4em;
+  -moz-box-shadow:
+    inset rgba(255, 255, 255, 0.6) 0 -2px 0px,
+    inset rgba(255, 255, 255, 0.6) -2px 0px 0px;
+  padding: 4px 6px 6px 4px;
+  top: 4px;
+  left: 4px;
+  border-right: 1px solid rgba(0,0,0,0.2);
+  border-bottom: 1px solid rgba(0,0,0,0.2);
+  height: 17px;
+  width: 17px;
+}
+
+.favicon img {
+  border: none;
+  width: 16px;
+  height: 16px;
+}
+
+.close {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  width: 16px;
+  height: 16px;
+  /* background is set in platform.css */
+  opacity: 0.2;
+  cursor: pointer;
+}
+
+.close:hover {
+  opacity: 1.0;
+}
+
+.expander {
+  position: absolute;
+  bottom: 6px;
+  right: 6px;
+  width: 16px;
+  height: 16px;
+  background: url(chrome://global/skin/icons/resizer.png) no-repeat;
+  opacity: 0.2;
+}
+
+.expander:hover {
+  opacity: 1.0;
+}
+
+.favicon img:hover, 
+.close img:hover, 
+.expander img:hover {
+  opacity: 1;
+  border: none;
+}
+
+.tab-title {
+  position: absolute;
+  top: 100%;
+  text-align: center;
+  width: 94.5%;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+.stacked {
+  padding: 0;
+}
+
+.stacked .tab-title {
+  display: none;
+}
+
+.stacked .thumb-shadow {
+  display: none;
+}
+
+.stacked .thumb {
+  -moz-box-shadow: rgba(0,0,0,.2) 1px 1px 6px;
+}
+
+.stack-trayed .tab-title {
+  display: block !important;
+  text-shadow: rgba(0,0,0,1) 1px 1px 2px;
+  color: #EEE;
+  font-size: 11px;
+}
+
+.stack-trayed .thumb {
+  -moz-box-shadow: none !important;
+}
+
+.focus {
+  -moz-box-shadow:  rgba(54,79,225,1) 0px 0px 5px -1px !important;
+}
+
+.front .tab-title, 
+.front .close, 
+.front .favicon, 
+.front .expander, 
+.front .thumb-shadow {
+  display: none;
+}
+
+.front .focus {
+  -moz-box-shadow: none !important;
+}
+
+/* Tab GroupItem
+----------------------------------*/
+
+.tabInGroupItem {
+  border: none;
+  -moz-box-shadow: none !important;
+}
+
+
+.groupItem {
+  position: absolute;
+/*   float: left;  */
+  cursor: move;
+  border: 1px solid rgba(230,230,230,1);
+  background-color: rgba(248,248,248,1);
+  -moz-border-radius: 0.4em;
+  -moz-box-shadow:
+    inset rgba(255, 255, 255, 0.6) 0 0 0 2px,
+    rgba(0,0,0,0.2) 1px 1px 4px;
+}
+
+.groupItem.activeGroupItem {
+  -moz-box-shadow:
+    rgba(0,0,0,0.6) 1px 1px 8px;
+}
+
+.phantom {
+  border: 1px solid rgba(190,190,190,1);
+}
+
+.overlay {
+  background-color: rgba(0,0,0,.7) !important;
+  -moz-box-shadow: 3px 3px 8px rgba(0,0,0,.5);
+  -moz-border-radius: 0.4em;
+  /*
+  border: 1px solid rgba(230,230,230,1);
+  background-color: rgba(248,248,248,1);
+  -moz-box-shadow:
+    rgba(0,0,0, .3) 2px 2px 8px,
+    inset rgba(255, 255, 255, 0.6) 0 0 0 2px; */
+}
+
+/* InfoItems
+----------------------------------*/
+
+.info-item {
+  position: absolute;
+  cursor: move;
+  border: 1px solid rgba(230,230,230,1);
+  background-color: rgba(248,248,248,1);
+  -moz-border-radius: 0.4em;
+  -moz-box-shadow:
+    inset rgba(255, 255, 255, 0.6) 0 0 0 2px,
+    rgba(0,0,0, .2) 1px 1px 4px;
+}
+
+.intro {
+  margin: 10px;
+}
+
+/* Trenches
+----------------------------------*/
+
+.guideTrench, 
+.visibleTrench, 
+.activeVisibleTrench {
+  position: absolute;
+}
+
+.guideTrench {
+  z-index: -101;
+  opacity: 0.9;
+  border: 1px dashed  rgba(0,0,0,.12);
+  border-bottom: none;
+  border-right: none;
+  -moz-box-shadow: 1px 1px 0 rgba(255,255,255,.15);
+}
+
+.visibleTrench {
+  z-index: -103;
+  opacity: 0.05;
+}
+
+.activeVisibleTrench {
+  z-index: -102;
+  opacity: 0;
+}
+
+.activeVisibleTrench.activeTrench {
+  opacity: 0.45;
+}
+
+.visibleTrench.border, 
+.activeVisibleTrench.border {
+  background-color: red;
+}
+
+.visibleTrench.guide, 
+.activeVisibleTrench.guide {
+  background-color: blue;
+}
+
+/* Other
+----------------------------------*/
+
+.newTabButton {
+  width: 16px;
+  height: 15px;
+  bottom: 10px;
+  left: 10px;
+  position: absolute !important;
+  cursor: pointer;
+  opacity: .3;
+  background-image: url(chrome://browser/skin/tabview/new-tab.png);
+  z-index: 99999;
+}
+
+.newTabButton:hover {
+  opacity: 1;
+}
+
+.newTabButtonAlt {
+  position: absolute;
+  cursor: pointer;
+  z-index: 99999;
+  border: none;
+  -moz-border-radius: 4px;
+  font-size: 50px;
+  line-height: 50px;
+  height: 57px !important;
+  width: 70px !important;
+  margin-top: 4px !important;
+  text-align: center;
+  background-color: #888888;
+  -moz-box-shadow: inset 0px 0px 5px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.3);
+}
+
+.newTabButtonAlt > span {
+  color: #909090;
+  text-shadow: 0px 0px 7px rgba(0,0,0,.4), 0 -1px 0 rgba(255,255,255,.6);
+  font-weight: bold;
+}
+
+.active {
+  -moz-box-shadow: 5px 5px 4px rgba(0,0,0,.5);
+}
+
+.acceptsDrop {
+  -moz-box-shadow: 2px 2px 10px -1px rgba(0,0,0,.6);
+}
+
+.titlebar {
+  font-size: 12px;
+  line-height: 18px;
+  height: 18px;
+}
+
+input.name {
+  background: transparent;
+  border: 1px solid transparent;
+  color: #999;
+  margin: 3px 0px 0px 3px;
+  padding: 1px;
+  background-image: url(chrome://browser/skin/tabview/edit-light.png);
+  padding-left: 20px;
+}
+
+input.name:hover {
+  border: 1px solid #ddd;
+}
+
+input.name-locked:hover {
+  border: 1px solid transparent !important;
+  cursor: default;
+}
+
+input.name:focus {
+  color: #555;
+}
+
+input.defaultName {
+  font-style: italic !important;
+  background-image-opacity: .1;
+  color: transparent;
+}
+
+input.defaultName:hover {
+  color: #CCC;
+}
+
+.title-container {
+  cursor: text;
+}
+
+.title-shield {
+  position: absolute;
+  margin: 3px 0px 0px 3px;
+  padding: 1px;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 10;
+}
+
+.transparentBorder {
+  border: 1px solid transparent !important;
+}
+
+.stackExpander {
+  position: absolute;
+  opacity: .4;
+  cursor: pointer;
+  background-image: url(chrome://browser/skin/tabview/stack-expander.png);
+  width: 24px;
+  height: 24px;
+}
+
+.stackExpander:hover {
+  opacity: .7 !important;
+}
+
+.shield {
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	position: absolute;
+}
+
+/* Resizable
+----------------------------------*/
+.resizer {
+  background-image: url(chrome://global/skin/icons/resizer.png);
+  position: absolute;
+	width: 16px;
+	height: 16px;
+	bottom: 0px;
+	right: 0px;
+  opacity: .2;
+}
+
+.iq-resizable { }
+
+.iq-resizable-handle {
+  position: absolute;
+  font-size: 0.1px;
+  z-index: 99999;
+  display: block;
+}
+
+.iq-resizable-disabled .iq-resizable-handle, 
+.iq-resizable-autohide .iq-resizable-handle {
+  display: none;
+}
+
+.iq-resizable-se {
+  cursor: se-resize;
+  width: 12px;
+  height: 12px;
+  right: 1px;
+  bottom: 1px;
+}
+
+/* Utils
+----------------------------------*/
+
+.front {
+  z-index: 999999 !important;
+  -moz-border-radius: 0 !important;
+  -moz-box-shadow: none !important;
+  -moz-transform: none !important;
+  image-rendering: -moz-crisp-edges;
+}
+
+/* Feedback
+----------------------------------*/
+
+.bottomButton {
+  position: absolute;
+  bottom: 0px;
+  width: 100px;
+  height: 20px;
+  line-height: 20px;
+  z-index: 99999 !important;
+  background-color: blue;
+  text-align: center;
+  color: white;
+  background-color: #9E9E9E;
+  -moz-box-shadow: 0px 0px 4px rgba(0,0,0,.3), inset 0px 1px 0px rgba(255,255,255,.4);
+}
+
+.bottomButton:hover {
+  cursor: pointer;
+  background-color: #A5A5A5;
+  -moz-box-shadow: 0px 0px 5px rgba(0,0,0,.6), inset 0px 1px 0px rgba(255,255,255,.4);
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/tabview.html
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
+<head>
+  <title>&nbsp;</title>
+  <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
+  <link rel="stylesheet" href="tabview.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/skin/tabview/tabview.css" type="text/css"/>
+</head>
+
+<body transparent="true">
+  <div id="content">
+    <div id="bg" />
+  </div>
+
+  <script type="text/javascript;version=1.8" src="tabview.js"></script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/tabview.js
@@ -0,0 +1,38 @@
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/tabview/AllTabs.jsm");
+Cu.import("resource://gre/modules/tabview/groups.jsm");
+Cu.import("resource://gre/modules/tabview/utils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gWindow", function() {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor).
+    getInterface(Ci.nsIWebNavigation).
+    QueryInterface(Ci.nsIDocShell).
+    chromeEventHandler.ownerDocument.defaultView;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gBrowser", function() gWindow.gBrowser);
+
+XPCOMUtils.defineLazyGetter(this, "gTabViewDeck", function() {
+  return gWindow.document.getElementById("tab-view-deck");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTabViewFrame", function() {
+  return gWindow.document.getElementById("tab-view");
+});
+
+# NB: Certain files need to evaluate before others
+
+#include iq.js
+#include storage.js
+#include items.js
+#include groupitems.js
+#include tabitems.js
+#include drag.js
+#include trench.js
+#include infoitems.js
+#include ui.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/trench.js
@@ -0,0 +1,677 @@
+/* ***** 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 trench.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: trench.js
+
+// ##########
+// Class: Trench
+//
+// Class for drag-snapping regions; called "trenches" as they are long and narrow.
+
+// Constructor: Trench
+//
+// Parameters:
+//   element - the DOM element for Item (GroupItem or TabItem) from which the trench is projected
+//   xory - either "x" or "y": whether the trench's <position> is along the x- or y-axis.
+//     In other words, if "x", the trench is vertical; if "y", the trench is horizontal.
+//   type - either "border" or "guide". Border trenches mark the border of an Item.
+//     Guide trenches extend out (unless they are intercepted) and act as "guides".
+//   edge - which edge of the Item that this trench corresponds to.
+//     Either "top", "left", "bottom", or "right".
+var Trench = function(element, xory, type, edge) {
+  //----------
+  // Variable: id
+  // (integer) The id for the Trench. Set sequentially via <Trenches.nextId>
+  this.id = Trenches.nextId++;
+
+  // ---------
+  // Variables: Initial parameters
+  //   element - (DOMElement)
+  //   parentItem - <Item> which projects this trench; to be set with setParentItem
+  //   xory - (string) "x" or "y"
+  //   type - (string) "border" or "guide"
+  //   edge - (string) "top", "left", "bottom", or "right"
+  this.el = element;
+  this.parentItem = null;
+  this.xory = xory; // either "x" or "y"
+  this.type = type; // "border" or "guide"
+  this.edge = edge; // "top", "left", "bottom", or "right"
+
+  this.$el = iQ(this.el);
+
+  //----------
+  // Variable: dom
+  // (array) DOM elements for visible reflexes of the Trench
+  this.dom = [];
+
+  //----------
+  // Variable: showGuide
+  // (boolean) Whether this trench will project a visible guide (dotted line) or not.
+  this.showGuide = false;
+
+  //----------
+  // Variable: active
+  // (boolean) Whether this trench is currently active or not.
+  // Basically every trench aside for those projected by the Item currently being dragged
+  // all become active.
+  this.active = false;
+  this.gutter = Items.defaultGutter;
+
+  //----------
+  // Variable: position
+  // (integer) position is the position that we should snap to.
+  this.position = 0;
+
+  //----------
+  // Variables: some Ranges
+  //   range - (<Range>) explicit range; this is along the transverse axis
+  //   minRange - (<Range>) the minimum active range
+  //   activeRange - (<Range>) the currently active range
+  this.range = new Range(0,10000);
+  this.minRange = new Range(0,0);
+  this.activeRange = new Range(0,10000);
+};
+
+Trench.prototype = {
+  //----------
+  // Variable: radius
+  // (integer) radius is how far away we should snap from
+  get radius() this.customRadius || Trenches.defaultRadius,
+
+  setParentItem: function Trench_setParentItem(item) {
+    if (!item.isAnItem) {
+      Utils.assert(false, "parentItem must be an Item");
+      return false;
+    }
+    this.parentItem = item;
+    return true;
+  },
+
+  //----------
+  // Function: setPosition
+  // set the trench's position.
+  //
+  // Parameters:
+  //   position - (integer) px center position of the trench
+  //   range - (<Range>) the explicit active range of the trench
+  //   minRange - (<Range>) the minimum range of the trench
+  setPosition: function Trench_setPos(position, range, minRange) {
+    this.position = position;
+
+    var page = Items.getPageBounds(true);
+
+    // optionally, set the range.
+    if (Utils.isRange(range)) {
+      this.range = range;
+    } else {
+      this.range = new Range(0, (this.xory == 'x' ? page.height : page.width));
+    }
+
+    // if there's a minRange, set that too.
+    if (Utils.isRange(minRange))
+      this.minRange = minRange;
+
+    // set the appropriate bounds as a rect.
+    if (this.xory == "x") // vertical
+      this.rect = new Rect(this.position - this.radius, this.range.min, 2 * this.radius, this.range.extent);
+    else // horizontal
+      this.rect = new Rect(this.range.min, this.position - this.radius, this.range.extent, 2 * this.radius);
+
+    this.show(); // DEBUG
+  },
+
+  //----------
+  // Function: setActiveRange
+  // set the trench's currently active range.
+  //
+  // Parameters:
+  //   activeRange - (<Range>)
+  setActiveRange: function Trench_setActiveRect(activeRange) {
+    if (!Utils.isRange(activeRange))
+      return false;
+    this.activeRange = activeRange;
+    if (this.xory == "x") { // horizontal
+      this.activeRect = new Rect(this.position - this.radius, this.activeRange.min, 2 * this.radius, this.activeRange.extent);
+      this.guideRect = new Rect(this.position, this.activeRange.min, 0, this.activeRange.extent);
+    } else { // vertical
+      this.activeRect = new Rect(this.activeRange.min, this.position - this.radius, this.activeRange.extent, 2 * this.radius);
+      this.guideRect = new Rect(this.activeRange.min, this.position, this.activeRange.extent, 0);
+    }
+    return true;
+  },
+
+  //----------
+  // Function: setWithRect
+  // Set the trench's position using the given rect. We know which side of the rect we should match
+  // because we've already recorded this information in <edge>.
+  //
+  // Parameters:
+  //   rect - (<Rect>)
+  setWithRect: function Trench_setWithRect(rect) {
+
+    if (!Utils.isRect(rect))
+      Utils.error('argument must be Rect');
+
+    // First, calculate the range for this trench.
+    // Border trenches are always only active for the length of this range.
+    // Guide trenches, however, still use this value as its minRange.
+    if (this.xory == "x")
+      var range = new Range(rect.top - this.gutter, rect.bottom + this.gutter);
+    else
+      var range = new Range(rect.left - this.gutter, rect.right + this.gutter);
+
+    if (this.type == "border") {
+      // border trenches have a range, so set that too.
+      if (this.edge == "left")
+        this.setPosition(rect.left - this.gutter, range);
+      else if (this.edge == "right")
+        this.setPosition(rect.right + this.gutter, range);
+      else if (this.edge == "top")
+        this.setPosition(rect.top - this.gutter, range);
+      else if (this.edge == "bottom")
+        this.setPosition(rect.bottom + this.gutter, range);
+    } else if (this.type == "guide") {
+      // guide trenches have no range, but do have a minRange.
+      if (this.edge == "left")
+        this.setPosition(rect.left, false, range);
+      else if (this.edge == "right")
+        this.setPosition(rect.right, false, range);
+      else if (this.edge == "top")
+        this.setPosition(rect.top, false, range);
+      else if (this.edge == "bottom")
+        this.setPosition(rect.bottom, false, range);
+    }
+  },
+
+  //----------
+  // Function: show
+  //
+  // Show guide (dotted line), if <showGuide> is true.
+  //
+  // If <Trenches.showDebug> is true, we will draw the trench. Active portions are drawn with 0.5
+  // opacity. If <active> is false, the entire trench will be
+  // very translucent.
+  show: function Trench_show() { // DEBUG
+    if (this.active && this.showGuide) {
+      if (!this.dom.guideTrench)
+        this.dom.guideTrench = iQ("<div/>").addClass('guideTrench').css({id: 'guideTrench'+this.id});
+      var guideTrench = this.dom.guideTrench;
+      guideTrench.css(this.guideRect.css());
+      iQ("body").append(guideTrench);
+    } else {
+      if (this.dom.guideTrench) {
+        this.dom.guideTrench.remove();
+        delete this.dom.guideTrench;
+      }
+    }
+
+    if (!Trenches.showDebug) {
+      this.hide(true); // true for dontHideGuides
+      return;
+    }
+
+    if (!this.dom.visibleTrench)
+      this.dom.visibleTrench = iQ("<div/>")
+        .addClass('visibleTrench')
+        .addClass(this.type) // border or guide
+        .css({id: 'visibleTrench'+this.id});
+    var visibleTrench = this.dom.visibleTrench;
+
+    if (!this.dom.activeVisibleTrench)
+      this.dom.activeVisibleTrench = iQ("<div/>")
+        .addClass('activeVisibleTrench')
+        .addClass(this.type) // border or guide
+        .css({id: 'activeVisibleTrench'+this.id});
+    var activeVisibleTrench = this.dom.activeVisibleTrench;
+
+    if (this.active)
+      activeVisibleTrench.addClass('activeTrench');
+    else
+      activeVisibleTrench.removeClass('activeTrench');
+
+    visibleTrench.css(this.rect.css());
+    activeVisibleTrench.css((this.activeRect || this.rect).css());
+    iQ("body").append(visibleTrench);
+    iQ("body").append(activeVisibleTrench);
+  },
+
+  //----------
+  // Function: hide
+  // Hide the trench.
+  hide: function Trench_hide(dontHideGuides) {
+    if (this.dom.visibleTrench)
+      this.dom.visibleTrench.remove();
+    if (this.dom.activeVisibleTrench)
+      this.dom.activeVisibleTrench.remove();
+    if (!dontHideGuides && this.dom.guideTrench)
+      this.dom.guideTrench.remove();
+  },
+
+  //----------
+  // Function: rectOverlaps
+  // Given a <Rect>, compute whether it overlaps with this trench. If it does, return an
+  // adjusted ("snapped") <Rect>; if it does not overlap, simply return false.
+  //
+  // Note that simply overlapping is not all that is required to be affected by this function.
+  // Trenches can only affect certain edges of rectangles... for example, a "left"-edge guide
+  // trench should only affect left edges of rectangles. We don't snap right edges to left-edged
+  // guide trenches. For border trenches, the logic is a bit different, so left snaps to right and
+  // top snaps to bottom.
+  //
+  // Parameters:
+  //   rect - (<Rect>) the rectangle in question
+  //   stationaryCorner   - which corner is stationary? by default, the top left.
+  //                        "topleft", "bottomleft", "topright", "bottomright"
+  //   assumeConstantSize - (boolean) whether the rect's dimensions are sacred or not
+  //   keepProportional - (boolean) if we are allowed to change the rect's size, whether the
+  //                                dimensions should scaled proportionally or not.
+  //
+  // Returns:
+  //   false - if rect does not overlap with this trench
+  //   newRect - (<Rect>) an adjusted version of rect, if it is affected by this trench
+  rectOverlaps: function Trench_rectOverlaps(rect,stationaryCorner,assumeConstantSize,keepProportional) {
+    var edgeToCheck;
+    if (this.type == "border") {
+      if (this.edge == "left")
+        edgeToCheck = "right";
+      else if (this.edge == "right")
+        edgeToCheck = "left";
+      else if (this.edge == "top")
+        edgeToCheck = "bottom";
+      else if (this.edge == "bottom")
+        edgeToCheck = "top";
+    } else { // if trench type is guide or barrier...
+      edgeToCheck = this.edge;
+    }
+
+    rect.adjustedEdge = edgeToCheck;
+
+    switch (edgeToCheck) {
+      case "left":
+        if (this.ruleOverlaps(rect.left, rect.yRange)) {
+          if (stationaryCorner.indexOf('right') > -1)
+            rect.width = rect.right - this.position;
+          rect.left = this.position;
+          return rect;
+        }
+        break;
+      case "right":
+        if (this.ruleOverlaps(rect.right, rect.yRange)) {
+          if (assumeConstantSize) {
+            rect.left = this.position - rect.width;
+          } else {
+            var newWidth = this.position - rect.left;
+            if (keepProportional)
+              rect.height = rect.height * newWidth / rect.width;
+            rect.width = newWidth;
+          }
+          return rect;
+        }
+        break;
+      case "top":
+        if (this.ruleOverlaps(rect.top, rect.xRange)) {
+          if (stationaryCorner.indexOf('bottom') > -1)
+            rect.height = rect.bottom - this.position;
+          rect.top = this.position;
+          return rect;
+        }
+        break;
+      case "bottom":
+        if (this.ruleOverlaps(rect.bottom, rect.xRange)) {
+          if (assumeConstantSize) {
+            rect.top = this.position - rect.height;
+          } else {
+            var newHeight = this.position - rect.top;
+            if (keepProportional)
+              rect.width = rect.width * newHeight / rect.height;
+            rect.height = newHeight;
+          }
+          return rect;
+        }
+    }
+
+    return false;
+  },
+
+  //----------
+  // Function: ruleOverlaps
+  // Computes whether the given "rule" (a line segment, essentially), given by the position and
+  // range arguments, overlaps with the current trench. Note that this function assumes that
+  // the rule and the trench are in the same direction: both horizontal, or both vertical.
+  //
+  // Parameters:
+  //   position - (integer) a position in px
+  //   range - (<Range>) the rule's range
+  ruleOverlaps: function Trench_ruleOverlaps(position, range) {
+    return (this.position - this.radius < position &&
+           position < this.position + this.radius &&
+           this.activeRange.contains(range));
+  },
+
+  //----------
+  // Function: adjustRangeIfIntercept
+  // Computes whether the given boundary (given as a position and its active range), perpendicular
+  // to the trench, intercepts the trench or not. If it does, it returns an adjusted <Range> for
+  // the trench. If not, it returns false.
+  //
+  // Parameters:
+  //   position - (integer) the position of the boundary
+  //   range - (<Range>) the target's range, on the trench's transverse axis
+  adjustRangeIfIntercept: function Trench_adjustRangeIfIntercept(position, range) {
+    if (this.position - this.radius > range.min && this.position + this.radius < range.max) {
+      var activeRange = new Range(this.activeRange);
+
+      // there are three ways this can go:
+      // 1. position < minRange.min
+      // 2. position > minRange.max
+      // 3. position >= minRange.min && position <= minRange.max
+
+      if (position < this.minRange.min) {
+        activeRange.min = Math.min(this.minRange.min,position);
+      } else if (position > this.minRange.max) {
+        activeRange.max = Math.max(this.minRange.max,position);
+      } else {
+        // this should be impossible because items can't overlap and we've already checked
+        // that the range intercepts.
+      }
+      return activeRange;
+    }
+    return false;
+  },
+
+  //----------
+  // Function: calculateActiveRange
+  // Computes and sets the <activeRange> for the trench, based on the <GroupItems> around.
+  // This makes it so trenches' active ranges don't extend through other groupItems.
+  calculateActiveRange: function Trench_calculateActiveRange() {
+
+    // set it to the default: just the range itself.
+    this.setActiveRange(this.range);
+
+    // only guide-type trenches need to set a separate active range
+    if (this.type != 'guide')
+      return;
+
+    var groupItems = GroupItems.groupItems;
+    var trench = this;
+    groupItems.forEach(function(groupItem) {
+      if (groupItem.isDragging) // floating groupItems don't block trenches
+        return;
+      if (trench.el == groupItem.container) // groupItems don't block their own trenches
+        return;
+      var bounds = groupItem.getBounds();
+      var activeRange = new Range();
+      if (trench.xory == 'y') { // if this trench is horizontal...
+        activeRange = trench.adjustRangeIfIntercept(bounds.left, bounds.yRange);
+        if (activeRange)
+          trench.setActiveRange(activeRange);
+        activeRange = trench.adjustRangeIfIntercept(bounds.right, bounds.yRange);
+        if (activeRange)
+          trench.setActiveRange(activeRange);
+      } else { // if this trench is vertical...
+        activeRange = trench.adjustRangeIfIntercept(bounds.top, bounds.xRange);
+        if (activeRange)
+          trench.setActiveRange(activeRange);
+        activeRange = trench.adjustRangeIfIntercept(bounds.bottom, bounds.xRange);
+        if (activeRange)
+          trench.setActiveRange(activeRange);
+      }
+    });
+  }
+};
+
+// ##########
+// Class: Trenches
+// Singelton for managing all <Trench>es.
+var Trenches = {
+  // ---------
+  // Variables:
+  //   nextId - (integer) a counter for the next <Trench>'s <Trench.id> value.
+  //   showDebug - (boolean) whether to draw the <Trench>es or not.
+  //   defaultRadius - (integer) the default radius for new <Trench>es.
+  nextId: 0,
+  showDebug: false,
+  defaultRadius: 10,
+
+  // ---------
+  // Variables: snapping preferences; used to break ties in snapping.
+  //   preferTop - (boolean) prefer snapping to the top to the bottom
+  //   preferLeft - (boolean) prefer snapping to the left to the right
+  preferTop: true,
+  preferLeft: true,
+
+  trenches: [],
+
+  // ---------
+  // Function: getById
+  // Return the specified <Trench>.
+  //
+  // Parameters:
+  //   id - (integer)
+  getById: function Trenches_getById(id) {
+    return this.trenches[id];
+  },
+
+  // ---------
+  // Function: register
+  // Register a new <Trench> and returns the resulting <Trench> ID.
+  //
+  // Parameters:
+  // See the constructor <Trench.Trench>'s parameters.
+  //
+  // Returns:
+  //   id - (int) the new <Trench>'s ID.
+  register: function Trenches_register(element, xory, type, edge) {
+    var trench = new Trench(element, xory, type, edge);
+    this.trenches[trench.id] = trench;
+    return trench.id;
+  },
+
+  // ---------
+  // Function: registerWithItem
+  // Register a whole set of <Trench>es using an <Item> and returns the resulting <Trench> IDs.
+  //
+  // Parameters:
+  //   item - the <Item> to project trenches
+  //   type - either "border" or "guide"
+  //
+  // Returns:
+  //   ids - array of the new <Trench>es' IDs.
+  registerWithItem: function Trenches_registerWithItem(item, type) {
+    var container = item.container;
+    var ids = {};
+    ids.left = Trenches.register(container,"x",type,"left");
+    ids.right = Trenches.register(container,"x",type,"right");
+    ids.top = Trenches.register(container,"y",type,"top");
+    ids.bottom = Trenches.register(container,"y",type,"bottom");
+
+    this.getById(ids.left).setParentItem(item);
+    this.getById(ids.right).setParentItem(item);
+    this.getById(ids.top).setParentItem(item);
+    this.getById(ids.bottom).setParentItem(item);
+
+    return ids;
+  },
+
+  // ---------
+  // Function: unregister
+  // Unregister one or more <Trench>es.
+  //
+  // Parameters:
+  //   ids - (integer) a single <Trench> ID or (array) a list of <Trench> IDs.
+  unregister: function Trenches_unregister(ids) {
+    if (!Array.isArray(ids))
+      ids = [ids];
+    var self = this;
+    ids.forEach(function(id) {
+      self.trenches[id].hide();
+      delete self.trenches[id];
+    });
+  },
+
+  // ---------
+  // Function: activateOthersTrenches
+  // Activate all <Trench>es other than those projected by the current element.
+  //
+  // Parameters:
+  //   element - (DOMElement) the DOM element of the Item being dragged or resized.
+  activateOthersTrenches: function Trenches_activateOthersTrenches(element) {
+    this.trenches.forEach(function(t) {
+      if (t.el === element)
+        return;
+      if (t.parentItem && (t.parentItem.isAFauxItem ||
+         t.parentItem.isDragging ||
+         t.parentItem.isDropTarget))
+        return;
+      t.active = true;
+      t.calculateActiveRange();
+      t.show(); // debug
+    });
+  },
+
+  // ---------
+  // Function: disactivate
+  // After <activateOthersTrenches>, disactivates all the <Trench>es again.
+  disactivate: function Trenches_disactivate() {
+    this.trenches.forEach(function(t) {
+      t.active = false;
+      t.showGuide = false;
+      t.show();
+    });
+  },
+
+  // ---------
+  // Function: hideGuides
+  // Hide all guides (dotted lines) en masse.
+  hideGuides: function Trenches_hideGuides() {
+    this.trenches.forEach(function(t) {
+      t.showGuide = false;
+      t.show();
+    });
+  },
+
+  // ---------
+  // Function: snap
+  // Used to "snap" an object's bounds to active trenches and to the edge of the window.
+  // If the meta key is down (<Key.meta>), it will not snap but will still enforce the rect
+  // not leaving the safe bounds of the window.
+  //
+  // Parameters:
+  //   rect               - (<Rect>) the object's current bounds
+  //   stationaryCorner   - which corner is stationary? by default, the top left.
+  //                        "topleft", "bottomleft", "topright", "bottomright"
+  //   assumeConstantSize - (boolean) whether the rect's dimensions are sacred or not
+  //   keepProportional   - (boolean) if we are allowed to change the rect's size, whether the
+  //                                  dimensions should scaled proportionally or not.
+  //
+  // Returns:
+  //   (<Rect>) - the updated bounds, if they were updated
+  //   false - if the bounds were not updated
+  snap: function Trenches_snap(rect,stationaryCorner,assumeConstantSize,keepProportional) {
+    // hide all the guide trenches, because the correct ones will be turned on later.
+    Trenches.hideGuides();
+
+    var updated = false;
+    var updatedX = false;
+    var updatedY = false;
+
+    var snappedTrenches = {};
+
+    for (var i in this.trenches) {
+      var t = this.trenches[i];
+      if (!t.active || t.parentItem.isDropTarget)
+        continue;
+      // newRect will be a new rect, or false
+      var newRect = t.rectOverlaps(rect,stationaryCorner,assumeConstantSize,keepProportional);
+
+      if (newRect) { // if rectOverlaps returned an updated rect...
+
+        if (assumeConstantSize && updatedX && updatedY)
+          break;
+        if (assumeConstantSize && updatedX && (newRect.adjustedEdge == "left"||newRect.adjustedEdge == "right"))
+          continue;
+        if (assumeConstantSize && updatedY && (newRect.adjustedEdge == "top"||newRect.adjustedEdge == "bottom"))
+          continue;
+
+        rect = newRect;
+        updated = true;
+
+        // register this trench as the "snapped trench" for the appropriate edge.
+        snappedTrenches[newRect.adjustedEdge] = t;
+
+        // if updatedX, we don't need to update x any more.
+        if (newRect.adjustedEdge == "left" && this.preferLeft)
+          updatedX = true;
+        if (newRect.adjustedEdge == "right" && !this.preferLeft)
+          updatedX = true;
+
+        // if updatedY, we don't need to update x any more.
+        if (newRect.adjustedEdge == "top" && this.preferTop)
+          updatedY = true;
+        if (newRect.adjustedEdge == "bottom" && !this.preferTop)
+          updatedY = true;
+
+      }
+    }
+
+    if (updated) {
+      rect.snappedTrenches = snappedTrenches;
+      return rect;
+    }
+    return false;
+  },
+
+  // ---------
+  // Function: show
+  // <Trench.show> all <Trench>es.
+  show: function Trenches_show() {
+    this.trenches.forEach(function(t) {
+      t.show();
+    });
+  },
+
+  // ---------
+  // Function: toggleShown
+  // Toggle <Trenches.showDebug> and trigger <Trenches.show>
+  toggleShown: function Trenches_toggleShown() {
+    this.showDebug = !this.showDebug;
+    this.show();
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabview/ui.js
@@ -0,0 +1,1072 @@
+/* ***** 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 ui.js.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Ian Gilman <ian@iangilman.com>
+ * Aza Raskin <aza@mozilla.com>
+ * Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ * Ehsan Akhgari <ehsan@mozilla.com>
+ * Raymond Lee <raymond@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+// **********
+// Title: ui.js
+
+(function() {
+
+window.Keys = { meta: false };
+
+// ##########
+// Class: UIManager
+// Singleton top-level UI manager.
+var UIManager = {
+  // Variable: _frameInitalized
+  // True if the Tab View UI frame has been initialized.
+  _frameInitalized: false,
+
+  // Variable: _pageBounds
+  // Stores the page bounds.
+  _pageBounds : null,
+
+  // Variable: _closedLastVisibleTab
+  // If true, the last visible tab has just been closed in the tab strip.
+  _closedLastVisibleTab : false,
+
+  // Variable: _closedSelectedTabInTabView
+  // If true, a select tab has just been closed in TabView.
+  _closedSelectedTabInTabView : false,
+
+  // Variable: _stopZoomPreparation
+  // If true, prevent the next zoom preparation.
+  _stopZoomPreparation : false,
+
+  // Variable: _reorderTabItemsOnShow
+  // Keeps track of the <GroupItem>s which their tab items' tabs have been moved
+  // and re-orders the tab items when switching to TabView.
+  _reorderTabItemsOnShow : [],
+
+  // Variable: _reorderTabsOnHide
+  // Keeps track of the <GroupItem>s which their tab items have been moved in
+  // TabView UI and re-orders the tabs when switcing back to main browser.
+  _reorderTabsOnHide : [],
+
+  // Variable: _currentTab
+  // Keeps track of which xul:tab we are currently on.
+  // Used to facilitate zooming down from a previous tab.
+  _currentTab : null,
+
+  // ----------
+  // Function: init
+  // Must be called after the object is created.
+  init: function() {
+    try {
+      let self = this;
+
+      // ___ storage
+      Storage.init();
+      let data = Storage.readUIData(gWindow);
+      this._storageSanity(data);
+      this._pageBounds = data.pageBounds;
+
+      // ___ hook into the browser
+      gWindow.addEventListener("tabviewshow", function() {
+        self.showTabView(true);
+      }, false);
+
+      // ___ currentTab
+      this._currentTab = gBrowser.selectedTab;
+
+      // ___ Dev Menu
+      this._addDevMenu();
+
+      // When you click on the background/empty part of TabView,
+      // we create a new groupItem.
+      iQ(gTabViewFrame.contentDocument).mousedown(function(e) {
+        if (iQ(":focus").length > 0) {
+          iQ(":focus").each(function(element) {
+            if (element.nodeName == "INPUT")
+              element.blur();
+          });
+        }
+        if (e.originalTarget.id == "content")
+          self._createGroupItemOnDrag(e)
+      });
+
+      iQ(window).bind("beforeunload", function() {
+        Array.forEach(gBrowser.tabs, function(tab) {
+          tab.hidden = false;
+        });
+      });
+      iQ(window).bind("unload", function() {
+        self.uninit();
+      });
+
+      gWindow.addEventListener("tabviewhide", function() {
+        var activeTab = self.getActiveTab();
+        if (activeTab)
+          activeTab.zoomIn();
+      }, false);
+
+      // ___ setup key handlers
+      this._setTabViewFrameKeyHandlers();
+
+      // ___ add tab action handlers
+      this._addTabActionHandlers();
+
+      // ___ Storage
+
+      GroupItems.init();
+
+      var groupItemsData = Storage.readGroupItemsData(gWindow);
+      var firstTime = !groupItemsData || Utils.isEmptyObject(groupItemsData);
+      var groupItemData = Storage.readGroupItemData(gWindow);
+      GroupItems.reconstitute(groupItemsData, groupItemData);
+      GroupItems.killNewTabGroup(); // temporary?
+
+      if (firstTime) {
+        var padding = 10;
+        var infoWidth = 350;
+        var infoHeight = 350;
+        var pageBounds = Items.getPageBounds();
+        pageBounds.inset(padding, padding);
+
+        // ___ make a fresh groupItem
+        var box = new Rect(pageBounds);
+        box.width =
+          Math.min(box.width * 0.667, pageBounds.width - (infoWidth + padding));
+        box.height = box.height * 0.667;
+        var options = {
+          bounds: box
+        };
+
+        var groupItem = new GroupItem([], options);
+
+        var items = TabItems.getItems();
+        items.forEach(function(item) {
+          if (item.parent)
+            item.parent.remove(item);
+
+          groupItem.add(item);
+        });
+
+        // ___ make info item
+        var html =
+          "<div class='intro'>"
+            + "<h1>Welcome to Firefox Tab Sets</h1>" // TODO: This needs to be localized if it's kept in
+            + "<div>(more goes here)</div><br>"
+            + "<video src='http://people.mozilla.org/~araskin/movies/tabcandy_howto.webm' "
+            + "width='100%' preload controls>"
+          + "</div>";
+
+        box.left = box.right + padding;
+        box.width = infoWidth;
+        box.height = infoHeight;
+        var infoItem = new InfoItem(box);
+        infoItem.html(html);
+      }
+
+      // ___ tabs
+      TabItems.init();
+      TabItems.pausePainting();
+
+      // ___ resizing
+      if (this._pageBounds)
+        this._resize(true);
+      else
+        this._pageBounds = Items.getPageBounds();
+
+      iQ(window).resize(function() {
+        self._resize();
+      });
+
+      // ___ setup observer to save canvas images
+      var observer = {
+        observe : function(subject, topic, data) {
+          if (topic == "quit-application-requested") {
+            if (self._isTabViewVisible())
+              TabItems.saveAll(true);
+            self._save();
+          }
+        }
+      };
+      Services.obs.addObserver(observer, "quit-application-requested", false);
+
+      // ___ Done
+      this._frameInitalized = true;
+      this._save();
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  uninit: function() {
+    TabItems.uninit();
+    GroupItems.uninit();
+    Storage.uninit();
+
+    this._currentTab = null;
+    this._pageBounds = null;
+    this._reorderTabItemsOnShow = null;
+    this._reorderTabsOnHide = null;
+  },
+
+  // ----------
+  // Function: getActiveTab
+  // Returns the currently active tab as a <TabItem>
+  //
+  getActiveTab: function() {
+    return this._activeTab;
+  },
+
+  // ----------
+  // Function: setActiveTab
+  // Sets the currently active tab. The idea of a focused tab is useful
+  // for keyboard navigation and returning to the last zoomed-in tab.
+  // Hitting return/esc brings you to the focused tab, and using the
+  // arrow keys lets you navigate between open tabs.
+  //
+  // Parameters:
+  //  - Takes a <TabItem>
+  setActiveTab: function(tab) {
+    if (tab == this._activeTab)
+      return;
+
+    if (this._activeTab) {
+      this._activeTab.makeDeactive();
+      this._activeTab.removeSubscriber(this, "close");
+    }
+    this._activeTab = tab;
+
+    if (this._activeTab) {
+      var self = this;
+      this._activeTab.addSubscriber(this, "close", function() {
+        self._activeTab = null;
+      });
+
+      this._activeTab.makeActive();
+    }
+  },
+
+  // ----------
+  // Function: _isTabViewVisible
+  // Returns true if the TabView UI is currently shown.
+  _isTabViewVisible: function() {
+    return gTabViewDeck.selectedIndex == 1;
+  },
+
+  // ----------
+  // Function: showTabView
+  // Shows TabView and hides the main browser UI.
+  // Parameters:
+  //   zoomOut - true for zoom out animation, false for nothing.
+  showTabView: function(zoomOut) {
+    if (this._isTabViewVisible())
+      return;
+
+    var self = this;
+    var currentTab = this._currentTab;
+    var item = null;
+
+    this._reorderTabItemsOnShow.forEach(function(groupItem) {
+      groupItem.reorderTabItemsBasedOnTabOrder();
+    });
+    this._reorderTabItemsOnShow = [];
+
+#ifdef XP_WIN
+    // Restore the full height when showing TabView
+    gTabViewFrame.style.marginTop = 0;
+#endif
+    gTabViewDeck.selectedIndex = 1;
+    gTabViewFrame.contentWindow.focus();
+
+    gBrowser.updateTitlebar();
+#ifdef XP_MACOSX
+    this._setActiveTitleColor(true);
+#endif
+    let event = document.createEvent("Events");
+    event.initEvent("tabviewshown", true, false);
+
+    if (zoomOut && currentTab && currentTab.tabItem) {
+      item = currentTab.tabItem;
+      // If there was a previous currentTab we want to animate
+      // its thumbnail (canvas) for the zoom out.
+      // Note that we start the animation on the chrome thread.
+
+      // Zoom out!
+      item.zoomOut(function() {
+        if (!currentTab.tabItem) // if the tab's been destroyed
+          item = null;
+
+        self.setActiveTab(item);
+
+        if (item.parent) {
+          var activeGroupItem = GroupItems.getActiveGroupItem();
+          if (activeGroupItem)
+            activeGroupItem.setTopChild(item);
+        }
+
+        self._resize(true);
+        dispatchEvent(event);
+      });
+    } else
+      dispatchEvent(event);
+
+    TabItems.resumePainting();
+  },
+
+  // ----------
+  // Function: hideTabView
+  // Hides TabView and shows the main browser UI.
+  hideTabView: function() {
+    if (!this._isTabViewVisible())
+      return;
+
+    TabItems.pausePainting();
+
+    this._reorderTabsOnHide.forEach(function(groupItem) {
+      groupItem.reorderTabsBasedOnTabItemOrder();
+    });
+    this._reorderTabsOnHide = [];
+
+#ifdef XP_WIN
+    // Push the top of TabView frame to behind the tabbrowser, so glass can show
+    // XXX bug 586679: avoid shrinking the iframe and squishing iframe contents
+    // as well as avoiding the flash of black as we animate out
+    gTabViewFrame.style.marginTop = gBrowser.boxObject.y + "px";
+#endif
+    gTabViewDeck.selectedIndex = 0;
+    gBrowser.contentWindow.focus();
+
+    // set the close button on tab
+    gBrowser.tabContainer.adjustTabstrip();
+
+    gBrowser.updateTitlebar();
+#ifdef XP_MACOSX
+    this._setActiveTitleColor(false);
+#endif
+    let event = document.createEvent("Events");
+    event.initEvent("tabviewhidden", true, false);
+    dispatchEvent(event);
+  },
+
+#ifdef XP_MACOSX
+  // ----------
+  // Function: _setActiveTitleColor
+  // Used on the Mac to make the title bar match the gradient in the rest of the
+  // TabView UI.
+  //
+  // Parameters:
+  //   set - true for the special TabView color, false for the normal color.
+  _setActiveTitleColor: function(set) {
+    // Mac Only
+    var mainWindow = gWindow.document.getElementById("main-window");
+    if (set)
+      mainWindow.setAttribute("activetitlebarcolor", "#C4C4C4");
+    else
+      mainWindow.removeAttribute("activetitlebarcolor");
+  },
+#endif
+
+  // ----------
+  // Function: _addTabActionHandlers
+  // Adds handlers to handle tab actions.
+  _addTabActionHandlers: function() {
+    var self = this;
+
+    AllTabs.register("close", function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      if (self._isTabViewVisible()) {
+        // just closed the selected tab in the TabView interface.
+        if (self._currentTab == tab)
+          self._closedSelectedTabInTabView = true;
+      } else {
+        // if not closing the last tab
+        if (gBrowser.tabs.length > 1) {
+          var groupItem = GroupItems.getActiveGroupItem();
+
+          // 1) Only go back to the TabView tab when there you close the last
+          // tab of a groupItem.
+          // 2) Take care of the case where you've closed the last tab in
+          // an un-named groupItem, which means that the groupItem is gone (null) and
+          // there are no visible tabs.
+          // Can't use timeout here because user would see a flicker of
+          // switching to another tab before the TabView interface shows up.
+          if ((groupItem && groupItem._children.length == 1) ||
+              (groupItem == null && gBrowser.visibleTabs.length == 1)) {
+            // for the tab focus event to pick up.
+            self._closedLastVisibleTab = true;
+            // remove the zoom prep.
+            if (tab && tab.tabItem)
+              tab.tabItem.setZoomPrep(false);
+            self.showTabView();
+          }
+          // ToDo: When running unit tests, everything happens so quick so
+          // new tabs might be added after a tab is closing. Therefore, this
+          // hack is used. We should look for a better solution.
+          setTimeout(function() { // Marshal event from chrome thread to DOM thread
+            if ((groupItem && groupItem._children.length > 0) ||
+              (groupItem == null && gBrowser.visibleTabs.length > 0))
+              self.hideTabView();
+          }, 1);
+        }
+      }
+    });
+
+    AllTabs.register("move", function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      setTimeout(function() { // Marshal event from chrome thread to DOM thread
+        var activeGroupItem = GroupItems.getActiveGroupItem();
+        if (activeGroupItem)
+          self.setReorderTabItemsOnShow(activeGroupItem);
+      }, 1);
+    });
+
+    AllTabs.register("select", function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      self.tabOnFocus(tab);
+    });
+  },
+
+  // ----------
+  // Function: tabOnFocus
+  // Called when the user switches from one tab to another outside of the TabView UI.
+  tabOnFocus: function(tab) {
+    var self = this;
+    var focusTab = tab;
+    var currentTab = this._currentTab;
+
+    this._currentTab = focusTab;
+    // if the last visible tab has just been closed, don't show the chrome UI.
+    if (this._isTabViewVisible() &&
+        (this._closedLastVisibleTab || this._closedSelectedTabInTabView)) {
+      this._closedLastVisibleTab = false;
+      this._closedSelectedTabInTabView = false;
+      return;
+    }
+
+    // if TabView is visible but we didn't just close the last tab or
+    // selected tab, show chrome.
+    if (this._isTabViewVisible())
+      this.hideTabView();
+
+    // reset these vars, just in case.
+    this._closedLastVisibleTab = false;
+    this._closedSelectedTabInTabView = false;
+
+    setTimeout(function() { // Marshal event from chrome thread to DOM thread
+      // this value is true when TabView is open at browser startup.
+      if (self._stopZoomPreparation) {
+        self._stopZoomPreparation = false;
+        if (focusTab && focusTab.tabItem)
+          self.setActiveTab(focusTab.tabItem);
+        return;
+      }
+
+      if (focusTab != self._currentTab) {
+        // things have changed while we were in timeout
+        return;
+      }
+
+      var visibleTabCount = gBrowser.visibleTabs.length;
+
+      var newItem = null;
+      if (focusTab && focusTab.tabItem) {
+        newItem = focusTab.tabItem;
+        if (newItem.parent)
+          GroupItems.setActiveGroupItem(newItem.parent);
+        else {
+          GroupItems.setActiveGroupItem(null);
+          GroupItems.setActiveOrphanTab(newItem);
+        }
+        GroupItems.updateTabBar();
+      }
+
+      // ___ prepare for when we return to TabView
+      var oldItem = null;
+      if (currentTab && currentTab.tabItem)
+        oldItem = currentTab.tabItem;
+
+      if (newItem != oldItem) {
+        if (oldItem)
+          oldItem.setZoomPrep(false);
+
+        // if the last visible tab is removed, don't set zoom prep because
+        // we shoud be in the TabView interface.
+        if (visibleTabCount > 0 && newItem && !self._isTabViewVisible())
+          newItem.setZoomPrep(true);
+      } else {
+        // the tab is already focused so the new and old items are the
+        // same.
+        if (oldItem)
+          oldItem.setZoomPrep(!self._isTabViewVisible());
+      }
+    }, 1);
+  },
+
+  // ----------
+  // Function: setReorderTabsOnHide
+  // Sets the groupItem which the tab items' tabs should be re-ordered when
+  // switching to the main browser UI.
+  // Parameters:
+  //   groupItem - the groupItem which would be used for re-ordering tabs.
+  setReorderTabsOnHide: function(groupItem) {
+    if (this._isTabViewVisible()) {
+      var index = this._reorderTabsOnHide.indexOf(groupItem);
+      if (index == -1)
+        this._reorderTabsOnHide.push(groupItem);
+    }
+  },
+
+  // ----------
+  // Function: setReorderTabItemsOnShow
+  // Sets the groupItem which the tab items should be re-ordered when
+  // switching to the tab view UI.
+  // Parameters:
+  //   groupItem - the groupItem which would be used for re-ordering tab items.
+  setReorderTabItemsOnShow: function(groupItem) {
+    if (!this._isTabViewVisible()) {
+      var index = this._reorderTabItemsOnShow.indexOf(groupItem);
+      if (index == -1)
+        this._reorderTabItemsOnShow.push(groupItem);
+    }
+  },
+
+  // ----------
+  // Function: _setTabViewFrameKeyHandlers
+  // Sets up the key handlers for navigating between tabs within the TabView UI.
+  _setTabViewFrameKeyHandlers: function() {
+    var self = this;
+
+    iQ(window).keyup(function(event) {
+      if (!event.metaKey) window.Keys.meta = false;
+    });
+
+    iQ(window).keydown(function(event) {
+      if (event.metaKey) window.Keys.meta = true;
+
+      if (!self.getActiveTab() || iQ(":focus").length > 0) {
+        // prevent the default action when tab is pressed so it doesn't gives
+        // us problem with content focus.
+        if (event.which == 9) {
+          event.stopPropagation();
+          event.preventDefault();
+        }
+        return;
+      }
+
+      function getClosestTabBy(norm) {
+        var centers =
+          [[item.bounds.center(), item] for each(item in TabItems.getItems())];
+        var myCenter = self.getActiveTab().bounds.center();
+        var matches = centers
+          .filter(function(item){return norm(item[0], myCenter)})
+          .sort(function(a,b){
+            return myCenter.distance(a[0]) - myCenter.distance(b[0]);
+          });
+        if (matches.length > 0)
+          return matches[0][1];
+        return null;
+      }
+
+      var norm = null;
+      switch (event.which) {
+        case 39: // Right
+          norm = function(a, me){return a.x > me.x};
+          break;
+        case 37: // Left
+          norm = function(a, me){return a.x < me.x};
+          break;
+        case 40: // Down
+          norm = function(a, me){return a.y > me.y};
+          break;
+        case 38: // Up
+          norm = function(a, me){return a.y < me.y}
+          break;
+      }
+
+      if (norm != null) {
+        var nextTab = getClosestTabBy(norm);
+        if (nextTab) {
+          if (nextTab.inStack() && !nextTab.parent.expanded)
+            nextTab = nextTab.parent.getChild(0);
+          self.setActiveTab(nextTab);
+        }
+        event.stopPropagation();
+        event.preventDefault();
+      } else if (event.which == 32) {
+        // alt/control + space to zoom into the active tab.
+#ifdef XP_MACOSX
+        if (event.altKey && !event.metaKey && !event.shiftKey &&
+            !event.ctrlKey) {
+#else
+        if (event.ctrlKey && !event.metaKey && !event.shiftKey &&
+            event.altKey) {
+#endif
+          var activeTab = self.getActiveTab();
+          if (activeTab)
+            activeTab.zoomIn();
+          event.stopPropagation();
+          event.preventDefault();
+        }
+      } else if (event.which == 27 || event.which == 13) {
+        // esc or return to zoom into the active tab.
+        var activeTab = self.getActiveTab();
+        if (activeTab)
+          activeTab.zoomIn();
+        event.stopPropagation();
+        event.preventDefault();
+      } else if (event.which == 9) {
+        // tab/shift + tab to go to the next tab.
+        var activeTab = self.getActiveTab();
+        if (activeTab) {
+          var tabItems = (activeTab.parent ? activeTab.parent.getChildren() :
+                          [activeTab]);
+          var length = tabItems.length;
+          var currentIndex = tabItems.indexOf(activeTab);
+
+          if (length > 1) {
+            if (event.shiftKey) {
+              if (currentIndex == 0)
+                newIndex = (length - 1);
+              else
+                newIndex = (currentIndex - 1);
+            } else {
+              if (currentIndex == (length - 1))
+                newIndex = 0;
+              else
+                newIndex = (currentIndex + 1);
+            }
+            self.setActiveTab(tabItems[newIndex]);
+          }
+        }
+        event.stopPropagation();
+        event.preventDefault();
+      }
+    });
+  },
+
+  // ----------
+  // Function: _createGroupItemOnDrag
+  // Called in response to a mousedown in empty space in the TabView UI;
+  // creates a new groupItem based on the user's drag.
+  _createGroupItemOnDrag: function(e) {
+    const minSize = 60;
+    const minMinSize = 15;
+
+    let lastActiveGroupItem = GroupItems.getActiveGroupItem();
+    GroupItems.setActiveGroupItem(null);
+
+    var startPos = { x: e.clientX, y: e.clientY };
+    var phantom = iQ("<div>")
+      .addClass("groupItem phantom activeGroupItem")
+      .css({
+        position: "absolute",
+        opacity: .7,
+        zIndex: -1,
+        cursor: "default"
+      })
+      .appendTo("body");
+
+    var item = { // a faux-Item
+      container: phantom,
+      isAFauxItem: true,
+      bounds: {},
+      getBounds: function FauxItem_getBounds() {
+        return this.container.bounds();
+      },
+      setBounds: function FauxItem_setBounds(bounds) {
+        this.container.css(bounds);
+      },
+      setZ: function FauxItem_setZ(z) {
+        this.container.css("z-index", z);
+      },
+      setOpacity: function FauxItem_setOpacity(opacity) {
+        this.container.css("opacity", opacity);
+      },
+      // we don't need to pushAway the phantom item at the end, because
+      // when we create a new GroupItem, it'll do the actual pushAway.
+      pushAway: function () {},
+    };
+    item.setBounds(new Rect(startPos.y, startPos.x, 0, 0));
+
+    var dragOutInfo = new Drag(item, e, true); // true = isResizing
+
+    function updateSize(e) {
+      var box = new Rect();
+      box.left = Math.min(startPos.x, e.clientX);
+      box.right = Math.max(startPos.x, e.clientX);
+      box.top = Math.min(startPos.y, e.clientY);
+      box.bottom = Math.max(startPos.y, e.clientY);
+      item.setBounds(box);
+
+      // compute the stationaryCorner
+      var stationaryCorner = "";
+
+      if (startPos.y == box.top)
+        stationaryCorner += "top";
+      else
+        stationaryCorner += "bottom";
+
+      if (startPos.x == box.left)
+        stationaryCorner += "left";
+      else
+        stationaryCorner += "right";
+
+      dragOutInfo.snap(stationaryCorner, false, false); // null for ui, which we don't use anyway.
+
+      box = item.getBounds();
+      if (box.width > minMinSize && box.height > minMinSize &&
+         (box.width > minSize || box.height > minSize))
+        item.setOpacity(1);
+      else
+        item.setOpacity(0.7);
+
+      e.preventDefault();
+    }
+
+    function collapse() {
+      phantom.animate({
+        width: 0,
+        height: 0,
+        top: phantom.position().x + phantom.height()/2,
+        left: phantom.position().y + phantom.width()/2
+      }, {
+        duration: 300,
+        complete: function() {
+          phantom.remove();
+        }
+      });
+      GroupItems.setActiveGroupItem(lastActiveGroupItem);
+    }
+
+    function finalize(e) {
+      iQ(window).unbind("mousemove", updateSize);
+      dragOutInfo.stop();
+      if (phantom.css("opacity") != 1)
+        collapse();
+      else {
+        var bounds = item.getBounds();
+
+        // Add all of the orphaned tabs that are contained inside the new groupItem
+        // to that groupItem.
+        var tabs = GroupItems.getOrphanedTabs();
+        var insideTabs = [];
+        for each(tab in tabs) {
+          if (bounds.contains(tab.bounds))
+            insideTabs.push(tab);
+        }
+
+        var groupItem = new GroupItem(insideTabs,{bounds:bounds});
+        GroupItems.setActiveGroupItem(groupItem);
+        phantom.remove();
+        dragOutInfo = null;
+      }
+    }
+
+    iQ(window).mousemove(updateSize)
+    iQ(gWindow).one("mouseup", finalize);
+    e.preventDefault();
+    return false;
+  },
+
+  // ----------
+  // Function: _resize
+  // Update the TabView UI contents in response to a window size change.
+  // Won't do anything if it doesn't deem the resize necessary.
+  // Parameters:
+  //   force - true to update even when "unnecessary"; default false
+  _resize: function(force) {
+    if (typeof force == "undefined")
+      force = false;
+
+    // If TabView isn't focused and is not showing, don't perform a resize.
+    // This resize really slows things down.
+    if (!force && !this._isTabViewVisible())
+      return;
+
+    var oldPageBounds = new Rect(this._pageBounds);
+    var newPageBounds = Items.getPageBounds();
+    if (newPageBounds.equals(oldPageBounds))
+      return;
+
+    var items = Items.getTopLevelItems();
+
+    // compute itemBounds: the union of all the top-level items' bounds.
+    var itemBounds = new Rect(this._pageBounds);
+    // We start with pageBounds so that we respect the empty space the user
+    // has left on the page.
+    itemBounds.width = 1;
+    itemBounds.height = 1;
+    items.forEach(function(item) {
+      if (item.locked.bounds)
+        return;
+
+      var bounds = item.getBounds();
+      itemBounds = (itemBounds ? itemBounds.union(bounds) : new Rect(bounds));
+    });
+
+    if (newPageBounds.width < this._pageBounds.width &&
+        newPageBounds.width > itemBounds.width)
+      newPageBounds.width = this._pageBounds.width;
+
+    if (newPageBounds.height < this._pageBounds.height &&
+        newPageBounds.height > itemBounds.height)
+      newPageBounds.height = this._pageBounds.height;
+
+    var wScale;
+    var hScale;
+    if (Math.abs(newPageBounds.width - this._pageBounds.width)
+         > Math.abs(newPageBounds.height - this._pageBounds.height)) {
+      wScale = newPageBounds.width / this._pageBounds.width;
+      hScale = newPageBounds.height / itemBounds.height;
+    } else {
+      wScale = newPageBounds.width / itemBounds.width;
+      hScale = newPageBounds.height / this._pageBounds.height;
+    }
+
+    var scale = Math.min(hScale, wScale);
+    var self = this;
+    var pairs = [];
+    items.forEach(function(item) {
+      if (item.locked.bounds)
+        return;
+
+      var bounds = item.getBounds();
+      bounds.left += newPageBounds.left - self._pageBounds.left;
+      bounds.left *= scale;
+      bounds.width *= scale;
+
+      bounds.top += newPageBounds.top - self._pageBounds.top;
+      bounds.top *= scale;
+      bounds.height *= scale;
+
+      pairs.push({
+        item: item,
+        bounds: bounds
+      });
+    });
+
+    Items.unsquish(pairs);
+
+    pairs.forEach(function(pair) {
+      pair.item.setBounds(pair.bounds, true);
+      pair.item.snap();
+    });
+
+    this._pageBounds = Items.getPageBounds();
+    this._save();
+  },
+
+  // ----------
+  // Function: _addDevMenu
+  // Fills out the "dev menu" in the TabView UI.
+  _addDevMenu: function() {
+    try {
+      var self = this;
+
+      var $select = iQ("<select>")
+        .css({
+          position: "absolute",
+          bottom: 5,
+          right: 5,
+          zIndex: 99999,
+          opacity: .2
+        })
+        .appendTo("#content")
+        .change(function () {
+          var index = iQ(this).val();
+          try {
+            commands[index].code.apply(commands[index].element);
+          } catch(e) {
+            Utils.log("dev menu error", e);
+          }
+          iQ(this).val(0);
+        });
+
+      var commands = [{
+        name: "dev menu",
+        code: function() { }
+      }, {
+        name: "show trenches",
+        code: function() {
+          Trenches.toggleShown();
+          iQ(this).html((Trenches.showDebug ? "hide" : "show") + " trenches");
+        }
+      }, {
+/*
+        name: "refresh",
+        code: function() {
+          location.href = "tabview.html";
+        }
+      }, {
+        name: "reset",
+        code: function() {
+          self._reset();
+        }
+      }, {
+*/
+        name: "save",
+        code: function() {
+          self._saveAll();
+        }
+      }, {
+        name: "group sites",
+        code: function() {
+          self._arrangeBySite();
+        }
+      }];
+
+      var count = commands.length;
+      var a;
+      for (a = 0; a < count; a++) {
+        commands[a].element = (iQ("<option>")
+          .val(a)
+          .html(commands[a].name)
+          .appendTo($select))[0];
+      }
+    } catch(e) {
+      Utils.log(e);
+    }
+  },
+
+  // -----------
+  // Function: _reset
+  // Wipes all TabView storage and refreshes, giving you the "first-run" state.
+  _reset: function() {
+    Storage.wipe();
+    location.href = "";
+  },
+
+  // ----------
+  // Function: storageSanity
+  // Given storage data for this object, returns true if it looks valid.
+  _storageSanity: function(data) {
+    if (Utils.isEmptyObject(data))
+      return true;
+
+    if (!Utils.isRect(data.pageBounds)) {
+      Utils.log("UI.storageSanity: bad pageBounds", data.pageBounds);
+      data.pageBounds = null;
+      return false;
+    }
+
+    return true;
+  },
+
+  // ----------
+  // Function: _save
+  // Saves the data for this object to persistent storage
+  _save: function() {
+    if (!this._frameInitalized)
+      return;
+
+    var data = {
+      pageBounds: this._pageBounds
+    };
+
+    if (this._storageSanity(data))
+      Storage.saveUIData(gWindow, data);
+  },
+
+  // ----------
+  // Function: _saveAll
+  // Saves all data associated with TabView.
+  // TODO: Save info items
+  _saveAll: function() {
+    this._save();
+    GroupItems.saveAll();
+    TabItems.saveAll();
+  },
+
+  // ----------
+  // Function: _arrangeBySite
+  // Blows away all existing groupItems and organizes the tabs into new groupItems based
+  // on domain.
+  _arrangeBySite: function() {
+    function putInGroupItem(set, key) {
+      var groupItem = GroupItems.getGroupItemWithTitle(key);
+      if (groupItem) {
+        set.forEach(function(el) {
+          groupItem.add(el);
+        });
+      } else
+        new GroupItem(set, { dontPush: true, dontArrange: true, title: key });
+    }
+
+    GroupItems.removeAll();
+
+    var groupItems = [];
+    var leftovers = [];
+    var items = TabItems.getItems();
+    items.forEach(function(item) {
+      var url = item.tab.linkedBrowser.currentURI.spec;
+      var domain = url.split('/')[2];
+
+      if (!domain)
+        leftovers.push(item.container);
+      else {
+        var domainParts = domain.split(".");
+        var mainDomain = domainParts[domainParts.length - 2];
+        if (groupItems[mainDomain])
+          groupItems[mainDomain].push(item.container);
+        else
+          groupItems[mainDomain] = [item.container];
+      }
+    });
+
+    for (key in groupItems) {
+      var set = groupItems[key];
+      if (set.length > 1) {
+        putInGroupItem(set, key);
+      } else
+        leftovers.push(set[0]);
+    }
+
+    if (leftovers.length)
+      putInGroupItem(leftovers, "mixed");
+
+    GroupItems.arrange();
+  },
+};
+
+// ----------
+window.UI = UIManager;
+window.UI.init();
+
+})();
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -35,16 +35,18 @@
 # ***** END LICENSE BLOCK *****
 
 DEPTH		= ../../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = browser/base/content/test
 
+DIRS += tabview
+
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = \
 		test_feed_discovery.html \
 		feed_discovery.html \
 		test_bug395533.html \
 		bug395533-data.txt \
@@ -162,16 +164,20 @@ endif
                  browser_sanitize-passwordDisabledHosts.js \
                  browser_sanitize-sitepermissions.js \
                  browser_sanitize-timespans.js \
                  browser_sanitizeDialog.js \
                  browser_scope.js \
                  browser_selectTabAtIndex.js \
                  browser_tabfocus.js \
                  browser_tabs_owner.js \
+                 browser_visibleTabs.js \
+                 browser_visibleTabs_contextMenu.js \
+                 browser_visibleTabs_bookmarkAllPages.js \
+                 browser_visibleTabs_tabPreview.js \
                  discovery.html \
                  moz.png \
                  test_bug435035.html \
                  test_bug462673.html \
                  page_style_sample.html \
                  feed_tab.html \
                  plugin_unknown.html \
                  plugin_test.html \
--- a/browser/base/content/test/browser_bug380960.js
+++ b/browser/base/content/test/browser_bug380960.js
@@ -1,15 +1,20 @@
 function test() {
   gBrowser.tabContainer.addEventListener("TabOpen", tabAdded, false);
 
   var tab = gBrowser.addTab("about:blank", { skipAnimation: true });
   gBrowser.removeTab(tab);
   is(tab.parentNode, null, "tab removed immediately");
 
+  tab = gBrowser.addTab("about:blank", { skipAnimation: true });
+  gBrowser.removeTab(tab, { animate: true });
+  gBrowser.removeTab(tab);
+  is(tab.parentNode, null, "tab removed immediately when calling removeTab again after the animation was kicked off");
+
   waitForExplicitFinish();
 
   Services.prefs.setBoolPref("browser.tabs.animate", true);
   nextAsyncText();
 }
 
 function tabAdded() {
   info("tab added");
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_visibleTabs.js
@@ -0,0 +1,126 @@
+/* ***** 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 tabbrowser visibleTabs test.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Edward Lee <edilee@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+function test() {
+  // There should be one tab when we start the test
+  let [origTab] = gBrowser.visibleTabs;
+
+  // Add a tab that will get pinned
+  let pinned = gBrowser.addTab();
+  gBrowser.pinTab(pinned);
+
+  let testTab = gBrowser.addTab();
+
+  let visible = gBrowser.visibleTabs;
+  is(visible.length, 3, "3 tabs should be open");
+  is(visible[0], pinned, "the pinned tab is first");
+  is(visible[1], origTab, "original tab is next");
+  is(visible[2], testTab, "last created tab is last");
+
+  // Only show the test tab (but also get pinned and selected)
+  is(gBrowser.selectedTab, origTab, "sanity check that we're on the original tab");
+  gBrowser.showOnlyTheseTabs([testTab]);
+  is(gBrowser.visibleTabs.length, 3, "all 3 tabs are still visible");
+
+  // Select the test tab and only show that (and pinned)
+  gBrowser.selectedTab = testTab;
+  gBrowser.showOnlyTheseTabs([testTab]);
+
+  visible = gBrowser.visibleTabs;
+  is(visible.length, 2, "2 tabs should be visible including the pinned");
+  is(visible[0], pinned, "first is pinned");
+  is(visible[1], testTab, "next is the test tab");
+  is(gBrowser.tabs.length, 3, "3 tabs should still be open");
+
+  gBrowser.selectTabAtIndex(0);
+  is(gBrowser.selectedTab, pinned, "first tab is pinned");
+  gBrowser.selectTabAtIndex(1);
+  is(gBrowser.selectedTab, testTab, "second tab is the test tab");
+  gBrowser.selectTabAtIndex(2);
+  is(gBrowser.selectedTab, testTab, "no third tab, so no change");
+  gBrowser.selectTabAtIndex(0);
+  is(gBrowser.selectedTab, pinned, "switch back to the pinned");
+  gBrowser.selectTabAtIndex(2);
+  is(gBrowser.selectedTab, pinned, "no third tab, so no change");
+  gBrowser.selectTabAtIndex(-1);
+  is(gBrowser.selectedTab, testTab, "last tab is the test tab");
+
+  gBrowser.tabContainer.advanceSelectedTab(1, true);
+  is(gBrowser.selectedTab, pinned, "wrapped around the end to pinned");
+  gBrowser.tabContainer.advanceSelectedTab(1, true);
+  is(gBrowser.selectedTab, testTab, "next to test tab");
+  gBrowser.tabContainer.advanceSelectedTab(1, true);
+  is(gBrowser.selectedTab, pinned, "next to pinned again");
+
+  gBrowser.tabContainer.advanceSelectedTab(-1, true);
+  is(gBrowser.selectedTab, testTab, "going backwards to last tab");
+  gBrowser.tabContainer.advanceSelectedTab(-1, true);
+  is(gBrowser.selectedTab, pinned, "next to pinned");
+  gBrowser.tabContainer.advanceSelectedTab(-1, true);
+  is(gBrowser.selectedTab, testTab, "next to test tab again");
+
+  // Try showing all tabs
+  gBrowser.showOnlyTheseTabs(Array.slice(gBrowser.tabs));
+  is(gBrowser.visibleTabs.length, 3, "all 3 tabs are visible again");
+
+  // Select the pinned tab and show the testTab to make sure selection updates
+  gBrowser.selectedTab = pinned;
+  gBrowser.showOnlyTheseTabs([testTab]);
+  is(gBrowser.tabs[1], origTab, "make sure origTab is in the middle");
+  is(origTab.hidden, true, "make sure it's hidden");
+  gBrowser.removeTab(pinned);
+  is(gBrowser.selectedTab, testTab, "making sure origTab was skipped");
+  is(gBrowser.visibleTabs.length, 1, "only testTab is there");
+
+  // Only show one of the non-pinned tabs (but testTab is selected)
+  gBrowser.showOnlyTheseTabs([origTab]);
+  is(gBrowser.visibleTabs.length, 2, "got 2 tabs");
+
+  // Now really only show one of the tabs
+  gBrowser.showOnlyTheseTabs([testTab]);
+  visible = gBrowser.visibleTabs;
+  is(visible.length, 1, "only the original tab is visible");
+  is(visible[0], testTab, "it's the original tab");
+  is(gBrowser.tabs.length, 2, "still have 2 open tabs");
+
+  // Close the last visible tab and make sure we still get a visible tab
+  gBrowser.removeTab(testTab);
+  is(gBrowser.visibleTabs.length, 1, "only orig is left and visible");
+  is(gBrowser.tabs.length, 1, "sanity check that it matches");
+  is(gBrowser.selectedTab, origTab, "got the orig tab");
+  is(origTab.hidden, false, "and it's not hidden -- visible!");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_visibleTabs_bookmarkAllPages.js
@@ -0,0 +1,67 @@
+/* ***** 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 bookmark all pages test with tab view.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Raymond Lee <raymond@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * 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 ***** */
+
+function test() {
+  waitForExplicitFinish();
+
+  let tabOne = gBrowser.addTab("about:blank");
+  let tabTwo = gBrowser.addTab("http://mochi.test:8888/");
+  gBrowser.selectedTab = tabTwo;
+
+  let browser = gBrowser.getBrowserForTab(tabTwo);
+  let onLoad = function() {
+    browser.removeEventListener("load", onLoad, true);
+
+    gBrowser.showOnlyTheseTabs([tabTwo]);
+
+    is(gBrowser.visibleTabs.length, 1, "Only one tab is visible");
+
+    let uris = PlacesCommandHook._getUniqueTabInfo();
+    is(uris.length, 1, "Only one uri is returned");
+
+    is(uris[0].spec, tabTwo.linkedBrowser.currentURI.spec, "It's the correct URI");
+
+    gBrowser.removeTab(tabOne);
+    gBrowser.removeTab(tabTwo);
+    Array.forEach(gBrowser.tabs, function(tab) {
+      tab.hidden = false;
+    });
+
+    finish();
+  }
+  browser.addEventListener("load", onLoad, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_visibleTabs_bookmarkAllTabs.js
@@ -0,0 +1,78 @@
+/* ***** 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 tabbrowser visibleTabs Bookmark All Tabs test.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):