Merge from Tracemonkey.
authorDavid Mandelin <dmandelin@mozilla.com>
Thu, 19 Aug 2010 11:11:36 -0700
changeset 53458 2ead9f3860ddd404dfd829858988b97f5274e205
parent 53455 305b00ec07e7cf4871fdaa6fc94b832af29f8ee6 (current diff)
parent 51118 e717a555461116bb62f0d747157a818f851f4b4c (diff)
child 53459 ab92f8f395a74bf19d61bac3e7d0f1cbaf221adb
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone2.0b5pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge from Tracemonkey.
browser/components/sessionstore/test/browser/browser_480893.js
build/automation.py.in
content/base/public/nsContentUtils.h
content/base/src/nsContentUtils.cpp
content/base/src/nsFrameMessageManager.cpp
content/events/src/nsEventListenerManager.cpp
content/html/document/src/nsHTMLDocument.cpp
content/xbl/builtin/gtk2/Makefile.in
content/xbl/builtin/gtk2/jar.mn
content/xbl/builtin/gtk2/platformHTMLBindings.xml
dom/base/nsDOMClassInfo.cpp
dom/base/nsGlobalWindow.cpp
dom/base/nsJSEnvironment.cpp
js/src/Makefile.in
js/src/assembler/assembler/ARMAssembler.cpp
js/src/assembler/assembler/ARMAssembler.h
js/src/assembler/assembler/MacroAssemblerARM.cpp
js/src/assembler/assembler/MacroAssemblerARM.h
js/src/assembler/jit/ExecutableAllocator.h
js/src/assembler/wtf/Platform.h
js/src/config/autoconf.mk.in
js/src/configure.in
js/src/js.msg
js/src/jsapi.cpp
js/src/jsapi.h
js/src/jsarray.cpp
js/src/jsatom.cpp
js/src/jsatom.h
js/src/jsatominlines.h
js/src/jscntxt.h
js/src/jsdate.cpp
js/src/jsgc.cpp
js/src/jsgc.h
js/src/jsinterp.cpp
js/src/jsparse.cpp
js/src/jsparse.h
js/src/jsprvtd.h
js/src/jsstr.h
js/src/jstracer.cpp
js/src/jstypedarray.cpp
js/src/jstypes.h
js/src/jsval.h
js/src/methodjit/Compiler.cpp
js/src/shell/js.cpp
js/src/xpconnect/src/XPCNativeWrapper.cpp
js/src/xpconnect/src/XPCSafeJSObjectWrapper.cpp
js/src/xpconnect/src/dom_quickstubs.qsconf
layout/reftests/svg/smil/restart/reset-5.svg
modules/libpr0n/decoders/icon/nsIconDecoder.cpp
modules/libpr0n/decoders/icon/nsIconDecoder.h
modules/libpr0n/src/imgContainer.cpp
modules/libpr0n/src/imgContainer.h
modules/libpr0n/src/imgDiscardTracker.cpp
modules/libpr0n/src/imgDiscardTracker.h
modules/libpref/src/init/all.js
modules/plugin/base/src/nsNPAPIPlugin.cpp
modules/plugin/base/src/nsNPAPIPlugin.h
toolkit/components/search/nsIBrowserSearchService.idl
toolkit/content/tests/chrome/bug558403.logical.html
toolkit/content/tests/chrome/bug558403.visual.html
toolkit/content/tests/chrome/test_bug558403.xul
toolkit/crashreporter/google-breakpad/src/client/mac/handler/exception_handler_test.cc
toolkit/locales/en-US/chrome/mozapps/extensions/errors.dtd
toolkit/mozapps/extensions/amInstallTrigger.cpp
toolkit/mozapps/extensions/amInstallTrigger.h
toolkit/mozapps/extensions/content/errors.xul
toolkit/mozapps/extensions/test/addons/test_bug511091/icon.png
toolkit/mozapps/extensions/test/addons/test_bug511091/install.rdf
toolkit/mozapps/extensions/test/addons/test_bug542391_3/install.rdf
toolkit/mozapps/extensions/test/unit/test_bug511091.js
toolkit/mozapps/extensions/test/unit/test_bug542391.js
--- a/accessible/src/atk/nsAccessibleWrap.cpp
+++ b/accessible/src/atk/nsAccessibleWrap.cpp
@@ -843,19 +843,18 @@ AtkObject *
 getParentCB(AtkObject *aAtkObj)
 {
     if (!aAtkObj->accessible_parent) {
         nsAccessibleWrap *accWrap = GetAccessibleWrap(aAtkObj);
         if (!accWrap) {
             return nsnull;
         }
 
-        nsCOMPtr<nsIAccessible> accParent;
-        nsresult rv = accWrap->GetParent(getter_AddRefs(accParent));
-        if (NS_FAILED(rv) || !accParent)
+        nsAccessible* accParent = accWrap->GetParent();
+        if (!accParent)
             return nsnull;
 
         AtkObject *parent = nsAccessibleWrap::GetAtkObject(accParent);
         if (parent)
             atk_object_set_parent(aAtkObj, parent);
     }
     return aAtkObj->accessible_parent;
 }
@@ -863,42 +862,33 @@ getParentCB(AtkObject *aAtkObj)
 gint
 getChildCountCB(AtkObject *aAtkObj)
 {
     nsAccessibleWrap *accWrap = GetAccessibleWrap(aAtkObj);
     if (!accWrap || nsAccUtils::MustPrune(accWrap)) {
         return 0;
     }
 
-    // Links within hypertext accessible play role of accessible children in
-    // ATK since every embedded object is a link and text accessibles are
-    // ignored.
-    nsRefPtr<nsHyperTextAccessible> hyperText = do_QueryObject(accWrap);
-    return hyperText ? hyperText->GetLinkCount() : accWrap->GetChildCount();
+    return accWrap->GetEmbeddedChildCount();
 }
 
 AtkObject *
 refChildCB(AtkObject *aAtkObj, gint aChildIndex)
 {
     // aChildIndex should not be less than zero
     if (aChildIndex < 0) {
       return nsnull;
     }
 
     nsAccessibleWrap *accWrap = GetAccessibleWrap(aAtkObj);
     if (!accWrap || nsAccUtils::MustPrune(accWrap)) {
         return nsnull;
     }
 
-    // Links within hypertext accessible play role of accessible children in
-    // ATK since every embedded object is a link and text accessibles are
-    // ignored.
-    nsRefPtr<nsHyperTextAccessible> hyperText = do_QueryObject(accWrap);
-    nsAccessible* accChild = hyperText ? hyperText->GetLinkAt(aChildIndex) :
-                                         accWrap->GetChildAt(aChildIndex);
+    nsAccessible* accChild = accWrap->GetEmbeddedChildAt(aChildIndex);
     if (!accChild)
         return nsnull;
 
     AtkObject* childAtkObj = nsAccessibleWrap::GetAtkObject(accChild);
 
     NS_ASSERTION(childAtkObj, "Fail to get AtkObj");
     if (!childAtkObj)
         return nsnull;
@@ -919,22 +909,17 @@ getIndexInParentCB(AtkObject *aAtkObj)
         return -1;
     }
 
     nsAccessible *parent = accWrap->GetParent();
     if (!parent) {
         return -1; // No parent
     }
 
-    // Links within hypertext accessible play role of accessible children in
-    // ATK since every embedded object is a link and text accessibles are
-    // ignored.
-    nsRefPtr<nsHyperTextAccessible> hyperTextParent(do_QueryObject(parent));
-    return hyperTextParent ?
-        hyperTextParent->GetLinkIndex(accWrap) : accWrap->GetIndexInParent();
+    return parent->GetIndexOfEmbeddedChild(accWrap);
 }
 
 static void TranslateStates(PRUint32 aState, const AtkStateMap *aStateMap,
                             AtkStateSet *aStateSet)
 {
   NS_ASSERTION(aStateSet, "Can't pass in null state set");
 
   // Convert every state to an entry in AtkStateMap
--- a/accessible/src/atk/nsMaiInterfaceText.cpp
+++ b/accessible/src/atk/nsMaiInterfaceText.cpp
@@ -314,19 +314,22 @@ getCharacterExtentsCB(AtkText *aText, gi
     PRInt32 extWidth = 0, extHeight = 0;
 
     PRUint32 geckoCoordType;
     if (aCoords == ATK_XY_SCREEN)
         geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE;
     else
         geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE;
 
-    nsresult rv = accText->GetCharacterExtents(aOffset, &extX, &extY,
-                                               &extWidth, &extHeight,
-                                               geckoCoordType);
+#ifdef DEBUG
+    nsresult rv =
+#endif
+    accText->GetCharacterExtents(aOffset, &extX, &extY,
+                                 &extWidth, &extHeight,
+                                 geckoCoordType);
     *aX = extX;
     *aY = extY;
     *aWidth = extWidth;
     *aHeight = extHeight;
     NS_ASSERTION(NS_SUCCEEDED(rv),
                  "MaiInterfaceText::GetCharacterExtents, failed\n");
 }
 
@@ -348,20 +351,23 @@ getRangeExtentsCB(AtkText *aText, gint a
     PRInt32 extWidth = 0, extHeight = 0;
 
     PRUint32 geckoCoordType;
     if (aCoords == ATK_XY_SCREEN)
         geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE;
     else
         geckoCoordType = nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE;
 
-    nsresult rv = accText->GetRangeExtents(aStartOffset, aEndOffset,
-                                           &extX, &extY,
-                                           &extWidth, &extHeight,
-                                           geckoCoordType);
+#ifdef DEBUG
+    nsresult rv =
+#endif
+    accText->GetRangeExtents(aStartOffset, aEndOffset,
+                             &extX, &extY,
+                             &extWidth, &extHeight,
+                             geckoCoordType);
     aRect->x = extX;
     aRect->y = extY;
     aRect->width = extWidth;
     aRect->height = extHeight;
     NS_ASSERTION(NS_SUCCEEDED(rv),
                  "MaiInterfaceText::GetRangeExtents, failed\n");
 }
 
--- a/accessible/src/base/AccCollector.cpp
+++ b/accessible/src/base/AccCollector.cpp
@@ -87,32 +87,61 @@ nsAccessible*
 AccCollector::EnsureNGetObject(PRUint32 aIndex)
 {
   PRInt32 childCount = mRoot->GetChildCount();
   while (mRootChildIdx < childCount) {
     nsAccessible* child = mRoot->GetChildAt(mRootChildIdx++);
     if (!mFilterFunc(child))
       continue;
 
-    mObjects.AppendElement(child);
+    AppendObject(child);
     if (mObjects.Length() - 1 == aIndex)
       return mObjects[aIndex];
   }
 
   return nsnull;
 }
 
 PRInt32
 AccCollector::EnsureNGetIndex(nsAccessible* aAccessible)
 {
   PRInt32 childCount = mRoot->GetChildCount();
   while (mRootChildIdx < childCount) {
     nsAccessible* child = mRoot->GetChildAt(mRootChildIdx++);
     if (!mFilterFunc(child))
       continue;
 
-    mObjects.AppendElement(child);
+    AppendObject(child);
     if (child == aAccessible)
       return mObjects.Length() - 1;
   }
 
   return -1;
 }
+
+void
+AccCollector::AppendObject(nsAccessible* aAccessible)
+{
+  mObjects.AppendElement(aAccessible);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// EmbeddedObjCollector
+////////////////////////////////////////////////////////////////////////////////
+
+PRInt32
+EmbeddedObjCollector::GetIndexAt(nsAccessible *aAccessible)
+{
+  if (aAccessible->mParent != mRoot)
+    return -1;
+
+  if (aAccessible->mIndexOfEmbeddedChild != -1)
+    return aAccessible->mIndexOfEmbeddedChild;
+
+  return mFilterFunc(aAccessible) ? EnsureNGetIndex(aAccessible) : -1;
+}
+
+void
+EmbeddedObjCollector::AppendObject(nsAccessible* aAccessible)
+{
+  aAccessible->mIndexOfEmbeddedChild = mObjects.Length();
+  mObjects.AppendElement(aAccessible);
+}
--- a/accessible/src/base/AccCollector.h
+++ b/accessible/src/base/AccCollector.h
@@ -61,34 +61,61 @@ public:
   /**
    * Return an accessible from the collection at the given index.
    */
   nsAccessible* GetAccessibleAt(PRUint32 aIndex);
 
   /**
    * Return index of the given accessible within the collection.
    */
-  PRInt32 GetIndexAt(nsAccessible* aAccessible);
+  virtual PRInt32 GetIndexAt(nsAccessible* aAccessible);
 
 protected:
   /**
    * Ensure accessible at the given index is stored and return it.
    */
   nsAccessible* EnsureNGetObject(PRUint32 aIndex);
 
   /**
    * Ensure index for the given accessible is stored and return it.
    */
   PRInt32 EnsureNGetIndex(nsAccessible* aAccessible);
 
+  /**
+   * Append the object to collection.
+   */
+  virtual void AppendObject(nsAccessible* aAccessible);
+
   filters::FilterFuncPtr mFilterFunc;
   nsAccessible* mRoot;
   PRInt32 mRootChildIdx;
 
   nsTArray<nsAccessible*> mObjects;
 
 private:
   AccCollector();
   AccCollector(const AccCollector&);
   AccCollector& operator =(const AccCollector&);
 };
 
+/**
+ * Collect embedded objects. Provide quick access to accessible by index and
+ * vice versa.
+ */
+class EmbeddedObjCollector : public AccCollector
+{
+public:
+  virtual ~EmbeddedObjCollector() { };
+
+public:
+  virtual PRInt32 GetIndexAt(nsAccessible* aAccessible);
+
+protected:
+  // Make sure it's used by nsAccessible class only.
+  EmbeddedObjCollector(nsAccessible* aRoot) :
+    AccCollector(aRoot, filters::GetEmbeddedObject) { }
+
+  virtual void AppendObject(nsAccessible* aAccessible);
+
+  friend class nsAccessible;
+};
+
 #endif
--- a/accessible/src/base/nsAccEvent.cpp
+++ b/accessible/src/base/nsAccEvent.cpp
@@ -385,20 +385,24 @@ NS_IMPL_ISUPPORTS_INHERITED1(nsAccTextCh
 // node. This means we won't try to create an accessible based on the node when
 // we are ready to fire the event and so we will no longer assert at that point
 // if the node was removed from the document. Either way, the AT won't work with
 // a defunct accessible so the behaviour should be equivalent.
 // XXX revisit this when coalescence is faster (eCoalesceFromSameSubtree)
 nsAccTextChangeEvent::
   nsAccTextChangeEvent(nsAccessible *aAccessible, PRInt32 aStart,
                        nsAString& aModifiedText, PRBool aIsInserted,
-                       PRBool aIsAsynch, EIsFromUserInput aIsFromUserInput) :
-  nsAccEvent(aIsInserted ? nsIAccessibleEvent::EVENT_TEXT_INSERTED : nsIAccessibleEvent::EVENT_TEXT_REMOVED,
-             aAccessible, aIsAsynch, aIsFromUserInput, eAllowDupes),
-  mStart(aStart), mIsInserted(aIsInserted), mModifiedText(aModifiedText)
+                       PRBool aIsAsynch, EIsFromUserInput aIsFromUserInput)
+  : nsAccEvent(aIsInserted ?
+               static_cast<PRUint32>(nsIAccessibleEvent::EVENT_TEXT_INSERTED) :
+               static_cast<PRUint32>(nsIAccessibleEvent::EVENT_TEXT_REMOVED),
+               aAccessible, aIsAsynch, aIsFromUserInput, eAllowDupes)
+  , mStart(aStart)
+  , mIsInserted(aIsInserted)
+  , mModifiedText(aModifiedText)
 {
 }
 
 NS_IMETHODIMP
 nsAccTextChangeEvent::GetStart(PRInt32 *aStart)
 {
   NS_ENSURE_ARG_POINTER(aStart);
   *aStart = mStart;
--- a/accessible/src/base/nsAccessible.cpp
+++ b/accessible/src/base/nsAccessible.cpp
@@ -98,16 +98,18 @@
 #include "nsWhitespaceTokenizer.h"
 #include "nsAttrName.h"
 #include "nsNetUtil.h"
 
 #ifdef NS_DEBUG
 #include "nsIDOMCharacterData.h"
 #endif
 
+#include "mozilla/unused.h"
+
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsAccessible. nsISupports
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(nsAccessible)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsAccessible, nsAccessNode)
   NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mParent");
@@ -188,18 +190,18 @@ nsresult nsAccessible::QueryInterface(RE
     return NS_ERROR_NO_INTERFACE;
   }
 
   return nsAccessNodeWrap::QueryInterface(aIID, aInstancePtr);
 }
 
 nsAccessible::nsAccessible(nsIContent *aContent, nsIWeakReference *aShell) :
   nsAccessNodeWrap(aContent, aShell),
