Merge from Tracemonkey.
Merge from Tracemonkey.
--- 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 && 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'> </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> </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();
+
+ this._reorderTabsOnHide.forEach(function(groupItem) {
+ groupItem.reorderTabsBasedOnTabItemOrder();
+ });
+ this._reorderTabsOnHide = [];
+
+#ifdef XP_WIN
+ // Push the top of TabView frame to behind the tabbrowser, so glass can show
+ // XXX bug 586679: avoid shrinking the iframe and squishing iframe contents
+ // as well as avoiding the flash of black as we animate out
+ gTabViewFrame.style.marginTop = gBrowser.boxObject.y + "px";
+#endif
+ gTabViewDeck.selectedIndex = 0;
+ gBrowser.contentWindow.focus();
+
+ // set the close button on tab
+ gBrowser.tabContainer.adjustTabstrip();
+
+ gBrowser.updateTitlebar();
+#ifdef XP_MACOSX
+ this._setActiveTitleColor(false);
+#endif
+ let event = document.createEvent("Events");
+ event.initEvent("tabviewhidden", true, false);
+ dispatchEvent(event);
+ },
+
+#ifdef XP_MACOSX
+ // ----------
+ // Function: _setActiveTitleColor
+ // Used on the Mac to make the title bar match the gradient in the rest of the
+ // TabView UI.
+ //
+ // Parameters:
+ // set - true for the special TabView color, false for the normal color.
+ _setActiveTitleColor: function(set) {
+ // Mac Only
+ var mainWindow = gWindow.document.getElementById("main-window");
+ if (set)
+ mainWindow.setAttribute("activetitlebarcolor", "#C4C4C4");
+ else
+ mainWindow.removeAttribute("activetitlebarcolor");
+ },
+#endif
+
+ // ----------
+ // Function: _addTabActionHandlers
+ // Adds handlers to handle tab actions.
+ _addTabActionHandlers: function() {
+ var self = this;
+
+ AllTabs.register("close", function(tab) {
+ if (tab.ownerDocument.defaultView != gWindow)
+ return;
+
+ if (self._isTabViewVisible()) {
+ // just closed the selected tab in the TabView interface.
+ if (self._currentTab == tab)
+ self._closedSelectedTabInTabView = true;
+ } else {
+ // if not closing the last tab