-  mParent(nsnull), mAreChildrenInitialized(PR_FALSE), mIndexInParent(-1),
-  mRoleMapEntry(nsnull)
+  mParent(nsnull), mIndexInParent(-1), mChildrenFlags(eChildrenUninitialized),
+  mIndexOfEmbeddedChild(-1), mRoleMapEntry(nsnull)
 {
 #ifdef NS_DEBUG_X
    {
      nsCOMPtr<nsIPresShell> shell(do_QueryReferent(aShell));
      printf(">>> %p Created Acc - DOM: %p  PS: %p", 
             (void*)static_cast<nsIAccessible*>(this), (void*)aNode,
             (void*)shell.get());
     nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
@@ -1095,17 +1097,17 @@ nsAccessible::TakeFocus()
     return NS_ERROR_FAILURE;
 
   nsIFrame *frame = GetFrame();
   NS_ENSURE_STATE(frame);
 
   nsIContent* focusContent = mContent;
 
   // If the current element can't take real DOM focus and if it has an ID and
-  // ancestor with a the aria-activedescendant attribute present, then set DOM
+  // an ancestor with an aria-activedescendant attribute present, then set DOM
   // focus to that ancestor and set aria-activedescendant on the ancestor to
   // the ID of the desired element.
   if (!frame->IsFocusable()) {
     nsAutoString id;
     if (nsCoreUtils::GetID(mContent, id)) {
 
       nsIContent* ancestorContent = mContent;
       while ((ancestorContent = ancestorContent->GetParent()) &&
@@ -2746,65 +2748,77 @@ nsAccessible::BindToParent(nsAccessible*
   mIndexInParent = aIndexInParent;
 }
 
 void
 nsAccessible::UnbindFromParent()
 {
   mParent = nsnull;
   mIndexInParent = -1;
+  mIndexOfEmbeddedChild = -1;
   mGroupInfo = nsnull;
 }
 
 void
 nsAccessible::InvalidateChildren()
 {
   PRInt32 childCount = mChildren.Length();
   for (PRInt32 childIdx = 0; childIdx < childCount; childIdx++) {
     nsAccessible* child = mChildren.ElementAt(childIdx);
     child->UnbindFromParent();
   }
 
+  mEmbeddedObjCollector = nsnull;
   mChildren.Clear();
-  mAreChildrenInitialized = PR_FALSE;
+  mChildrenFlags = eChildrenUninitialized;
 }
 
 PRBool
 nsAccessible::AppendChild(nsAccessible* aChild)
 {
   if (!mChildren.AppendElement(aChild))
     return PR_FALSE;
 
+  if (nsAccUtils::IsText(aChild))
+    mChildrenFlags = eMixedChildren;
+
   aChild->BindToParent(this, mChildren.Length() - 1);
   return PR_TRUE;
 }
 
 PRBool
 nsAccessible::InsertChildAt(PRUint32 aIndex, nsAccessible* aChild)
 {
   if (!mChildren.InsertElementAt(aIndex, aChild))
     return PR_FALSE;
 
   for (PRUint32 idx = aIndex + 1; idx < mChildren.Length(); idx++)
     mChildren[idx]->mIndexInParent++;
 
+  if (nsAccUtils::IsText(aChild))
+    mChildrenFlags = eMixedChildren;
+
+  mEmbeddedObjCollector = nsnull;
+
   aChild->BindToParent(this, aIndex);
   return PR_TRUE;
 }
 
 PRBool
 nsAccessible::RemoveChild(nsAccessible* aChild)
 {
   if (aChild->mParent != this || aChild->mIndexInParent == -1)
     return PR_FALSE;
 
   for (PRUint32 idx = aChild->mIndexInParent + 1; idx < mChildren.Length(); idx++)
     mChildren[idx]->mIndexInParent--;
 
   mChildren.RemoveElementAt(aChild->mIndexInParent);
+  mEmbeddedObjCollector = nsnull;
+
   aChild->UnbindFromParent();
   return PR_TRUE;
 }
 
 nsAccessible*
 nsAccessible::GetParent()
 {
   if (mParent)
@@ -2868,20 +2882,67 @@ nsAccessible::GetIndexOf(nsAccessible* a
   return EnsureChildren() || (aChild->mParent != this) ?
     -1 : aChild->GetIndexInParent();
 }
 
 PRInt32
 nsAccessible::GetIndexInParent()
 {
   // XXX: call GetParent() to repair the tree if it's broken.
-  nsAccessible* parent = GetParent();
+  GetParent();
   return mIndexInParent;
 }
 
+PRInt32
+nsAccessible::GetEmbeddedChildCount()
+{
+  if (EnsureChildren())
+    return -1;
+
+  if (mChildrenFlags == eMixedChildren) {
+    if (!mEmbeddedObjCollector)
+      mEmbeddedObjCollector = new EmbeddedObjCollector(this);
+    return mEmbeddedObjCollector ? mEmbeddedObjCollector->Count() : -1;
+  }
+
+  return GetChildCount();
+}
+
+nsAccessible*
+nsAccessible::GetEmbeddedChildAt(PRUint32 aIndex)
+{
+  if (EnsureChildren())
+    return nsnull;
+
+  if (mChildrenFlags == eMixedChildren) {
+    if (!mEmbeddedObjCollector)
+      mEmbeddedObjCollector = new EmbeddedObjCollector(this);
+    return mEmbeddedObjCollector ?
+      mEmbeddedObjCollector->GetAccessibleAt(aIndex) : nsnull;
+  }
+
+  return GetChildAt(aIndex);
+}
+
+PRInt32
+nsAccessible::GetIndexOfEmbeddedChild(nsAccessible* aChild)
+{
+  if (EnsureChildren())
+    return -1;
+
+  if (mChildrenFlags == eMixedChildren) {
+    if (!mEmbeddedObjCollector)
+      mEmbeddedObjCollector = new EmbeddedObjCollector(this);
+    return mEmbeddedObjCollector ?
+      mEmbeddedObjCollector->GetIndexAt(aChild) : -1;
+  }
+
+  return GetIndexOf(aChild);
+}
+
 #ifdef DEBUG
 PRBool
 nsAccessible::IsInCache()
 {
   nsDocAccessible *docAccessible =
     GetAccService()->GetDocAccessible(mContent->GetOwnerDoc());
   if (!docAccessible)
     return nsnull;
@@ -2907,17 +2968,18 @@ nsAccessible::CacheChildren()
 }
 
 void
 nsAccessible::TestChildCache(nsAccessible *aCachedChild)
 {
 #ifdef DEBUG
   PRInt32 childCount = mChildren.Length();
   if (childCount == 0) {
-    NS_ASSERTION(!mAreChildrenInitialized, "No children but initialized!");
+    NS_ASSERTION(mChildrenFlags == eChildrenUninitialized,
+                 "No children but initialized!");
     return;
   }
 
   nsAccessible *child = nsnull;
   for (PRInt32 childIdx = 0; childIdx < childCount; childIdx++) {
     child = mChildren[childIdx];
     if (child == aCachedChild)
       break;
@@ -2928,24 +2990,25 @@ nsAccessible::TestChildCache(nsAccessibl
 #endif
 }
 
 // nsAccessible public
 PRBool
 nsAccessible::EnsureChildren()
 {
   if (IsDefunct()) {
-    mAreChildrenInitialized = PR_FALSE;
+    mChildrenFlags = eChildrenUninitialized;
     return PR_TRUE;
   }
 
-  if (mAreChildrenInitialized)
+  if (mChildrenFlags != eChildrenUninitialized)
     return PR_FALSE;
 
-  mAreChildrenInitialized = PR_TRUE; // Prevent reentry
+  // State is embedded children until text leaf accessible is appended.
+  mChildrenFlags = eEmbeddedChildren; // Prevent reentry
   CacheChildren();
 
   return PR_FALSE;
 }
 
 nsAccessible*
 nsAccessible::GetSiblingAtOffset(PRInt32 aOffset, nsresult* aError)
 {
--- a/accessible/src/base/nsAccessible.h
+++ b/accessible/src/base/nsAccessible.h
@@ -48,16 +48,17 @@
 #include "nsIAccessibleRole.h"
 #include "nsIAccessibleStates.h"
 
 #include "nsStringGlue.h"
 #include "nsTArray.h"
 #include "nsRefPtrHashtable.h"
 
 class AccGroupInfo;
+class EmbeddedObjCollector;
 class nsAccessible;
 class nsAccEvent;
 struct nsRoleMapEntry;
 
 struct nsRect;
 class nsIContent;
 class nsIFrame;
 class nsIAtom;
@@ -252,31 +253,46 @@ public:
   virtual PRInt32 GetIndexInParent();
 
   /**
    * Return true if accessible has children;
    */
   PRBool HasChildren() { return !!GetChildAt(0); }
 
   /**
+   * Return embedded accessible children count.
+   */
+  PRInt32 GetEmbeddedChildCount();
+
+  /**
+   * Return embedded accessible child at the given index.
+   */
+  nsAccessible* GetEmbeddedChildAt(PRUint32 aIndex);
+
+  /**
+   * Return index of the given embedded accessible child.
+   */
+  PRInt32 GetIndexOfEmbeddedChild(nsAccessible* aChild);
+
+  /**
    * Return cached accessible of parent-child relatives.
    */
   nsAccessible* GetCachedParent() const { return mParent; }
   nsAccessible* GetCachedNextSibling() const
   {
     return mParent ?
       mParent->mChildren.SafeElementAt(mIndexInParent + 1, nsnull).get() : nsnull;
   }
   nsAccessible* GetCachedPrevSibling() const
   {
     return mParent ?
       mParent->mChildren.SafeElementAt(mIndexInParent - 1, nsnull).get() : nsnull;
   }
   PRUint32 GetCachedChildCount() const { return mChildren.Length(); }
-  PRBool AreChildrenCached() const { return mAreChildrenInitialized; }
+  PRBool AreChildrenCached() const { return mChildrenFlags != eChildrenUninitialized; }
 
 #ifdef DEBUG
   /**
    * Return true if the access node is cached.
    */
   PRBool IsInCache();
 #endif
 
@@ -438,19 +454,29 @@ protected:
    *
    * @param aEvent  the accessible event to fire.
    */
   virtual nsresult FirePlatformEvent(nsAccEvent *aEvent) = 0;
 
   // Data Members
   nsRefPtr<nsAccessible> mParent;
   nsTArray<nsRefPtr<nsAccessible> > mChildren;
-  PRBool mAreChildrenInitialized;
   PRInt32 mIndexInParent;
 
+  enum ChildrenFlags {
+    eChildrenUninitialized = 0x00,
+    eMixedChildren = 0x01,
+    eEmbeddedChildren = 0x02
+  };
+  ChildrenFlags mChildrenFlags;
+
+  nsAutoPtr<EmbeddedObjCollector> mEmbeddedObjCollector;
+  PRInt32 mIndexOfEmbeddedChild;
+  friend class EmbeddedObjCollector;
+
   nsAutoPtr<AccGroupInfo> mGroupInfo;
   friend class AccGroupInfo;
 
   nsRoleMapEntry *mRoleMapEntry; // Non-null indicates author-supplied role; possibly state & value as well
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsAccessible,
                               NS_ACCESSIBLE_IMPL_IID)
--- a/accessible/src/base/nsApplicationAccessible.h
+++ b/accessible/src/base/nsApplicationAccessible.h
@@ -1,10 +1,10 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
-/* vim:expandtab:shiftwidth=4:tabstop=4:
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=2:
  */
 /* ***** 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/
@@ -58,16 +58,18 @@
  * All the accessibility objects for toplevel windows are direct children of
  * the nsApplicationAccessible instance.
  */
 
 class nsApplicationAccessible: public nsAccessibleWrap,
                                public nsIAccessibleApplication
 {
 public:
+  using nsAccessible::GetChildAtPoint;
+
   nsApplicationAccessible();
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
 
   // nsIAccessNode
   NS_DECL_NSIACCESSNODE
 
--- a/accessible/src/base/nsBaseWidgetAccessible.h
+++ b/accessible/src/base/nsBaseWidgetAccessible.h
@@ -50,16 +50,18 @@
   */
 
 /** 
   * Leaf version of DOM Accessible -- has no children
   */
 class nsLeafAccessible : public nsAccessibleWrap
 {
 public:
+  using nsAccessible::GetChildAtPoint;
+
   nsLeafAccessible(nsIContent *aContent, nsIWeakReference *aShell);
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
 
   // nsAccessible
   virtual nsresult GetChildAtPoint(PRInt32 aX, PRInt32 aY,
                                    PRBool aDeepestChild,
--- a/accessible/src/base/nsDocAccessible.cpp
+++ b/accessible/src/base/nsDocAccessible.cpp
@@ -1705,19 +1705,21 @@ nsDocAccessible::InvalidateCacheSubtree(
   PRBool isHiding =
     aChangeType == nsIAccessibilityService::FRAME_HIDE ||
     aChangeType == nsIAccessibilityService::NODE_REMOVE;
 
   PRBool isShowing =
     aChangeType == nsIAccessibilityService::FRAME_SHOW ||
     aChangeType == nsIAccessibilityService::NODE_APPEND;
 
+#ifdef DEBUG
   PRBool isChanging =
     aChangeType == nsIAccessibilityService::NODE_SIGNIFICANT_CHANGE ||
     aChangeType == nsIAccessibilityService::FRAME_SIGNIFICANT_CHANGE;
+#endif
 
   NS_ASSERTION(isChanging || isHiding || isShowing,
                "Incorrect aChangeEventType passed in");
 
   PRBool isAsynch =
     aChangeType == nsIAccessibilityService::FRAME_HIDE ||
     aChangeType == nsIAccessibilityService::FRAME_SHOW ||
     aChangeType == nsIAccessibilityService::FRAME_SIGNIFICANT_CHANGE;
--- a/accessible/src/base/nsDocAccessible.h
+++ b/accessible/src/base/nsDocAccessible.h
@@ -77,16 +77,18 @@ class nsDocAccessible : public nsHyperTe
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsDocAccessible, nsAccessible)
 
   NS_DECL_NSIACCESSIBLEDOCUMENT
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOCACCESSIBLE_IMPL_CID)
 
   NS_DECL_NSIOBSERVER
 
 public:
+  using nsAccessible::GetParent;
+
   nsDocAccessible(nsIDocument *aDocument, nsIContent *aRootContent,
                   nsIWeakReference* aShell);
   virtual ~nsDocAccessible();
 
   // nsIAccessible
   NS_IMETHOD GetName(nsAString& aName);
   NS_IMETHOD GetDescription(nsAString& aDescription);
   NS_IMETHOD GetAttributes(nsIPersistentProperties **aAttributes);
--- a/accessible/src/base/nsOuterDocAccessible.cpp
+++ b/accessible/src/base/nsOuterDocAccessible.cpp
@@ -186,17 +186,17 @@ nsOuterDocAccessible::InvalidateChildren
   // document accessible lifetime when DOM document is created or destroyed. If
   // DOM document isn't destroyed but its presshell is destroyed (for example,
   // when DOM node of outerdoc accessible is hidden), then outerdoc accessible
   // notifies nsAccDocManager about this. If presshell is created for existing
   // DOM document (for example when DOM node of outerdoc accessible is shown)
   // then allow nsAccDocManager to handle this case since the document
   // accessible is created and appended as a child when it's requested.
 
-  mAreChildrenInitialized = PR_FALSE;
+  mChildrenFlags = eChildrenUninitialized;
 }
 
 PRBool
 nsOuterDocAccessible::AppendChild(nsAccessible *aAccessible)
 {
   NS_ASSERTION(!mChildren.Length(),
                "Previous child document of outerdoc accessible wasn't removed!");
 
--- a/accessible/src/base/nsTextAttrs.cpp
+++ b/accessible/src/base/nsTextAttrs.cpp
@@ -460,19 +460,20 @@ nsFontSizeTextAttr::Format(const nscoord
   // Convert from nscoord to pt.
   //
   // Note: according to IA2, "The conversion doesn't have to be exact.
   // The intent is to give the user a feel for the size of the text."
   // 
   // ATK does not specify a unit and will likely follow IA2 here.
   //
   // XXX todo: consider sharing this code with layout module? (bug 474621)
-  float inches = static_cast<float>(aValue) /
-    static_cast<float>(mDC->AppUnitsPerInch());
-  int pts = static_cast<int>(inches * 72 + .5); // 72 pts per inch
+  float px =
+    NSAppUnitsToFloatPixels(aValue, nsIDeviceContext::AppUnitsPerCSSPixel());
+  // Each pt is 4/3 of a CSS pixel.
+  int pts = NS_lround(px*3/4);
 
   nsAutoString value;
   value.AppendInt(pts);
   value.Append(NS_LITERAL_STRING("pt"));
   aFormattedValue = value;
 }
 
 nscoord
--- a/accessible/src/html/nsHTMLImageMapAccessible.h
+++ b/accessible/src/html/nsHTMLImageMapAccessible.h
@@ -76,18 +76,19 @@ private:
 };
 
 
 /**
  * Accessible for image map areas - must be child of image.
  */
 class nsHTMLAreaAccessible : public nsHTMLLinkAccessible
 {
+public:
+  using nsAccessible::GetChildAtPoint;
 
-public:
   nsHTMLAreaAccessible(nsIContent *aContent, nsIWeakReference *aShell);
 
   // nsIAccessible
   NS_IMETHOD GetDescription(nsAString& aDescription);
 
   NS_IMETHOD GetBounds(PRInt32 *x, PRInt32 *y, PRInt32 *width, PRInt32 *height);
 
   // nsAccessible
--- a/accessible/src/html/nsHyperTextAccessible.cpp
+++ b/accessible/src/html/nsHyperTextAccessible.cpp
@@ -1086,44 +1086,45 @@ nsHyperTextAccessible::GetTextAttributes
 
     nsCOMPtr<nsIPersistentProperties> attributes =
       do_CreateInstance(NS_PERSISTENTPROPERTIES_CONTRACTID);
     NS_ENSURE_TRUE(attributes, NS_ERROR_OUT_OF_MEMORY);
 
     NS_ADDREF(*aAttributes = attributes);
   }
 
-  PRInt32 offsetAccIdx = -1;
-  PRInt32 startOffset = 0, endOffset = 0;
-  nsAccessible *offsetAcc = GetAccessibleAtOffset(aOffset, &offsetAccIdx,
-                                                  &startOffset, &endOffset);
-  if (!offsetAcc) {
+  nsAccessible* accAtOffset = GetChildAtOffset(aOffset);
+  if (!accAtOffset) {
     // Offset 0 is correct offset when accessible has empty text. Include
     // default attributes if they were requested, otherwise return empty set.
     if (aOffset == 0) {
       if (aIncludeDefAttrs) {
         nsTextAttrsMgr textAttrsMgr(this, PR_TRUE, nsnull, -1);
         return textAttrsMgr.GetAttributes(*aAttributes);
       }
       return NS_OK;
     }
     return NS_ERROR_INVALID_ARG;
   }
 
+  PRInt32 accAtOffsetIdx = accAtOffset->GetIndexInParent();
+  PRInt32 startOffset = GetChildOffset(accAtOffsetIdx);
+  PRInt32 endOffset = GetChildOffset(accAtOffsetIdx + 1);
   PRInt32 offsetInAcc = aOffset - startOffset;
 
-  nsTextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, offsetAcc, offsetAccIdx);
+  nsTextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, accAtOffset,
+                              accAtOffsetIdx);
   nsresult rv = textAttrsMgr.GetAttributes(*aAttributes, &startOffset,
                                            &endOffset);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Compute spelling attributes on text accessible only.
-  nsIFrame *offsetFrame = offsetAcc->GetFrame();
+  nsIFrame *offsetFrame = accAtOffset->GetFrame();
   if (offsetFrame && offsetFrame->GetType() == nsAccessibilityAtoms::textFrame) {
-    nsCOMPtr<nsIDOMNode> node = offsetAcc->GetDOMNode();
+    nsCOMPtr<nsIDOMNode> node = accAtOffset->GetDOMNode();
 
     PRInt32 nodeOffset = 0;
     nsresult rv = RenderedToContentOffset(offsetFrame, offsetInAcc,
                                           &nodeOffset);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Set 'misspelled' text attribute.
     rv = GetSpellTextAttribute(node, nodeOffset, &startOffset, &endOffset,
@@ -1364,48 +1365,26 @@ nsHyperTextAccessible::GetLinkIndex(nsIA
     return NS_ERROR_FAILURE;
 
   nsRefPtr<nsAccessible> link(do_QueryObject(aLink));
   *aIndex = GetLinkIndex(link);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsHyperTextAccessible::GetLinkIndexAtOffset(PRInt32 aCharIndex,
+nsHyperTextAccessible::GetLinkIndexAtOffset(PRInt32 aOffset,
                                             PRInt32* aLinkIndex)
 {
   NS_ENSURE_ARG_POINTER(aLinkIndex);
   *aLinkIndex = -1; // API says this magic value means 'not found'
 
   if (IsDefunct())
     return NS_ERROR_FAILURE;
 
-  PRInt32 characterCount = 0;
-  PRInt32 linkIndex = 0;
-
-  PRInt32 childCount = GetChildCount();
-  for (PRInt32 childIdx = 0;
-       childIdx < childCount && characterCount <= aCharIndex; childIdx++) {
-    nsAccessible *childAcc = mChildren[childIdx];
-
-    PRUint32 role = nsAccUtils::Role(childAcc);
-    if (role == nsIAccessibleRole::ROLE_TEXT_LEAF ||
-        role == nsIAccessibleRole::ROLE_STATICTEXT) {
-      characterCount += nsAccUtils::TextLength(childAcc);
-    }
-    else {
-      if (characterCount ++ == aCharIndex) {
-        *aLinkIndex = linkIndex;
-        break;
-      }
-      if (role != nsIAccessibleRole::ROLE_WHITESPACE) {
-        ++ linkIndex;
-      }
-    }
-  }
+  *aLinkIndex = GetLinkIndexAtOffset(aOffset);
   return NS_OK;
 }
 
 /**
   * nsIAccessibleEditableText impl.
   */
 NS_IMETHODIMP nsHyperTextAccessible::SetAttributes(PRInt32 aStartPos, PRInt32 aEndPos,
                                                    nsISupports *aAttributes)
@@ -2038,17 +2017,16 @@ nsHyperTextAccessible::ScrollSubstringTo
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsAccessible public
 
 void
 nsHyperTextAccessible::InvalidateChildren()
 {
-  mLinks = nsnull;
   mOffsets.Clear();
 
   nsAccessibleWrap::InvalidateChildren();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsHyperTextAccessible public static
 
@@ -2105,80 +2083,87 @@ nsresult nsHyperTextAccessible::Rendered
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsHyperTextAccessible public
 
 PRInt32
-nsHyperTextAccessible::GetChildOffset(nsAccessible* aChild,
+nsHyperTextAccessible::GetChildOffset(PRUint32 aChildIndex,
                                       PRBool aInvalidateAfter)
 {
-  PRInt32 index = GetIndexOf(aChild);
-  if (index == -1 || index == 0)
-    return index;
+  if (aChildIndex == 0)
+    return aChildIndex;
 
-  PRInt32 count = mOffsets.Length() - index;
+  PRInt32 count = mOffsets.Length() - aChildIndex;
   if (count > 0) {
     if (aInvalidateAfter)
-      mOffsets.RemoveElementsAt(index, count);
+      mOffsets.RemoveElementsAt(aChildIndex, count);
 
-    return mOffsets[index - 1];
+    return mOffsets[aChildIndex - 1];
   }
 
   PRUint32 lastOffset = mOffsets.IsEmpty() ?
     0 : mOffsets[mOffsets.Length() - 1];
 
   EnsureChildren();
-  while (mOffsets.Length() < index) {
+  while (mOffsets.Length() < aChildIndex) {
     nsAccessible* child = mChildren[mOffsets.Length()];
     lastOffset += nsAccUtils::TextLength(child);
     mOffsets.AppendElement(lastOffset);
   }
 
-  return mOffsets[index - 1];
-}
-////////////////////////////////////////////////////////////////////////////////
-// nsHyperTextAccessible protected
-
-AccCollector*
-nsHyperTextAccessible::GetLinkCollector()
-{
-  if (IsDefunct())
-    return nsnull;
-
-  if (!mLinks)
-    mLinks = new AccCollector(this, filters::GetEmbeddedObject);
-  return mLinks;
+  return mOffsets[aChildIndex - 1];
 }
 
-nsAccessible *
-nsHyperTextAccessible::GetAccessibleAtOffset(PRInt32 aOffset, PRInt32 *aAccIdx,
-                                             PRInt32 *aStartOffset,
-                                             PRInt32 *aEndOffset)
+PRInt32
+nsHyperTextAccessible::GetChildIndexAtOffset(PRUint32 aOffset)
 {
-  PRInt32 startOffset = 0, endOffset = 0;
-  PRInt32 childCount = GetChildCount();
-  for (PRInt32 childIdx = 0; childIdx < childCount; childIdx++) {
-    nsAccessible *child = mChildren[childIdx];
-    endOffset += nsAccUtils::TextLength(child);
-    if (endOffset > aOffset) {
-      *aStartOffset = startOffset;
-      *aEndOffset = endOffset;
-      *aAccIdx = childIdx;
-      return child;
+  PRUint32 lastOffset = 0;
+  PRUint32 offsetCount = mOffsets.Length();
+  if (offsetCount > 0) {
+    lastOffset = mOffsets[offsetCount - 1];
+    if (aOffset < lastOffset) {
+      PRUint32 low = 0, high = offsetCount;
+      while (high > low) {
+        PRUint32 mid = (high + low) >> 1;
+        if (mOffsets[mid] == aOffset)
+          return mid < offsetCount - 1 ? mid + 1 : mid;
+
+        if (mOffsets[mid] < aOffset)
+          low = mid + 1;
+        else
+          high = mid;
+      }
+      if (high == offsetCount)
+        return -1;
+
+      return low;
     }
-
-    startOffset = endOffset;
   }
 
-  return nsnull;
+  PRUint32 childCount = GetChildCount();
+  while (mOffsets.Length() < childCount) {
+    nsAccessible* child = GetChildAt(mOffsets.Length());
+    lastOffset += nsAccUtils::TextLength(child);
+    mOffsets.AppendElement(lastOffset);
+    if (aOffset < lastOffset)
+      return mOffsets.Length() - 1;
+  }
+
+  if (aOffset == lastOffset)
+    return mOffsets.Length() - 1;
+
+  return -1;
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// nsHyperTextAccessible protected
+
 nsresult
 nsHyperTextAccessible::GetDOMPointByFrameOffset(nsIFrame *aFrame,
                                                 PRInt32 aOffset,
                                                 nsIAccessible *aAccessible,
                                                 nsIDOMNode **aNode,
                                                 PRInt32 *aNodeOffset)
 {
   NS_ENSURE_ARG(aAccessible);
--- a/accessible/src/html/nsHyperTextAccessible.h
+++ b/accessible/src/html/nsHyperTextAccessible.h
@@ -101,36 +101,42 @@ public:
   static nsresult RenderedToContentOffset(nsIFrame *aFrame, PRUint32 aRenderedOffset,
                                           PRInt32 *aContentOffset);
 
   /**
    * Return link count within this hypertext accessible.
    */
   inline PRUint32 GetLinkCount()
   {
-    AccCollector* links = GetLinkCollector();
-    return links ? links->Count() : 0;
+    return GetEmbeddedChildCount();
   }
 
   /**
    * Return link accessible at the given index.
    */
   inline nsAccessible* GetLinkAt(PRUint32 aIndex)
   {
-    AccCollector* links = GetLinkCollector();
-    return links ? links->GetAccessibleAt(aIndex) : nsnull;
+    return GetEmbeddedChildAt(aIndex);
   }
 
   /**
    * Return index for the given link accessible.
    */
   inline PRInt32 GetLinkIndex(nsAccessible* aLink)
   {
-    AccCollector* links = GetLinkCollector();
-    return links ? links->GetIndexAt(aLink) : -1;
+    return GetIndexOfEmbeddedChild(aLink);
+  }
+
+  /**
+   * Return link accessible at the given text offset.
+   */
+  inline PRInt32 GetLinkIndexAtOffset(PRUint32 aOffset)
+  {
+    nsAccessible* child = GetChildAtOffset(aOffset);
+    return GetLinkIndex(child);
   }
 
   /**
     * Turn a DOM Node and offset into a character offset into this hypertext.
     * Will look for closest match when the DOM node does not have an accessible
     * object associated with it. Will return an offset for the end of
     * the string if the node is not found.
     *
@@ -182,33 +188,56 @@ public:
   nsresult HypertextOffsetsToDOMRange(PRInt32 aStartHTOffset,
                                       PRInt32 aEndHTOffset,
                                       nsIDOMNode **aStartNode,
                                       PRInt32 *aStartOffset,
                                       nsIDOMNode **aEndNode,
                                       PRInt32 *aEndOffset);
 
   /**
-   * Return text offset the given child accessible of hypertext accessible.
+   * Return text offset of the given child accessible within hypertext
+   * accessible.
    *
    * @param  aChild           [in] accessible child to get text offset for
    * @param  aInvalidateAfter [in, optional] indicates whether invalidate
    *                           cached offsets for next siblings of the child
    */
   PRInt32 GetChildOffset(nsAccessible* aChild,
+                         PRBool aInvalidateAfter = PR_FALSE)
+  {
+    PRInt32 index = GetIndexOf(aChild);
+    return index == -1 ? -1 : GetChildOffset(index, aInvalidateAfter);
+  }
+
+  /**
+   * Return text offset for the child accessible index.
+   */
+  PRInt32 GetChildOffset(PRUint32 aChildIndex,
                          PRBool aInvalidateAfter = PR_FALSE);
 
+  /**
+   * Return child accessible at the given text offset.
+   *
+   * @param  aOffset  [in] the given text offset
+   */
+  PRInt32 GetChildIndexAtOffset(PRUint32 aOffset);
+
+  /**
+   * Return child accessible at the given text offset.
+   *
+   * @param  aOffset  [in] the given text offset
+   */
+  nsAccessible* GetChildAtOffset(PRUint32 aOffset)
+  {
+    return GetChildAt(GetChildIndexAtOffset(aOffset));
+  }
+
 protected:
   // nsHyperTextAccessible
 
-  /**
-   * Return link collection, create it if necessary.
-   */
-  AccCollector* GetLinkCollector();
-
   /*
    * This does the work for nsIAccessibleText::GetText[At|Before|After]Offset
    * @param aType, eGetBefore, eGetAt, eGetAfter
    * @param aBoundaryType, char/word-start/word-end/line-start/line-end/paragraph/attribute
    * @param aOffset, offset into the hypertext to start from
    * @param *aStartOffset, the resulting start offset for the returned substring
    * @param *aEndOffset, the resulting end offset for the returned substring
    * @param aText, the resulting substring
@@ -294,28 +323,16 @@ protected:
 
   /**
    * Provide the line number for the caret, relative to the
    * current DOM node.
    * @return 1-based index for the line number with the caret
    */
   PRInt32 GetCaretLineNumber();
 
-  /**
-   * Return an accessible at the given hypertext offset.
-   *
-   * @param  aOffset       [out] the given hypertext offset
-   * @param  aAccIdx       [out] child index of returned accessible
-   * @param  aStartOffset  [out] start hypertext offset of returned accessible
-   * @param  aEndOffset    [out] end hypertext offset of returned accessible
-   */
-  nsAccessible *GetAccessibleAtOffset(PRInt32 aOffset, PRInt32 *aAccIdx,
-                                      PRInt32 *aStartOffset,
-                                      PRInt32 *aEndOffset);
-
   // Helpers
   nsresult GetDOMPointByFrameOffset(nsIFrame *aFrame, PRInt32 aOffset,
                                     nsIAccessible *aAccessible,
                                     nsIDOMNode **aNode, PRInt32 *aNodeOffset);
 
   
   /**
    * Return hyper text offset for the specified bound of the given DOM range.
--- a/accessible/src/msaa/CAccessibleHypertext.cpp
+++ b/accessible/src/msaa/CAccessibleHypertext.cpp
@@ -112,24 +112,19 @@ CAccessibleHypertext::get_hyperlink(long
 }
 
 STDMETHODIMP
 CAccessibleHypertext::get_hyperlinkIndex(long aCharIndex, long *aHyperlinkIndex)
 {
 __try {
   *aHyperlinkIndex = 0;
 
-  nsCOMPtr<nsIAccessibleHyperText> hyperAcc(do_QueryObject(this));
+  nsRefPtr<nsHyperTextAccessible> hyperAcc(do_QueryObject(this));
   if (!hyperAcc)
     return E_FAIL;
 
-  PRInt32 index = 0;
-  nsresult rv = hyperAcc->GetLinkIndexAtOffset(aCharIndex, &index);
-  if (NS_FAILED(rv))
-    return GetHRESULT(rv);
-
-  *aHyperlinkIndex = index;
+  *aHyperlinkIndex = hyperAcc->GetLinkIndexAtOffset(aCharIndex);
   return S_OK;
 
 } __except(nsAccessNodeWrap::FilterA11yExceptions(::GetExceptionCode(), GetExceptionInformation())) { }
   return E_FAIL;
 }
 
--- a/accessible/src/xul/nsXULTreeAccessible.cpp
+++ b/accessible/src/xul/nsXULTreeAccessible.cpp
@@ -192,18 +192,18 @@ nsXULTreeAccessible::GetRoleInternal(PRU
 
   nsCOMPtr<nsITreeColumns> cols;
   mTree->GetColumns(getter_AddRefs(cols));
   nsCOMPtr<nsITreeColumn> primaryCol;
   if (cols)
     cols->GetPrimaryColumn(getter_AddRefs(primaryCol));
 
   *aRole = primaryCol ?
-    nsIAccessibleRole::ROLE_OUTLINE :
-    nsIAccessibleRole::ROLE_LIST;
+    static_cast<PRUint32>(nsIAccessibleRole::ROLE_OUTLINE) :
+    static_cast<PRUint32>(nsIAccessibleRole::ROLE_LIST);
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsXULTreeAccessible: nsIAccessible implementation
 
 NS_IMETHODIMP
@@ -984,18 +984,18 @@ nsXULTreeItemAccessibleBase::GetStateInt
   *aState = nsIAccessibleStates::STATE_FOCUSABLE |
     nsIAccessibleStates::STATE_SELECTABLE;
 
   // expanded/collapsed state
   if (IsExpandable()) {
     PRBool isContainerOpen;
     mTreeView->IsContainerOpen(mRow, &isContainerOpen);
     *aState |= isContainerOpen ?
-      nsIAccessibleStates::STATE_EXPANDED:
-      nsIAccessibleStates::STATE_COLLAPSED;
+      static_cast<PRUint32>(nsIAccessibleStates::STATE_EXPANDED) :
+      static_cast<PRUint32>(nsIAccessibleStates::STATE_COLLAPSED);
   }
 
   // selected state
   nsCOMPtr<nsITreeSelection> selection;
   mTreeView->GetSelection(getter_AddRefs(selection));
   if (selection) {
     PRBool isSelected;
     selection->IsSelected(mRow, &isSelected);
@@ -1173,18 +1173,18 @@ nsXULTreeItemAccessible::GetRoleInternal
   nsCOMPtr<nsITreeColumns> columns;
   mTree->GetColumns(getter_AddRefs(columns));
   NS_ENSURE_STATE(columns);
 
   nsCOMPtr<nsITreeColumn> primaryColumn;
   columns->GetPrimaryColumn(getter_AddRefs(primaryColumn));
 
   *aRole = primaryColumn ?
-    nsIAccessibleRole::ROLE_OUTLINEITEM :
-    nsIAccessibleRole::ROLE_LISTITEM;
+    static_cast<PRUint32>(nsIAccessibleRole::ROLE_OUTLINEITEM) :
+    static_cast<PRUint32>(nsIAccessibleRole::ROLE_LISTITEM);
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsXULTreeItemAccessible: nsXULTreeItemAccessibleBase implementation
 
 void
--- a/accessible/src/xul/nsXULTreeAccessible.h
+++ b/accessible/src/xul/nsXULTreeAccessible.h
@@ -59,16 +59,20 @@ const PRUint32 kDefaultTreeCacheSize = 2
   0x6176,                                               \
   0x42ee,                                               \
   { 0xb8, 0xe1, 0x2c, 0x44, 0xb0, 0x41, 0x85, 0xe3 }    \
 }
 
 class nsXULTreeAccessible : public nsXULSelectableAccessible
 {
 public:
+  using nsAccessible::GetChildCount;
+  using nsAccessible::GetChildAt;
+  using nsAccessible::GetChildAtPoint;
+
   nsXULTreeAccessible(nsIContent *aContent, nsIWeakReference *aShell);
 
   // nsISupports and cycle collection
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsXULTreeAccessible,
                                            nsAccessible)
 
   // nsIAccessible
@@ -157,16 +161,18 @@ NS_DEFINE_STATIC_IID_ACCESSOR(nsXULTreeA
   0x766a,                                             \
   0x443c,                                             \
   { 0x94, 0x0b, 0xb1, 0xe6, 0xb0, 0x83, 0x1d, 0xfc }  \
 }
 
 class nsXULTreeItemAccessibleBase : public nsAccessibleWrap
 {
 public:
+  using nsAccessible::GetParent;
+
   nsXULTreeItemAccessibleBase(nsIContent *aContent, nsIWeakReference *aShell,
                               nsAccessible *aParent, nsITreeBoxObject *aTree,
                               nsITreeView *aTreeView, PRInt32 aRow);
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
 
   // nsIAccessNode
--- a/accessible/src/xul/nsXULTreeGridAccessible.cpp
+++ b/accessible/src/xul/nsXULTreeGridAccessible.cpp
@@ -581,18 +581,18 @@ nsXULTreeGridAccessible::GetRoleInternal
   nsCOMPtr<nsITreeColumns> treeColumns;
   mTree->GetColumns(getter_AddRefs(treeColumns));
   NS_ENSURE_STATE(treeColumns);
 
   nsCOMPtr<nsITreeColumn> primaryColumn;
   treeColumns->GetPrimaryColumn(getter_AddRefs(primaryColumn));
 
   *aRole = primaryColumn ?
-    nsIAccessibleRole::ROLE_TREE_TABLE :
-    nsIAccessibleRole::ROLE_TABLE;
+    static_cast<PRUint32>(nsIAccessibleRole::ROLE_TREE_TABLE) :
+    static_cast<PRUint32>(nsIAccessibleRole::ROLE_TABLE);
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsXULTreeGridAccessible: nsXULTreeAccessible implementation
 
 already_AddRefed<nsAccessible>
--- a/accessible/src/xul/nsXULTreeGridAccessible.h
+++ b/accessible/src/xul/nsXULTreeGridAccessible.h
@@ -70,16 +70,20 @@ protected:
 
 /**
  * Represents accessible for XUL tree item in the case when XUL tree has
  * multiple columns.
  */
 class nsXULTreeGridRowAccessible : public nsXULTreeItemAccessibleBase
 {
 public:
+  using nsAccessible::GetChildCount;
+  using nsAccessible::GetChildAt;
+  using nsAccessible::GetChildAtPoint;
+
   nsXULTreeGridRowAccessible(nsIContent *aContent, nsIWeakReference *aShell,
                              nsAccessible *aParent, nsITreeBoxObject *aTree,
                              nsITreeView *aTreeView, PRInt32 aRow);
 
   // nsISupports and cycle collection
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsXULTreeGridRowAccessible,
                                            nsAccessible)
@@ -122,16 +126,18 @@ protected:
   0x4196,                                             \
   { 0xa9, 0x32, 0x4c, 0x5c, 0xa5, 0xde, 0x5d, 0xff }  \
 }
 
 class nsXULTreeGridCellAccessible : public nsLeafAccessible,
                                     public nsIAccessibleTableCell
 {
 public:
+  using nsAccessible::GetParent;
+
   nsXULTreeGridCellAccessible(nsIContent *aContent, nsIWeakReference *aShell,
                               nsXULTreeGridRowAccessible *aRowAcc,
                               nsITreeBoxObject *aTree, nsITreeView *aTreeView,
                               PRInt32 aRow, nsITreeColumn* aColumn);
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
 
--- a/accessible/tests/mochitest/attributes/test_text.html
+++ b/accessible/tests/mochitest/attributes/test_text.html
@@ -423,18 +423,22 @@
       // test zero offset on empty hypertext accessibles
       ID = "area13";
       defAttrs = buildDefaultTextAttrs(ID, "12pt");
       attrs = { };
       testTextAttrs(ID, 0, attrs, defAttrs, 0, 0);
 
       ID = "area14";
       defAttrs = buildDefaultTextAttrs(ID, kInputFontSize);
-      attrs = { };
-      testTextAttrs(ID, 0, attrs, defAttrs, 0, 0);
+
+      // XXX: While we expose text leaf accessibles for placeholder we grab its
+      // style, bug 545817.
+      // attrs = { color: "rgb(109, 109, 109)" };
+      //testTextAttrs(ID, 0, attrs, defAttrs, 0, 0);
+      todo(false, "enable commented tests when bug 545817 is fixed");
 
       //////////////////////////////////////////////////////////////////////////
       // area15, embed char tests, "*plain*plain**bold*bold*"
       ID = "area15";
       defAttrs = buildDefaultTextAttrs(ID, "12pt");
 
       // p
       testTextAttrs(ID, 0, { }, { }, 0, 1);
--- 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/accessible/tests/mochitest/test_nsIAccessibleHyperText.html
+++ b/accessible/tests/mochitest/test_nsIAccessibleHyperText.html
@@ -13,20 +13,26 @@ https://bugzilla.mozilla.org/show_bug.cg
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
 
   <script type="application/javascript"
           src="chrome://mochikit/content/a11y/accessible/common.js"></script>
 
   <script type="application/javascript">
     var gParagraphAcc;
 
+    function testLinkIndexAtOffset(aID, aOffset, aIndex)
+    {
+      var htAcc = getAccessible(aID, [nsIAccessibleHyperText]);
+      is(htAcc.getLinkIndexAtOffset(aOffset), aIndex,
+         "Wrong link index at offset " + aOffset + " for ID " + aID + "!");
+    }
+
     function testThis(aID, aCharIndex, aExpectedLinkIndex, aName)
     {
-      is(gParagraphAcc.getLinkIndexAtOffset(aCharIndex), aExpectedLinkIndex,
-         "Wrong link index at offset " + aCharIndex + " for ID " + aID + "!");
+      testLinkIndexAtOffset(gParagraphAcc, aCharIndex, aExpectedLinkIndex);
 
       var linkAcc = gParagraphAcc.getLinkAt(aExpectedLinkIndex);
       ok(linkAcc, "No accessible for link " + aID + "!");
 
       var linkIndex = gParagraphAcc.getLinkIndex(linkAcc);
       is(linkIndex, aExpectedLinkIndex, "Wrong link index for " + aID + "!");
 
       // Just test the link's name to make sure we get the right one.
@@ -90,16 +96,37 @@ https://bugzilla.mozilla.org/show_bug.cg
       for (var jdx = 0; jdx < kLinksCount; jdx++) {
         var link = htAcc.getLinkAt(jdx);
         ok(link, "No link at index " + jdx + " for 'p3'");
 
         var linkIdx = htAcc.getLinkIndex(link);
         is(linkIdx, jdx, "Wrong link index for 'p3'!");
       };
 
+      // getLinkIndexAtOffset, causes the offsets to be cached;
+      testLinkIndexAtOffset("p4", 0, 0); // 1st 'mozilla' link
+      testLinkIndexAtOffset("p4", 1, 1); // 2nd 'mozilla' link
+      testLinkIndexAtOffset("p4", 2, -1); // ' ' of ' te' text node
+      testLinkIndexAtOffset("p4", 3, -1); // 't' of ' te' text node
+      testLinkIndexAtOffset("p4", 5, -1); // 'x' of 'xt ' text node
+      testLinkIndexAtOffset("p4", 7, -1); // ' ' of 'xt ' text node
+      testLinkIndexAtOffset("p4", 8, 2); // 3d 'mozilla' link
+      testLinkIndexAtOffset("p4", 9, 2); // the end, latest link
+
+      // the second pass to make sure link indexes are calculated propertly from
+      // cached offsets.
+      testLinkIndexAtOffset("p4", 0, 0); // 1st 'mozilla' link
+      testLinkIndexAtOffset("p4", 1, 1); // 2nd 'mozilla' link
+      testLinkIndexAtOffset("p4", 2, -1); // ' ' of ' te' text node
+      testLinkIndexAtOffset("p4", 3, -1); // 't' of ' te' text node
+      testLinkIndexAtOffset("p4", 5, -1); // 'x' of 'xt ' text node
+      testLinkIndexAtOffset("p4", 7, -1); // ' ' of 'xt ' text node
+      testLinkIndexAtOffset("p4", 8, 2); // 3d 'mozilla' link
+      testLinkIndexAtOffset("p4", 9, 2); // the end, latest link
+
       SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(prepareTest);
   </script>
 </head>
 <body>
@@ -145,10 +172,11 @@ https://bugzilla.mozilla.org/show_bug.cg
   ><a id="emptyLink" href=""><img src=""></img></a><br
   >Link with embedded span<br
   ><a id="LinkWithSpan" href="http://www.heise.de/"><span lang="de">Heise Online</span></a><br
   >Named anchor, must not have "linked" state for it to be exposed correctly:<br
   ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a>
   </p>
   <p id="p2"><a href="http://mozilla.org">mozilla.org</a></p>
   <p id="p3"></p>
+  <p id="p4"><a href="www">mozilla</a><a href="www">mozilla</a><span> te</span><span>xt </span><a href="www">mozilla</a></p>
 </body>
 </html>
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.xul
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/all-studies-window.xul
@@ -91,23 +91,23 @@
                       name="extensions.testpilot.popup.showOnNewStudy"/>
           <preference id="notify-results" type="bool"
                       name="extensions.testpilot.popup.showOnNewResults"/>
           <preference id="always-submit-data" type="bool"
                       name="extensions.testpilot.alwaysSubmitData"/>
         </preferences>
         <vbox style="padding: 12px;">
           <groupbox>
-            <caption label="Data Submission" />
+            <caption label="&testpilot.settings.dataSubmission.label;" />
             <checkbox label="&testpilot.settings.alwaysSubmitData.label;"
                       preference="always-submit-data"/>
           </groupbox>
           <groupbox>
-            <caption label="Notifications" />
-            <label value="&testpilot.settings.notifyMeWhen.label;"/>
+            <caption label="&testpilot.settings.notifications.label;" />
+            <label value="&testpilot.settings.notifyWhen.label;"/>
             <hbox>
               <separator orient="vertical" />
               <vbox>
                 <checkbox label="&testpilot.settings.readyToSubmit.label;"
                           preference="notify-finished"/>
                 <checkbox label="&testpilot.settings.newStudy.label;"
                           preference="notify-new"/>
                 <checkbox label="&testpilot.settings.hasNewResults.label;"
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/feedback-browser.xul
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/feedback-browser.xul
@@ -66,16 +66,16 @@
               oncommand="TestPilotWindowUtils.openFeedbackPage(true);"/>
     <menuitem id="feedback-menu-sad-button"
               class="menuitem-iconic"
               image="chrome://testpilot-os/skin/feedback-frown-16x16.png"
               label = "&testpilot.sad.label;"
               oncommand="TestPilotWindowUtils.openFeedbackPage(false);"/>
     <menuseparator/>
     <menuitem id="feedback-menu-show-studies"
-              label="&testpilot.allStudies.label;..."
+              label="&testpilot.allYourStudies.label;"
               oncommand="TestPilotWindowUtils.openAllStudiesWindow();"/>
     <menuitem id="feedback-menu-enable-studies" 
               label="&testpilot.enable.label;"
               oncommand="TestPilotMenuUtils.togglePref('runStudies');"/>
   </menupopup>
 </menu>
 </overlay>
\ No newline at end of file
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/tp-browser.xul
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/content/tp-browser.xul
@@ -19,17 +19,17 @@
 <menu id="pilot-menu" class="menu-iconic" label="&testpilot.brand.label;"
       insertafter="addonsManager"
       image="chrome://testpilot/skin/testpilot_16x16.png">
   <menupopup id="pilot-menu-popup"
              onpopuphiding="TestPilotMenuUtils.onPopupHiding(event);">
     <menu id="pilot-notification-settings" label="&testpilot.settings.label;">
       <menupopup onpopupshowing="TestPilotMenuUtils.updateSubmenu();">
         <menuitem class="pilot-notify-me-when"
-                  label="&testpilot.settings.notifyMeWhen.label;..."
+                  label="&testpilot.settings.notifyWhen.label;"
                   disabled="true"/>
         <menuitem id="pilot-menu-notify-finished"
                   label="&testpilot.settings.readyToSubmit.label;"
                   type="checkbox"
                   oncommand="
                     TestPilotMenuUtils.togglePref('popup.showOnStudyFinished');"/>
         <menuitem id="pilot-menu-notify-new"
                   label="&testpilot.settings.newStudy.label;" type="checkbox"
@@ -43,17 +43,17 @@
         <menuseparator />
         <menuitem id="pilot-menu-always-submit-data"
                   label="&testpilot.settings.alwaysSubmitData.label;"
                   type="checkbox"
                   oncommand="
                     TestPilotMenuUtils.togglePref('alwaysSubmitData');"/>
       </menupopup>
     </menu>
-    <menuitem label="&testpilot.allStudies.label;..."
+    <menuitem label="&testpilot.allYourStudies.label;"
               oncommand="TestPilotWindowUtils.openAllStudiesWindow();"/>
     <menuitem label="&testpilot.about.label;"
               oncommand="TestPilotWindowUtils.openHomepage();"/>
   </menupopup>
 </menu>
 
 <statusbar id="status-bar">
   <statusbarpanel id="pilot-notifications-button"
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/defaults/preferences/preferences.js
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/defaults/preferences/preferences.js
@@ -1,10 +1,12 @@
 pref("extensions.testpilot.indexFileName", "index.json");
 
+pref("extensions.testpilot@labs.mozilla.com.description", "chrome://testpilot/locale/main.properties");
+
 pref("extensions.testpilot.popup.delayAfterStartup", 180000); // 3 minutes
 pref("extensions.testpilot.popup.timeBetweenChecks", 86400000); // 24 hours
 pref("extensions.testpilot.uploadRetryInterval", 3600000); // 1 hour
 
 pref("extensions.testpilot.popup.showOnNewStudy", false);
 pref("extensions.testpilot.popup.showOnStudyFinished", true);
 pref("extensions.testpilot.popup.showOnNewResults", false);
 pref("extensions.testpilot.alwaysSubmitData", false);
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/install.rdf
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/install.rdf
@@ -1,24 +1,24 @@
 <?xml version="1.0"?>
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>testpilot@labs.mozilla.com</em:id>
-    <em:version>1.0.1</em:version>
+    <em:version>1.0.2</em:version>
     <em:type>2</em:type>
 
     <!-- Target Application this extension can install into, 
          with minimum and maximum supported versions. --> 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>3.5</em:minVersion>
-        <em:maxVersion>4.0b3</em:maxVersion>
+        <em:maxVersion>4.0b4</em:maxVersion>
       </Description>
     </em:targetApplication>
    
     <!-- Front End MetaData -->
     <em:name>Feedback</em:name>
     <em:description>Help make Firefox better by giving feedback.</em:description>
     <em:creator>Mozilla Corporation</em:creator>
     <em:homepageURL>http://testpilot.mozillalabs.com/</em:homepageURL>
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/modules/tasks.js
@@ -164,28 +164,36 @@ var TestPilotTask = {
   get uploadUrl() {
     let url = Application.prefs.getValue(DATA_UPLOAD_PREF, "");
     return url + this._id;
   },
 
   // event handlers:
 
   onExperimentStartup: function TestPilotTask_onExperimentStartup() {
+    // Called when experiment is to start running (either on Firefox
+    // startup, or when study first becomes IN_PROGRESS)
   },
 
   onExperimentShutdown: function TestPilotTask_onExperimentShutdown() {
+    // Called when experiment needs to stop running (either on Firefox
+    // shutdown, or on experiment reload, or on finishing or being canceled.)
+  },
+
+  doExperimentCleanup: function TestPilotTask_onExperimentCleanup() {
+    // Called when experiment has finished or been canceled; do any cleanup
+    // of user's profile.
   },
 
   onAppStartup: function TestPilotTask_onAppStartup() {
-    // Called by extension core when startup is complete.
+    // Called by extension core when Firefox startup is complete.
   },
 
   onAppShutdown: function TestPilotTask_onAppShutdown() {
-    // TODO: not implemented - should be called when firefox is ready to
-    // shut down.
+    // Called by extension core when Firefox is shutting down.
   },
 
   onEnterPrivateBrowsing: function TestPilotTask_onEnterPrivate() {
   },
 
   onExitPrivateBrowsing: function TestPilotTask_onExitPrivate() {
   },
 
@@ -489,16 +497,23 @@ TestPilotExperiment.prototype = {
   onExperimentShutdown: function TestPilotExperiment_onShutdown() {
     this._logger.trace("Experiment.onExperimentShutdown called.");
     if (this.experimentIsRunning() && this._startedUpHandlers) {
       this._handlers.onExperimentShutdown();
       this._startedUpHandlers = false;
     }
   },
 
+  doExperimentCleanup: function TestPilotExperiment_doExperimentCleanup() {
+    if (this._handlers.doExperimentCleanup) {
+      this._logger.trace("Doing experiment cleanup.");
+      this._handlers.doExperimentCleanup();
+    }
+  },
+
   onEnterPrivateBrowsing: function TestPilotExperiment_onEnterPrivate() {
     this._logger.trace("Task is entering private browsing.");
     if (this.experimentIsRunning()) {
       this._handlers.onEnterPrivateBrowsing();
     }
   },
 
   onExitPrivateBrowsing: function TestPilotExperiment_onExitPrivate() {
@@ -628,16 +643,17 @@ TestPilotExperiment.prototype = {
     // What happens when a test finishes:
     if (this._status < TaskConstants.STATUS_FINISHED &&
 	currentDate > this._endDate) {
       let self = this;
       let setDataDeletionDate = true;
       this._logger.info("Passed End Date - Switched Task Status to Finished");
       this.changeStatus(TaskConstants.STATUS_FINISHED);
       this.onExperimentShutdown();
+      this.doExperimentCleanup();
 
       if (this._recursAutomatically) {
         this._reschedule();
         // A recurring experiment may have been set to automatically submit. If
         // so, submit now!
         if (this.recurPref == TaskConstants.ALWAYS_SUBMIT) {
           this._logger.info("Automatically Uploading Data");
           this.upload(function(success) {
@@ -795,18 +811,21 @@ TestPilotExperiment.prototype = {
     });
   },
 
   optOut: function TestPilotExperiment_optOut(reason, callback) {
     // Regardless of study ID, post the opt-out message to a special
     // database table of just opt-out messages; include study ID in metadata.
     let url = Application.prefs.getValue(DATA_UPLOAD_PREF, "") + "opt-out";
     let logger = this._logger;
+
+    this.onExperimentShutdown();
     this.changeStatus(TaskConstants.STATUS_CANCELLED);
     this._dataStore.wipeAllData();
+    this.doExperimentCleanup();
     this._dateForDataDeletion = null;
     this._expirationDateForDataSubmission = null;
     logger.info("Opting out of test with reason " + reason);
     if (reason) {
       // Send us the reason...
       // (TODO: include metadata?)
       let answer = {id: this._id,
                     reason: reason};
--- a/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback.css
+++ b/browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/win/feedback.css
@@ -0,0 +1,4 @@
+
+#pilot-notification-popup {
+  color: -moz-dialogtext;
+}
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -178,17 +178,19 @@ pref("extensions.{972ce4c6-7e08-4474-a28
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description", "chrome://browser/locale/browser.properties");
 
 pref("xpinstall.whitelist.add", "addons.mozilla.org");
 pref("xpinstall.whitelist.add.36", "getpersonas.com");
 
 pref("lightweightThemes.update.enabled", true);
 
 pref("keyword.enabled", true);
-pref("keyword.URL", "chrome://browser-region/locale/region.properties");
+// Override the default keyword.URL. Empty value means
+// "use the search service's default engine"
+pref("keyword.URL", "");
 
 pref("general.useragent.locale", "@AB_CD@");
 pref("general.skins.selectedSkin", "classic/1.0");
 pref("general.useragent.extra.firefox", "@APP_UA_NAME@/@APP_VERSION@");
 
 pref("general.smoothScroll", false);
 #ifdef UNIX_BUT_NOT_MAC
 pref("general.autoScroll", false);
--- 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="&showTabView2.label;"
+                          accesskey="&showTabView2.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-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();"/>
@@ -145,18 +146,16 @@
     <command id="Inspector:Inspect"
              oncommand="InspectorUI.toggleInspection();"/>
     <command id="Inspector:Previous"
              oncommand="InspectorUI.inspectPrevious();"
              disabled="true"/>
     <command id="Inspector:Next"
              oncommand="InspectorUI.inspectNext();"
              disabled="true"/>
-    <command id="Inspector:Style"
-             oncommand="InspectorUI.toggleStylePanel();"/>
   </commandset>
 
   <broadcasterset id="mainBroadcasterSet">
     <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
                  type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
                  oncommand="toggleSidebar('viewBookmarksSidebar');"/>
 
     <!-- for both places and non-places, the sidebar lives at
--- 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");
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-tabview.js
@@ -0,0 +1,248 @@
+# ***** 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("tabView2.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 + space
+#endif
+
+        // Don't handle this event if it's coming from a node that might allow
+        // multiple keyboard selection like selects or trees
+        let node = event.target;
+        switch (node.namespaceURI) {
+          case "http://www.w3.org/1999/xhtml":
+            // xhtml:select only allows multiple when the attr is set
+            if (node.localName == "select" && node.hasAttribute("multiple"))
+              return;
+            break;
+
+          case "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul":
+            switch (node.localName) {
+              case "listbox":
+                // xul:listbox is by default single
+                if (node.getAttribute("seltype") == "multiple")
+                  return;
+                break;
+              case "tree":
+                // xul:tree is by default multiple
+                if (node.getAttribute("seltype") != "single")
+                  return;
+                break;
+            }
+        }
+
+        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
@@ -744,17 +745,17 @@ const gXPInstallObserver = {
       });
       break;
     case "addon-install-complete":
       var notification = PopupNotifications.getNotification(notificationID, browser);
       if (notification)
         PopupNotifications.remove(notification);
 
       var needsRestart = installInfo.installs.some(function(i) {
-        return (i.addon.pendingOperations & AddonManager.PENDING_INSTALL) != 0;
+        return i.addon.pendingOperations != AddonManager.PENDING_NONE;
       });
 
       if (needsRestart) {
         messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart");
         action = {
           label: gNavigatorBundle.getString("addonInstallRestartButton"),
           accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"),
           callback: function() {
@@ -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();
 
@@ -2352,20 +2355,21 @@ function losslessDecodeURI(aURI) {
                          encodeURIComponent);
     } catch (e) {}
 
   // Encode invisible characters (line and paragraph separator,
   // object replacement character) (bug 452979)
   value = value.replace(/[\v\x0c\x1c\x1d\x1e\x1f\u2028\u2029\ufffc]/g,
                         encodeURIComponent);
 
-  // Encode default ignorable characters. (bug 546013)
+  // Encode default ignorable characters (bug 546013)
+  // except ZWNJ (U+200C) and ZWJ (U+200D) (bug 582186).
   // This includes all bidirectional formatting characters.
   // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
-  value = value.replace(/[\u00ad\u034f\u115f-\u1160\u17b4-\u17b5\u180b-\u180d\u200b-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g,
+  value = value.replace(/[\u00ad\u034f\u115f-\u1160\u17b4-\u17b5\u180b-\u180d\u200b\u200e-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g,
                         encodeURIComponent);
   return value;
 }
 
 function UpdateUrlbarSearchSplitterState()
 {
   var splitter = document.getElementById("urlbar-search-splitter");
   var urlbar = document.getElementById("urlbar-container");
@@ -6764,16 +6768,18 @@ 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("TabShow", this, true);
+    gBrowser.tabContainer.addEventListener("TabHide", this, true);
     this._updateCommandState();
   },
 
   _updateCommandState: function BATH__updateCommandState() {
     let remainingTabs = gBrowser.visibleTabs.filter(function(tab) {
       return gBrowser._removingTabs.indexOf(tab) == -1;
     });
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -97,17 +97,17 @@
 
 #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"/>
 
-# All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the 
+# 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;">
       <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;"
@@ -121,16 +121,27 @@
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
       <menuitem id="context_pinTab" label="&pinTab.label;"
                 accesskey="&pinTab.accesskey;"
                 oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true"
                 accesskey="&unpinTab.accesskey;"
                 oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
+      <menu id="context_tabViewMenu" label="&moveToGroup.label;"
+            accesskey="&moveToGroup.accesskey;">
+        <menupopup id="context_tabViewMenuPopup" 
+                   onpopupshowing="if (event.target == this) TabView.updateContextMenu(TabContextMenu.contextTab, this);">
+          <menuitem label="&moveToNewGroup.label;" 
+                    accesskey="&moveToNewGroup.accesskey;" 
+                    oncommand="TabView.moveTabTo(TabContextMenu.contextTab, null);"/>
+          <menuitem id="context_namedGroups" label="&namedGroups.label;" 
+                    disabled="true"/>
+        </menupopup>
+      </menu>
       <menuseparator/>
       <menuitem id="context_bookmarkTab"
                 label="&bookmarkThisTab.label;"
                 accesskey="&bookmarkThisTab.accesskey;"
                 oncommand="BookmarkThisTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_bookmarkAllTabs"
                 label="&bookmarkAllTabs.label;"
                 accesskey="&bookmarkAllTabs.accesskey;"
@@ -226,17 +237,16 @@
            onMozMousePixelScroll="InspectorUI.highlighter.handlePixelScroll(event);"/>
 
     <panel id="inspector-panel"
            orient="vertical"
            hidden="true"
            ignorekeys="true"
            noautofocus="true"
            noautohide="true"
-           level="top"
            titlebar="normal"
            label="&inspectPanelTitle.label;">
       <toolbar id="inspector-toolbar"
                nowindowdrag="true">
         <toolbarbutton id="inspector-inspect-toolbutton"
                        label="&inspectButton.label;"
                        accesskey="&inspectButton.accesskey;"
                        class="toolbarbutton-text"
@@ -250,17 +260,22 @@
                        label="&inspectNextButton.label;"
                        accesskey="&inspectNextButton.accesskey;"
                        class="toolbarbutton-text"
                        command="Inspector:Next"/>
         <toolbarbutton id="inspector-style-toolbutton"
                        label="&inspectStyleButton.label;"
                        accesskey="&inspectStyleButton.accesskey;"
                        class="toolbarbutton-text"
-                       command="Inspector:Style"/>
+                       oncommand="InspectorUI.toggleStylePanel();'"/>
+        <toolbarbutton id="inspector-dom-toolbutton"
+                       label="&inspectDOMButton.label;"
+                       accesskey="&inspectDOMButton.accesskey;"
+                       class="toolbarbutton-text"
+                       oncommand="InspectorUI.toggleDOMPanel();"/>
       </toolbar>
       <tree id="inspector-tree" class="plain"
             seltype="single"
             treelines="true"
             onselect="InspectorUI.onTreeSelected()"
             flex="1">
         <treecols>
           <treecol id="colNodeName" label="nodeName" primary="true"
@@ -278,23 +293,22 @@
     </panel>
 
     <panel id="inspector-style-panel"
            hidden="true"
            orient="vertical"
            ignorekeys="true"
            noautofocus="true"
            noautohide="true"
-           level="top"
            titlebar="normal"
            label="&inspectStylePanelTitle.label;">
         <listbox id="inspector-style-listbox" flex="1"/>
         <hbox align="end">
           <spacer flex="1" />
-          <resizer dir="bottomend" />
+          <resizer dir="bottomend"/>
         </hbox>
     </panel>
 
     <menupopup id="toolbar-context-menu"
                onpopupshowing="onViewToolbarsPopupShowing(event);">
       <menuseparator/>
       <menuitem command="cmd_ToggleTabsOnTop"
                 type="checkbox"
@@ -514,16 +528,17 @@
         <menu id="appmenu_developer"
               label="&developerMenu.label;">
           <menupopup id="appmenu_developer_popup">
             <menuitem id="appmenu_pageSource"
                       label="&pageSourceCmd.label;"
                       command="View:PageSource"/>
             <menuseparator/>
             <menuitem id="appmenu_pageInspect"
+                      type="checkbox"
                       label="&inspectMenu.label;"
                       command="Tools:Inspect"/>
             <menuitem id="appmenu_webConsole"
                       label="&webConsoleCmd.label;"
                       oncommand="HUDConsoleUI.toggleHUD();"/>
           </menupopup>
         </menu>
         <menuseparator/>
@@ -575,16 +590,19 @@
                   label="&quitApplicationCmd.label;"
 #endif
                   command="cmd_quitApplication"/>
       </menupopup>
     </button>
   </hbox>
 #endif
 
+<deck flex="1" id="tab-view-deck">
+<vbox flex="1">
+
   <toolbox id="navigator-toolbox"
            defaultmode="icons" mode="icons"
 #ifdef WINCE
            defaulticonsize="small" iconsize="small"
 #endif
 #ifdef XP_WIN
            tabsontop="true"
 #endif
@@ -888,17 +906,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 +940,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="&tabViewButton2.label;"
+                     command="Browser:ToggleTabView"
+                     tooltiptext="&tabViewButton2.tooltip;"
+                     removable="true"/>
+
       <toolbarbutton id="tabs-closebutton"
                      class="close-button tabs-closebutton"
                      command="cmd_close"
                      label="&closeTab.label;"
                      tooltiptext="&closeTab.label;"/>
 
     </toolbar>
 
@@ -1035,17 +1059,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 +1100,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/inspector.js
+++ b/browser/base/content/inspector.js
@@ -56,50 +56,26 @@ const INSPECTOR_INVISIBLE_ELEMENTS = {
 ///////////////////////////////////////////////////////////////////////////
 //// PanelHighlighter
 
 /**
  * A highlighter mechanism using xul panels.
  *
  * @param aBrowser
  *        The XUL browser object for the content window being highlighted.
- * @param aColor
- *        A string containing an RGB color for the panel background.
- * @param aBorderSize
- *        A number representing the border thickness of the panel.
- * @param anOpacity
- *        A number representing the alpha value of the panel background.
  */
-function PanelHighlighter(aBrowser, aColor, aBorderSize, anOpacity)
+function PanelHighlighter(aBrowser)
 {
   this.panel = document.getElementById("highlighter-panel");
   this.panel.hidden = false;
   this.browser = aBrowser;
   this.win = this.browser.contentWindow;
-  this.backgroundColor = aColor;
-  this.border = aBorderSize;
-  this.opacity = anOpacity;
-  this.updatePanelStyles();
 }
 
 PanelHighlighter.prototype = {
-  
-  /**
-   * Update the panel's style object with current settings.
-   * TODO see bugXXXXXX, https://wiki.mozilla.org/Firefox/Projects/Inspector#0.7
-   * and, https://wiki.mozilla.org/Firefox/Projects/Inspector#1.0.
-   */
-  updatePanelStyles: function PanelHighlighter_updatePanelStyles()
-  {
-    let style = this.panel.style;
-    style.backgroundColor = this.backgroundColor;
-    style.border = "solid blue " + this.border + "px";
-    style.MozBorderRadius = "4px";
-    style.opacity = this.opacity;
-  },
 
   /**
    * Highlight this.node, unhilighting first if necessary.
    *
    * @param scroll
    *        Boolean determining whether to scroll or not.
    */
   highlight: function PanelHighlighter_highlight(scroll)
@@ -512,22 +488,16 @@ InspectorTreeView.prototype = {
 ///////////////////////////////////////////////////////////////////////////
 //// InspectorUI
 
 /**
  * Main controller class for the Inspector.
  */
 var InspectorUI = {
   browser: null,
-  _showTreePanel: true,
-  _showStylePanel: true,
-  _showDOMPanel: false,
-  highlightColor: "#EEEE66",
-  highlightThickness: 4,
-  highlightOpacity: 0.4,
   selectEventsSuppressed: false,
   inspecting: false,
 
   /**
    * Toggle the inspector interface elements on or off.
    *
    * @param aEvent
    *        The event that requested the UI change. Toolbar button or menu.
@@ -554,25 +524,39 @@ var InspectorUI = {
     }
   },
 
   /**
    * Toggle the style panel. Invoked from the toolbar's Style button.
    */
   toggleStylePanel: function IUI_toggleStylePanel()
   {
-    if (this._showStylePanel) {
+    if (this.isStylePanelOpen) {
       this.stylePanel.hidePopup();
     } else {
       this.openStylePanel();
       if (this.treeView.selectedNode) {
         this.updateStylePanel(this.treeView.selectedNode);
       }
     }
-    this._showStylePanel = !this._showStylePanel;
+  },
+
+  /**
+   * Toggle the DOM panel. Invoked from the toolbar's DOM button.
+   */
+  toggleDOMPanel: function IUI_toggleDOMPanel()
+  {
+    if (this.isDOMPanelOpen) {
+      this.domPanel.hidePopup();
+    } else {
+      this.openDOMPanel();
+      if (this.treeView.selectedNode) {
+        this.updateDOMPanel(this.treeView.selectedNode);
+      }
+    }
   },
 
   /**
    * Is the tree panel open?
    *
    * @returns boolean
    */
   get isPanelOpen()
@@ -586,116 +570,141 @@ var InspectorUI = {
    * @returns boolean
    */
   get isStylePanelOpen()
   {
     return this.stylePanel && this.stylePanel.state == "open";
   },
 
   /**
+   * Is the DOM panel open?
+   *
+   * @returns boolean
+   */
+  get isDOMPanelOpen()
+  {
+    return this.domPanel && this.domPanel.state == "open";
+  },
+
+  /**
    * Open the inspector's tree panel and initialize it.
    */
   openTreePanel: function IUI_openTreePanel()
   {
     if (!this.treePanel) {
       this.treePanel = document.getElementById("inspector-panel");
       this.treePanel.hidden = false;
     }
     if (!this.isPanelOpen) {
       const panelWidthRatio = 7 / 8;
       const panelHeightRatio = 1 / 5;
       let bar = document.getElementById("status-bar");
-      this.treePanel.openPopup(bar, "overlap", 120, -120, false, false);
+      this.treePanel.openPopupAtScreen(this.win.screenX + 80,
+        this.win.outerHeight + this.win.screenY);
       this.treePanel.sizeTo(this.win.outerWidth * panelWidthRatio, 
         this.win.outerHeight * panelHeightRatio);
       this.tree = document.getElementById("inspector-tree");
       this.createDocumentModel();
     }
   },
 
   /**
    * Open the style panel if not already onscreen.
    */
   openStylePanel: function IUI_openStylePanel()
   {
-    if (!this.stylePanel) {
+    if (!this.stylePanel)
       this.stylePanel = document.getElementById("inspector-style-panel");
+    if (!this.isStylePanelOpen) {
       this.stylePanel.hidden = false;
-    }
-    if (!this.isStylePanelOpen) {
       // open at top right of browser panel, offset by 20px from top.
       this.stylePanel.openPopup(this.browser, "end_before", 0, 20, false, false);
       // size panel to 200px wide by half browser height - 60.
       this.stylePanel.sizeTo(200, this.win.outerHeight / 2 - 60);
     }
   },
 
   /**
+   * Open the DOM panel if not already onscreen.
+   */
+  openDOMPanel: function IUI_openDOMPanel()
+  {
+    if (!this.isDOMPanelOpen) {
+      // open at middle right of browser panel, offset by 20px from middle.
+      this.domPanel.openPopup(this.browser, "end_before", 0,
+        this.win.outerHeight / 2 - 20, false, false);
+      // size panel to 200px wide by half browser height - 60.
+      this.domPanel.sizeTo(200, this.win.outerHeight / 2 - 60);
+    }
+  },
+
+  /**
    * Toggle the dimmed (semi-transparent) state for a panel by setting or
    * removing a dimmed attribute.
    *
    * @param aDim
    *        The panel to be dimmed.
    */
   toggleDimForPanel: function IUI_toggleDimForPanel(aDim)
   {
     if (aDim.hasAttribute("dimmed")) {
       aDim.removeAttribute("dimmed");
     } else {
       aDim.setAttribute("dimmed", "true");
     }
   },
 
-  openDOMPanel: function IUI_openDOMPanel()
-  {
-    // # todo bug 561782
-  },
-
   /**
    * Open inspector UI. tree, style and DOM panels if enabled. Add listeners for
    * document scrolling, resize and tabContainer.TabSelect.
    */
   openInspectorUI: function IUI_openInspectorUI()
   {
     // initialization
     this.browser = gBrowser.selectedBrowser;
     this.win = this.browser.contentWindow;
-    if (!this.style) {
-      Cu.import("resource:///modules/stylePanel.jsm", this);
-      this.style.initialize();
-    }
+
+    // DOM panel initialization and loading (via PropertyPanel.jsm)
+    let domPanelTitle = this.strings.GetStringFromName("dom.domPanelTitle");
+    let parent = document.getElementById("inspector-style-panel").parentNode;
+    this.propertyPanel = new (this.PropertyPanel)(parent, document, domPanelTitle, {});
+
+    // additional DOM panel setup needed for unittest identification and use
+    this.domPanel = this.propertyPanel.panel;
+    this.domPanel.setAttribute("id", "inspector-dom-panel");
+    this.domBox = this.propertyPanel.tree;
+    this.domTreeView = this.propertyPanel.treeView;
 
     // open inspector UI
-    if (this._showTreePanel) {
-      this.openTreePanel();
-    }
-    if (this._showStylePanel) {
-      this.styleBox = document.getElementById("inspector-style-listbox");
-      this.clearStylePanel();
-      this.openStylePanel();
-    }
-    if (this._showDOMPanel) {
-      this.openDOMPanel();
-    }
-    this.inspectorBundle = Services.strings.createBundle("chrome://browser/locale/inspector.properties");
+    this.openTreePanel();
+
+    // style panel setup and activation
+    this.styleBox = document.getElementById("inspector-style-listbox");
+    this.clearStylePanel();
+    this.openStylePanel();
+
+    // DOM panel setup and activation
+    this.clearDOMPanel();
+    this.openDOMPanel();
+
+    // setup highlighter and start inspecting
     this.initializeHighlighter();
     this.startInspecting();
     this.win.document.addEventListener("scroll", this, false);
     this.win.addEventListener("resize", this, false);
     gBrowser.tabContainer.addEventListener("TabSelect", this, false);
     this.inspectCmd.setAttribute("checked", true);
   },
 
   /**
    * Initialize highlighter.
    */
   initializeHighlighter: function IUI_initializeHighlighter()
   {
-    this.highlighter = new PanelHighlighter(this.browser, this.highlightColor,
-      this.highlightThickness, this.highlightOpacity);
+    this.highlighter = new PanelHighlighter(this.browser);
   },
 
   /**
    * Close inspector UI and associated panels. Unhighlight and stop inspecting.
    * Remove event listeners for document scrolling, resize and
    * tabContainer.TabSelect.
    */
   closeInspectorUI: function IUI_closeInspectorUI()
@@ -709,71 +718,80 @@ var InspectorUI = {
     }
     if (this.isPanelOpen) {
       this.treePanel.hidePopup();
       this.treeView.destroy();
     }
     if (this.isStylePanelOpen) {
       this.stylePanel.hidePopup();
     }
+    if (this.domPanel) {
+      this.domPanel.hidePopup();
+      this.domBox = null;
+      this.domTreeView = null;
+      this.propertyPanel.destroy();
+    }
     this.inspectCmd.setAttribute("checked", false);
     this.browser = this.win = null; // null out references to browser and window
   },
 
   /**
    * Begin inspecting webpage, attach page event listeners, activate
    * highlighter event listeners.
    */
   startInspecting: function IUI_startInspecting()
   {
     this.attachPageListeners();
     this.inspecting = true;
     this.toggleDimForPanel(this.stylePanel);
+    this.toggleDimForPanel(this.domPanel);
   },
 
   /**
    * Stop inspecting webpage, detach page listeners, disable highlighter
    * event listeners.
    */
   stopInspecting: function IUI_stopInspecting()
   {
     if (!this.inspecting)
       return;
     this.detachPageListeners();
     this.inspecting = false;
     this.toggleDimForPanel(this.stylePanel);
+    this.toggleDimForPanel(this.domPanel);
     if (this.treeView.selection) {
       this.updateStylePanel(this.treeView.selectedNode);
+      this.updateDOMPanel(this.treeView.selectedNode);
     }
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Model Creation Methods
 
   /**
    * Create treeView object from content window.
    */
   createDocumentModel: function IUI_createDocumentModel()
   {
     this.treeView = new InspectorTreeView(this.win);
   },
 
   /**
-   * add a new item to the listbox
+   * Add a new item to the style panel listbox.
    *
    * @param aLabel
    *        A bit of text to put in the listitem's label attribute.
    * @param aType
    *        The type of item.
    * @param content
    *        Text content or value of the listitem.
    */
   addStyleItem: function IUI_addStyleItem(aLabel, aType, aContent)
   {
-    let itemLabelString = this.inspectorBundle.GetStringFromName("style.styleItemLabel");
+    let itemLabelString = this.strings.GetStringFromName("style.styleItemLabel");
     let item = document.createElement("listitem");
 
     // Do not localize these strings
     let label = aLabel;
     item.className = "style-" + aType;
     if (aContent) {
       label = itemLabelString.replace("#1", aLabel);
       label = label.replace("#2", aContent);
@@ -786,17 +804,17 @@ var InspectorUI = {
   /**
    * Create items for each rule included in the given array.
    *
    * @param aRules
    *        an array of rule objects
    */
   createStyleRuleItems: function IUI_createStyleRuleItems(aRules)
   {
-    let selectorLabel = this.inspectorBundle.GetStringFromName("style.selectorLabel");
+    let selectorLabel = this.strings.GetStringFromName("style.selectorLabel");
 
     aRules.forEach(function(rule) {
       this.addStyleItem(selectorLabel, "selector", rule.id);
       rule.properties.forEach(function(property) {
         if (property.overridden)
           return; // property marked overridden elsewhere
         // Do not localize the strings below this line
         let important = "";
@@ -816,17 +834,17 @@ var InspectorUI = {
    * @param aSections
    *        Array of sections encapsulating the inherited rules for selectors
    *        and elements.
    */
   createStyleItems: function IUI_createStyleItems(aRules, aSections)
   {
     this.createStyleRuleItems(aRules);
     let inheritedString = 
-        this.inspectorBundle.GetStringFromName("style.inheritedFrom");
+        this.strings.GetStringFromName("style.inheritedFrom");
     aSections.forEach(function(section) {
       let sectionTitle = section.element.tagName;
       if (section.element.id)
         sectionTitle += "#" + section.element.id;
       let replacedString = inheritedString.replace("#1", sectionTitle);
       this.addStyleItem(replacedString, "section");
       this.createStyleRuleItems(section.rules);
     }, this);
@@ -837,33 +855,56 @@ var InspectorUI = {
    */
   clearStylePanel: function IUI_clearStylePanel()
   {
     for (let i = this.styleBox.childElementCount; i >= 0; --i)
       this.styleBox.removeItemAt(i);
   },
 
   /**
+   * Remove all items from the DOM Panel's listbox.
+   */
+  clearDOMPanel: function IUI_clearStylePanel()
+  {
+    this.domTreeView.data = {};
+  },
+
+  /**
    * Update the contents of the style panel with styles for the currently
    * inspected node.
    *
    * @param aNode
    *        The highlighted node to get styles for.
    */
   updateStylePanel: function IUI_updateStylePanel(aNode)
   {
-    if (this.inspecting || !this.isStylePanelOpen)
+    if (this.inspecting || !this.isStylePanelOpen) {
       return;
+    }
+
     let rules = [], styleSections = [], usedProperties = {};
     this.style.getInheritedRules(aNode, styleSections, usedProperties);
     this.style.getElementRules(aNode, rules, usedProperties);
     this.clearStylePanel();
     this.createStyleItems(rules, styleSections);
   },
 
+  /**
+   * Update the contents of the DOM panel with name/value pairs for the
+   * currently-inspected node.
+   */
+  updateDOMPanel: function IUI_updateDOMPanel(aNode)
+  {
+    if (this.inspecting || !this.isDOMPanelOpen) {
+      return;
+    }
+
+    this.domTreeView.data = aNode;
+  },
+
   /////////////////////////////////////////////////////////////////////////
   //// Event Handling
 
   /**
    * Main callback handler for events.
    *
    * @param event
    *        The event to be handled.
@@ -903,21 +944,21 @@ var InspectorUI = {
    * Event fired when a tree row is selected in the tree panel.
    */
   onTreeSelected: function IUI_onTreeSelected()
   {
     if (this.selectEventsSuppressed) {
       return false;
     }
 
-    let treeView = this.treeView;
-    let node = treeView.selectedNode;
+    let node = this.treeView.selectedNode;
     this.highlighter.highlightNode(node);
     this.stopInspecting();
     this.updateStylePanel(node);
+    this.updateDOMPanel(node);
     return true;
   },
 
   /**
    * Attach event listeners to content window and child windows to enable 
    * highlighting and click to stop inspection.
    */
   attachPageListeners: function IUI_attachPageListeners()
@@ -950,16 +991,17 @@ var InspectorUI = {
    */
   inspectNode: function IUI_inspectNode(aNode)
   {
     this.highlighter.highlightNode(aNode);
     this.selectEventsSuppressed = true;
     this.treeView.selectedNode = aNode;
     this.selectEventsSuppressed = false;
     this.updateStylePanel(aNode);
+    this.updateDOMPanel(aNode);
   },
 
   /**
    * Find an element from the given coordinates. This method descends through 
    * frames to find the element the user clicked inside frames.
    *
    * @param DOMDocument aDocument the document to look into.
    * @param integer aX
@@ -995,12 +1037,38 @@ var InspectorUI = {
    *        text message to send to the log
    */
   _log: function LOG(msg)
   {
     Services.console.logStringMessage(msg);
   },
 }
 
+/////////////////////////////////////////////////////////////////////////
+//// Initializors
+
 XPCOMUtils.defineLazyGetter(InspectorUI, "inspectCmd", function () {
   return document.getElementById("Tools:Inspect");
 });
 
+XPCOMUtils.defineLazyGetter(InspectorUI, "strings", function () {
+  return Services.strings.createBundle("chrome://browser/locale/inspector.properties");
+});
+
+XPCOMUtils.defineLazyGetter(InspectorUI, "PropertyTreeView", function () {
+  var obj = {};
+  Cu.import("resource://gre/modules/PropertyPanel.jsm", obj);
+  return obj.PropertyTreeView;
+});
+
+XPCOMUtils.defineLazyGetter(InspectorUI, "PropertyPanel", function () {
+  var obj = {};
+  Cu.import("resource://gre/modules/PropertyPanel.jsm", obj);
+  return obj.PropertyPanel;
+});
+
+XPCOMUtils.defineLazyGetter(InspectorUI, "style", function () {
+  var obj = {};
+  Cu.import("resource:///modules/stylePanel.jsm", obj);
+  obj.style.initialize();
+  return obj.style;
+});
+
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -83,18 +83,23 @@
                 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);"/>
+      <property name="visibleTabs" readonly="true">
+        <getter><![CDATA[
+          return Array.filter(this.tabs, function(tab) {
+            return !tab.hidden && this._removingTabs.indexOf(tab) == -1;
+          }, this);
+        ]]></getter>
+      </property>
       <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>
@@ -505,19 +510,22 @@
 
                 // Don't clear the favicon if this onLocationChange was
                 // triggered by a pushState or a replaceState.  See bug 550565.
                 if (aWebProgress.isLoadingDocument &&
                     !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE))
                   this.mBrowser.mIconURL = null;
 
                 let browserHistory = this.mTabBrowser.mBrowserHistory;
-                if (this.mBrowser.lastURI)
-                  browserHistory.unregisterOpenPage(this.mBrowser.lastURI);
+                if (this.mBrowser.registeredOpenURI) {
+                  browserHistory.unregisterOpenPage(this.mBrowser.registeredOpenURI);
+                  delete this.mBrowser.registeredOpenURI;
+                }
                 browserHistory.registerOpenPage(aLocation);
+                this.mBrowser.registeredOpenURI = aLocation;
               }
 
               if (!this.mBlank) {
                 this._callProgressListeners("onLocationChange",
                                             [aWebProgress, aRequest, aLocation]);
               }
 
               if (topLevel)
@@ -718,17 +726,23 @@
             return newTitle;
           ]]>
         </body>
       </method>
 
       <method name="updateTitlebar">
         <body>
           <![CDATA[
-            this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
+            if (window.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[
@@ -755,17 +769,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;
+            this.showTab(this.mCurrentTab);
 
             if (updatePageReport)
               this.mCurrentBrowser.updatePageReport();
 
             // Update the URL bar.
             var loc = this.mCurrentBrowser.currentURI;
 
             var webProgress = this.mCurrentBrowser.webProgress;
@@ -1424,18 +1438,20 @@
             aTab.dispatchEvent(evt);
 
             // Remove the tab's filter and progress listener.
             const filter = this.mTabFilters[aTab._tPos];
             browser.webProgress.removeProgressListener(filter);
             filter.removeProgressListener(this.mTabListeners[aTab._tPos]);
             this.mTabListeners[aTab._tPos].destroy();
 
-            if (browser.currentURI)
-              this.mBrowserHistory.unregisterOpenPage(browser.currentURI);
+            if (browser.registeredOpenURI) {
+              this.mBrowserHistory.unregisterOpenPage(browser.registeredOpenURI);
+              delete browser.registeredOpenURI;
+            }
 
             // We are no longer the primary content area.
             browser.setAttribute("type", "content-targetable");
 
             // Remove this tab as the owner of any other tabs, since it's going away.
             Array.forEach(this.tabs, function (tab) {
               if ("owner" in tab && tab.owner == aTab)
                 // |tab| is a child of the tab we're removing, make it an orphan
@@ -1573,27 +1589,25 @@
 
             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 others remaining
+            let remainingTabs = this.visibleTabs;
+            let numTabs = remainingTabs.length;
+            if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) {
+              remainingTabs = Array.filter(this.tabs, function(tab) {
+                return this._removingTabs.indexOf(tab) == -1;
+              }, this);
             }
 
-            // 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 && remainingTabs.indexOf(tab) == -1);
 
             if (!tab) {
               tab = aTab;
@@ -1777,18 +1791,50 @@
         </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;
-          });
+            if (aTabs.indexOf(tab) == -1)
+              this.hideTab(tab);
+            else
+              this.showTab(tab);
+          }, this);
+        ]]>
+        </body>
+      </method>
+
+      <method name="showTab">
+        <parameter name="aTab"/>
+        <body>
+        <![CDATA[
+          if (aTab.hidden) {
+            aTab.hidden = false;
+            let event = document.createEvent("Events");
+            event.initEvent("TabShow", true, false);
+            aTab.dispatchEvent(event);
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="hideTab">
+        <parameter name="aTab"/>
+        <body>
+        <![CDATA[
+          if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
+              this._removingTabs.indexOf(aTab) == -1) {
+            aTab.hidden = true;
+            let event = document.createEvent("Events");
+            event.initEvent("TabHide", true, false);
+            aTab.dispatchEvent(event);
+          }
         ]]>
         </body>
       </method>
 
       <method name="selectTabAtIndex">
         <parameter name="aIndex"/>
         <parameter name="aEvent"/>
         <body>
@@ -2300,17 +2346,20 @@
           this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
         ]]>
       </constructor>
 
       <destructor>
         <![CDATA[
           for (var i = 0; i < this.mTabListeners.length; ++i) {
             let browser = this.getBrowserAtIndex(i);
-            this.mBrowserHistory.unregisterOpenPage(browser.currentURI);
+            if (browser.registeredOpenURI) {
+              this.mBrowserHistory.unregisterOpenPage(browser.registeredOpenURI);
+              delete browser.registeredOpenURI;
+            }
             browser.webProgress.removeProgressListener(this.mTabFilters[i]);
             this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]);
             this.mTabFilters[i] = null;
             this.mTabListeners[i].destroy();
             this.mTabListeners[i] = null;
           }
           document.removeEventListener("keypress", this, false);
         ]]>
@@ -2601,18 +2650,17 @@
               this.setAttribute("closebuttons", "noclose");
             else
               this.setAttribute("closebuttons", "activetab");
             break;
           case 1:
             if (this.childNodes.length == 1 && this._closeWindowWithLastTab)
               this.setAttribute("closebuttons", "noclose");
             else {
-              // Grab the last tab for size comparison
-              let tab = this.tabbrowser.visibleTabs.pop();
+              let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs];
               if (tab && tab.getBoundingClientRect().width > this.mTabClipWidth)
                 this.setAttribute("closebuttons", "alltabs");
               else
                 this.setAttribute("closebuttons", "activetab");
             }
             break;
           case 2:
           case 3:
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,1771 @@
+/* ***** 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>")
+    .addClass('newTabButton')
+    .click(function() {
+      self.newTab();
+    })
+    .attr('title',
+          "New tab")
+    .appendTo($container);
+
+  // ___ 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 || this.defaultName);
+
+  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 tab group…",
+
+  // -----------
+  // 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 - parseInt(closeButton.width()) - parseInt(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);
+    } 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});
+
+    // TabItems will have handled the new tab and added the tabItem property
+    let newItem = newTab.tabItem;
+
+    var self = this;
+    iQ(newItem.container).css({opacity: 0});
+    let $anim = iQ("<div>")
+      .addClass("newTabAnimatee")
+      .css({
+        top: newItem.bounds.top + 5,
+        left: newItem.bounds.left + 5,
+        width: newItem.bounds.width - 10,
+        height: newItem.bounds.height - 10,
+        zIndex: 999,
+        opacity: 0
+      })
+      .appendTo("body")
+      .animate({opacity: 1}, {
+        duration: 500,
+        complete: function() {
+          $anim.animate({
+            top: 0,
+            left: 0,
+            width: window.innerWidth,
+            height: window.innerHeight
+          }, {
+            duration: 270,
+            complete: function() {
+              iQ(newItem.container).css({opacity: 1});
+              newItem.zoomIn(!url);
+              $anim.remove();
+              self._sendToSubscribers("tabAdded", {groupItemId: self.id});
+            }
+          });
+        }
+      });
+  },
+
+  // ----------
+  // 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;
+  }
+});
+
+// ##########
+// 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);
+        shouldUpdateTabBar = true;
+      } 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() {
+    let newTabGroupTitle = "New Tabs";
+    this.groupItems.forEach(function(groupItem) {
+      if (groupItem.getTitle() == newTabGroupTitle && 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);
+    }
+  }
+});
+
+})();
--- a/browser/base/content/tabview/iq.js
+++ b/browser/base/content/tabview/iq.js
@@ -526,19 +526,17 @@ iQClass.prototype = {
   //     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
-      
+      tabviewBounce: "cubic-bezier(0.0, 0.63, .6, 1.29)", 
       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
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/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,1099 @@
+/* ***** 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;
+
+      self.link(tab);
+    }
+    // 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;
+
+      self.update(tab);
+    }
+    // When a tab is closed, unlink.
+    this._eventListeners["close"] = function(tab) {
+      if (tab.ownerDocument.defaultView != gWindow)
+        return;
+
+      self.unlink(tab);
+    }
+    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;
+      Storage.saveTab(tab, 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;
+}
+
+.title-container:hover input.name {
+  border: 1px solid #ddd;
+}
+
+.title-container:hover input.name-locked {
+  border: 1px solid transparent !important;
+  cursor: default;
+}
+
+input.name:focus {
+  color: #555;
+}
+
+input.defaultName {
+  font-style: italic !important;
+  background-image-opacity: .1;
+  color: transparent;
+}
+
+.title-container:hover input.defaultName {
+  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,1052 @@
+/* ***** 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: _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 dev menu is not meant for shipping, nor is it of general
+      // interest, but we still need it for the time being. Change the 
+      // false below to enable; just remember to change back before 
+      // committing. Bug 586721 will track the ultimate removal. 
+      if (false)
+        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) {
+          gBrowser.showTab(tab);
+        });
+      });
+      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?
+
+      // ___ tabs
+      TabItems.init();
+      TabItems.pausePainting();
+
+      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
+        let welcome = "How to organize your tabs";
+        let more = "";
+        let video = "http://videos-cdn.mozilla.net/firefox4beta/tabcandy_howto.webm";
+        var html =
+          "<div class='intro'>"
+            + "<h1>" + welcome + "</h1>"
+            + ( more && more.length ? "<div>" + more + "</div><br>" : "")
+            + "<video src='" + video + "' "
+            + "width='100%' preload controls>"
+          + "</div>";
+
+        box.left = box.right + padding;
+        box.width = infoWidth;
+        box.height = infoHeight;
+        var infoItem = new InfoItem(box);
+        infoItem.html(html);
+      }
+
+      // ___ 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 = "";
+#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();