Implement window.matchMedia for matching of media queries and notification of media query changes. (Bug 542058, patch 3) r=bzbarsky
authorL. David Baron <dbaron@dbaron.org>
Thu, 21 Apr 2011 20:17:31 -0700
changeset 68411 1b37bbdb15b7
parent 68410 183330b5a008
child 68412 b5832780b4c2
push id19630
push userdbaron@mozilla.com
push date2011-04-22 03:18 +0000
treeherdermozilla-central@e00435bb54b5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbzbarsky
bugs542058
milestone6.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Implement window.matchMedia for matching of media queries and notification of media query changes. (Bug 542058, patch 3) r=bzbarsky This is specified at: http://dev.w3.org/csswg/cssom-view/#extensions-to-the-window-interface
dom/base/nsDOMClassInfo.cpp
dom/base/nsDOMClassInfoClasses.h
dom/base/nsGlobalWindow.cpp
dom/interfaces/base/Makefile.in
dom/interfaces/base/nsIDOMMediaQueryList.idl
dom/interfaces/base/nsIDOMWindowInternal.idl
layout/base/nsPresContext.cpp
layout/base/nsPresContext.h
layout/style/Makefile.in
layout/style/nsCSSStyleSheet.cpp
layout/style/nsDOMMediaQueryList.cpp
layout/style/nsDOMMediaQueryList.h
layout/style/test/Makefile.in
layout/style/test/test_media_queries_dynamic.html
layout/style/test/test_media_query_list.html
xpcom/glue/nsCycleCollectionParticipant.h
--- a/dom/base/nsDOMClassInfo.cpp
+++ b/dom/base/nsDOMClassInfo.cpp
@@ -505,16 +505,18 @@
 #include "mozilla/dom/indexedDB/IDBObjectStore.h"
 #include "mozilla/dom/indexedDB/IDBTransaction.h"
 #include "mozilla/dom/indexedDB/IDBCursor.h"
 #include "mozilla/dom/indexedDB/IDBKeyRange.h"
 #include "mozilla/dom/indexedDB/IDBIndex.h"
 #include "nsIIDBDatabaseException.h"
 #include "nsIDOMEventException.h"
 
+#include "nsIDOMMediaQueryList.h"
+
 using namespace mozilla::dom;
 
 static NS_DEFINE_CID(kDOMSOF_CID, NS_DOM_SCRIPT_OBJECT_FACTORY_CID);
 
 static const char kDOMStringBundleURL[] =
   "chrome://global/locale/dom/dom.properties";
 
 // NOTE: DEFAULT_SCRIPTABLE_FLAGS and DOM_DEFAULT_SCRIPTABLE_FLAGS
@@ -1497,16 +1499,19 @@ static nsDOMClassInfoData sClassInfoData
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
 
 #ifdef MOZ_CSS_ANIMATIONS
   NS_DEFINE_CLASSINFO_DATA(MozCSSKeyframeRule, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CLASSINFO_DATA(MozCSSKeyframesRule, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
 #endif
+
+  NS_DEFINE_CLASSINFO_DATA(MediaQueryList, nsDOMGenericSH,
+                           DOM_DEFAULT_SCRIPTABLE_FLAGS)
 };
 
 // Objects that should be constructable through |new Name();|
 struct nsContractIDMapData
 {
   PRInt32 mDOMClassInfoID;
   const char *mContractID;
 };
@@ -4253,16 +4258,20 @@ nsDOMClassInfo::Init()
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMMozCSSKeyframeRule)
   DOM_CLASSINFO_MAP_END
 
   DOM_CLASSINFO_MAP_BEGIN(MozCSSKeyframesRule, nsIDOMMozCSSKeyframesRule)
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMMozCSSKeyframesRule)
   DOM_CLASSINFO_MAP_END
 #endif
 
+  DOM_CLASSINFO_MAP_BEGIN(MediaQueryList, nsIDOMMediaQueryList)
+    DOM_CLASSINFO_MAP_ENTRY(nsIDOMMediaQueryList)
+  DOM_CLASSINFO_MAP_END
+
 #ifdef NS_DEBUG
   {
     PRUint32 i = NS_ARRAY_LENGTH(sClassInfoData);
 
     if (i != eDOMClassInfoIDCount) {
       NS_ERROR("The number of items in sClassInfoData doesn't match the "
                "number of nsIDOMClassInfo ID's, this is bad! Fix it!");
 
--- a/dom/base/nsDOMClassInfoClasses.h
+++ b/dom/base/nsDOMClassInfoClasses.h
@@ -512,8 +512,10 @@ DOMCI_CLASS(IDBVersionChangeRequest)
 DOMCI_CLASS(IDBDatabaseException)
 
 DOMCI_CLASS(EventException)
 
 #ifdef MOZ_CSS_ANIMATIONS
 DOMCI_CLASS(MozCSSKeyframeRule)
 DOMCI_CLASS(MozCSSKeyframesRule)
 #endif
+
+DOMCI_CLASS(MediaQueryList)
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -81,16 +81,17 @@
 #include "nsGeolocation.h"
 #include "nsDesktopNotification.h"
 #include "nsContentCID.h"
 #include "nsLayoutStatics.h"
 #include "nsCycleCollector.h"
 #include "nsCCUncollectableMarker.h"
 #include "nsDOMThreadService.h"
 #include "nsAutoJSValHolder.h"
+#include "nsDOMMediaQueryList.h"
 
 // Interfaces Needed
 #include "nsIFrame.h"
 #include "nsCanvasFrame.h"
 #include "nsIWidget.h"
 #include "nsIBaseWindow.h"
 #include "nsAccelerometer.h"
 #include "nsIContent.h"
@@ -3919,16 +3920,42 @@ nsGlobalWindow::GetMozAnimationStartTime
   }
 
   // If all else fails, just be compatible with Date.now()
   *aTime = JS_Now() / PR_USEC_PER_MSEC;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsGlobalWindow::MatchMedia(const nsAString& aMediaQueryList,
+                           nsIDOMMediaQueryList** aResult)
+{
+  // FIXME: This whole forward-to-outer and then get a pres
+  // shell/context off the docshell dance is sort of silly; it'd make
+  // more sense to forward to the inner, but it's what everyone else
+  // (GetSelection, GetScrollXY, etc.) does around here.
+  FORWARD_TO_OUTER(MatchMedia, (aMediaQueryList, aResult),
+                   NS_ERROR_NOT_INITIALIZED);
+
+  *aResult = nsnull;
+
+  if (!mDocShell)
+    return NS_OK;
+
+  nsRefPtr<nsPresContext> presContext;
+  mDocShell->GetPresContext(getter_AddRefs(presContext));
+
+  if (!presContext)
+    return NS_OK;
+
+  presContext->MatchMedia(aMediaQueryList, aResult);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsGlobalWindow::SetScreenX(PRInt32 aScreenX)
 {
   FORWARD_TO_OUTER(SetScreenX, (aScreenX), NS_ERROR_NOT_INITIALIZED);
 
   /*
    * If caller is not chrome and the user has not explicitly exempted the site,
    * prevent setting window.screenX by exiting early
    */
--- a/dom/interfaces/base/Makefile.in
+++ b/dom/interfaces/base/Makefile.in
@@ -62,16 +62,17 @@ XPIDLSRCS =					\
 	nsIContentPrefService.idl		\
 	nsIContentURIGrouper.idl		\
 	nsIDOMClientInformation.idl		\
 	nsIDOMConstructor.idl			\
 	nsIDOMCRMFObject.idl			\
 	nsIDOMCrypto.idl			\
 	nsIDOMHistory.idl			\
 	nsIDOMLocation.idl			\
+	nsIDOMMediaQueryList.idl		\
 	nsIDOMMimeType.idl			\
 	nsIDOMMimeTypeArray.idl			\
 	nsIDOMNavigator.idl			\
 	nsIDOMPkcs11.idl			\
 	nsIDOMPlugin.idl			\
 	nsIDOMPluginArray.idl			\
 	nsIDOMScreen.idl			\
 	nsIDOMWindowInternal.idl		\
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/base/nsIDOMMediaQueryList.idl
@@ -0,0 +1,58 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** 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 mozilla.org code.
+ *
+ * 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):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * 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 ***** */
+
+#include "domstubs.idl"
+
+interface nsIDOMMediaQueryListListener;
+
+/* see http://dev.w3.org/csswg/cssom-view/#the-mediaquerylist-interface */
+
+[scriptable, uuid(e0e49c52-915b-40f9-9cba-6026305cdf3e)]
+interface nsIDOMMediaQueryList : nsISupports
+{
+  readonly attribute DOMString media;
+  readonly attribute boolean matches;
+
+  void addListener(in nsIDOMMediaQueryListListener listener);
+  void removeListener(in nsIDOMMediaQueryListListener listener);
+};
+
+[scriptable, function, uuid(279a5cbd-5c15-475d-847b-e0de1624eb77)]
+interface nsIDOMMediaQueryListListener : nsISupports
+{
+  void handleChange(in nsIDOMMediaQueryList mql);
+};
--- a/dom/interfaces/base/nsIDOMWindowInternal.idl
+++ b/dom/interfaces/base/nsIDOMWindowInternal.idl
@@ -39,18 +39,19 @@
 
 #include "nsIDOMWindow2.idl"
 
 interface nsIPrompt;
 interface nsIControllers;
 interface nsIDOMLocation;
 interface nsIVariant;
 interface nsIAnimationFrameListener;
+interface nsIDOMMediaQueryList;
 
-[scriptable, uuid(9d6a1157-0719-46a7-b49f-7ffeaa0b5c86)]
+[scriptable, uuid(04eafa93-efbe-4254-9d65-91c344fa7ff2)]
 interface nsIDOMWindowInternal : nsIDOMWindow2
 {
   readonly attribute nsIDOMWindowInternal        window;
 
   /* [replaceable] self */
   readonly attribute nsIDOMWindowInternal        self;
 
   readonly attribute nsIDOMNavigator             navigator;
@@ -227,16 +228,21 @@ interface nsIDOMWindowInternal : nsIDOMW
    */
   void
     mozRequestAnimationFrame([optional] in nsIAnimationFrameListener aListener);
 
   /**
    * The current animation start time in milliseconds since the epoch.
    */
   readonly attribute long long mozAnimationStartTime;
+
+  /**
+   * http://dev.w3.org/csswg/cssom-view/#extensions-to-the-window-interface
+   */
+  nsIDOMMediaQueryList matchMedia(in DOMString media_query_list);
 };
 
 [scriptable, uuid(8fc58f56-f769-4368-a098-edd08550cf1a)]
 interface nsIDOMMozURLProperty : nsISupports
 {
   DOMString createObjectURL(in nsIDOMBlob blob);
   void revokeObjectURL(in DOMString URL);
 };
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -95,16 +95,17 @@
 #include "nsObjectFrame.h"
 #include "nsTransitionManager.h"
 #ifdef MOZ_CSS_ANIMATIONS
 #include "nsAnimationManager.h"
 #endif
 #include "mozilla/dom/Element.h"
 #include "nsIFrameMessageManager.h"
 #include "FrameLayerBuilder.h"
+#include "nsDOMMediaQueryList.h"
 
 #ifdef MOZ_SMIL
 #include "nsSMILAnimationController.h"
 #endif // MOZ_SMIL
 
 #ifdef IBMBIDI
 #include "nsBidiPresUtils.h"
 #endif // IBMBIDI
@@ -249,23 +250,28 @@ nsPresContext::nsPresContext(nsIDocument
     mNeverAnimate = PR_TRUE;
   } else {
     mImageAnimationMode = imgIContainer::kNormalAnimMode;
     mNeverAnimate = PR_FALSE;
   }
   NS_ASSERTION(mDocument, "Null document");
   mUserFontSet = nsnull;
   mUserFontSetDirty = PR_TRUE;
+
+  PR_INIT_CLIST(&mDOMMediaQueryLists);
 }
 
 nsPresContext::~nsPresContext()
 {
   NS_PRECONDITION(!mShell, "Presshell forgot to clear our mShell pointer");
   SetShell(nsnull);
 
+  NS_ABORT_IF_FALSE(PR_CLIST_IS_EMPTY(&mDOMMediaQueryLists),
+                    "must not have media query lists left");
+
   // Disconnect the refresh driver *after* the transition manager, which
   // needs it.
   if (mRefreshDriver && mRefreshDriver->PresContext() == this) {
     mRefreshDriver->Disconnect();
   }
 
   if (mEventManager) {
     // unclear if these are needed, but can't hurt
@@ -1638,21 +1644,66 @@ void
 nsPresContext::MediaFeatureValuesChanged(PRBool aCallerWillRebuildStyleData)
 {
   mPendingMediaFeatureValuesChanged = PR_FALSE;
   if (mShell &&
       mShell->StyleSet()->MediumFeaturesChanged(this) &&
       !aCallerWillRebuildStyleData) {
     RebuildAllStyleData(nsChangeHint(0));
   }
+
+  if (!nsContentUtils::IsSafeToRunScript()) {
+    NS_ABORT_IF_FALSE(mDocument->IsBeingUsedAsImage(),
+                      "How did we get here?  Are we failing to notify "
+                      "listeners that we should notify?");
+    return;
+  }
+
+  // Media query list listeners should be notified from a queued task
+  // (in HTML5 terms), although we also want to notify them on certain
+  // flushes.  (We're already running off an event.)
+  //
+  // Note that we do this after the new style from media queries in
+  // style sheets has been computed.
+
+  if (!PR_CLIST_IS_EMPTY(&mDOMMediaQueryLists)) {
+    // We build a list of all the notifications we're going to send
+    // before we send any of them.  (The spec says the notifications
+    // should be a queued task, so any removals that happen during the
+    // notifications shouldn't affect what gets notified.)  Furthermore,
+    // we hold strong pointers to everything we're going to make
+    // notification calls to, since each notification involves calling
+    // arbitrary script that might otherwise destroy these objects, or,
+    // for that matter, |this|.
+    //
+    // Note that we intentionally send the notifications to media query
+    // list in the order they were created and, for each list, to the
+    // listeners in the order added.
+    nsDOMMediaQueryList::NotifyList notifyList;
+    for (PRCList *l = PR_LIST_HEAD(&mDOMMediaQueryLists);
+         l != &mDOMMediaQueryLists; l = PR_NEXT_LINK(l)) {
+      nsDOMMediaQueryList *mql = static_cast<nsDOMMediaQueryList*>(l);
+      mql->MediumFeaturesChanged(notifyList);
+    }
+
+    for (PRUint32 i = 0, i_end = notifyList.Length(); i != i_end; ++i) {
+      nsDOMMediaQueryList::HandleChangeData &d = notifyList[i];
+      d.listener->HandleChange(d.mql);
+    }
+
+    // NOTE:  When |notifyList| goes out of scope, our destructor could run.
+  }
 }
 
 void
 nsPresContext::PostMediaFeatureValuesChangedEvent()
 {
+  // FIXME: We should probably replace this event with use of
+  // nsRefreshDriver::AddStyleFlushObserver (except the pres shell would
+  // need to track whether it's been added).
   if (!mPendingMediaFeatureValuesChanged) {
     nsCOMPtr<nsIRunnable> ev =
       NS_NewRunnableMethod(this, &nsPresContext::HandleMediaFeatureValuesChangedEvent);
     if (NS_SUCCEEDED(NS_DispatchToCurrentThread(ev))) {
       mPendingMediaFeatureValuesChanged = PR_TRUE;
     }
   }
 }
@@ -1663,16 +1714,29 @@ nsPresContext::HandleMediaFeatureValuesC
   // Null-check mShell in case the shell has been destroyed (and the
   // event is the only thing holding the pres context alive).
   if (mPendingMediaFeatureValuesChanged && mShell) {
     MediaFeatureValuesChanged(PR_FALSE);
   }
 }
 
 void
+nsPresContext::MatchMedia(const nsAString& aMediaQueryList,
+                          nsIDOMMediaQueryList** aResult)
+{
+  nsRefPtr<nsDOMMediaQueryList> result =
+    new nsDOMMediaQueryList(this, aMediaQueryList);
+
+  // Insert the new item at the end of the linked list.
+  PR_INSERT_BEFORE(result, &mDOMMediaQueryLists);
+
+  result.forget(aResult);
+}
+
+void
 nsPresContext::SetPaginatedScrolling(PRBool aPaginated)
 {
   if (mType == eContext_PrintPreview || mType == eContext_PageLayout)
     mCanPaginatedScroll = aPaginated;
 }
 
 void
 nsPresContext::SetPrintSettings(nsIPrintSettings *aPrintSettings)
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -68,16 +68,17 @@
 #include "nsRegion.h"
 #include "nsTArray.h"
 #include "nsAutoPtr.h"
 #include "nsThreadUtils.h"
 #include "nsContentUtils.h"
 #include "nsIWidget.h"
 #include "mozilla/TimeStamp.h"
 #include "nsIContent.h"
+#include "prclist.h"
 
 class nsImageLoader;
 #ifdef IBMBIDI
 class nsBidiPresUtils;
 #endif // IBMBIDI
 
 struct nsRect;
 
@@ -102,16 +103,17 @@ class nsUserFontSet;
 struct nsFontFaceRuleContainer;
 class nsObjectFrame;
 class nsTransitionManager;
 #ifdef MOZ_CSS_ANIMATIONS
 class nsAnimationManager;
 #endif
 class nsRefreshDriver;
 class imgIContainer;
+class nsIDOMMediaQueryList;
 
 #ifdef MOZ_REFLOW_PERF
 class nsRenderingContext;
 #endif
 
 enum nsWidgetType {
   eWidgetType_Button  	= 1,
   eWidgetType_Checkbox	= 2,
@@ -265,16 +267,22 @@ public:
   void PostMediaFeatureValuesChangedEvent();
   NS_HIDDEN_(void) HandleMediaFeatureValuesChangedEvent();
   void FlushPendingMediaFeatureValuesChanged() {
     if (mPendingMediaFeatureValuesChanged)
       MediaFeatureValuesChanged(PR_FALSE);
   }
 
   /**
+   * Support for window.matchMedia()
+   */
+  void MatchMedia(const nsAString& aMediaQueryList,
+                  nsIDOMMediaQueryList** aResult);
+
+  /**
    * Access compatibility mode for this context.  This is the same as
    * our document's compatibility mode.
    */
   nsCompatibility CompatibilityMode() const {
     return Document()->GetCompatibilityMode();
   }
   /**
    * Notify the context that the document's compatibility mode has changed
@@ -1072,16 +1080,18 @@ protected:
   // charset rather than explicitly specified as a lang attribute).
   nsIAtom*              mLanguage;      // [STRONG]
 
   nsRefPtrHashtable<nsVoidPtrHashKey, nsImageLoader>
                         mImageLoaders[IMAGE_LOAD_TYPE_COUNT];
 
   nsWeakPtr             mContainer;
 
+  PRCList               mDOMMediaQueryLists;
+
   PRInt32               mMinFontSize;   // Min font size, defaults to 0
   float                 mTextZoom;      // Text zoom, defaults to 1.0
   float                 mFullZoom;      // Page zoom, defaults to 1.0
 
   PRInt32               mCurAppUnitsPerDevPixel;
   PRInt32               mAutoQualityMinFontSizePixelsPref;
 
   nsCOMPtr<nsITheme> mTheme;
--- a/layout/style/Makefile.in
+++ b/layout/style/Makefile.in
@@ -66,16 +66,17 @@ EXPORTS		= \
 		nsCSSPseudoClasses.h \
 		nsCSSPseudoElementList.h \
 		nsCSSPseudoElements.h \
 		nsCSSRuleProcessor.h \
 		nsCSSStyleSheet.h \
 		nsCSSValue.h \
 		nsDOMCSSAttrDeclaration.h \
 		nsDOMCSSDeclaration.h \
+		nsDOMMediaQueryList.h \
 		nsICSSDeclaration.h \
 		nsICSSLoaderObserver.h \
 		nsICSSPseudoComparator.h \
 		nsICSSRule.h \
 		nsICSSRuleList.h \
 		nsICSSStyleRuleDOMWrapper.h \
 		nsIStyleRule.h \
 		nsIStyleRuleProcessor.h \
@@ -122,16 +123,17 @@ CPPSRCS		= \
 		nsCSSStyleSheet.cpp \
 		nsCSSValue.cpp \
 		nsComputedDOMStyle.cpp \
 		nsDOMCSSAttrDeclaration.cpp \
 		nsDOMCSSDeclaration.cpp \
 		nsDOMCSSRGBColor.cpp \
 		nsDOMCSSRect.cpp \
 		nsDOMCSSValueList.cpp \
+		nsDOMMediaQueryList.cpp \
 		nsFontFaceLoader.cpp \
 		nsHTMLCSSStyleSheet.cpp \
 		nsHTMLStyleSheet.cpp \
 		nsLayoutStylesheetCache.cpp \
 		nsMediaFeatures.cpp \
 		nsNthIndexCache.cpp \
 		nsROCSSPrimitiveValue.cpp \
 		nsRuleData.cpp \
--- a/layout/style/nsCSSStyleSheet.cpp
+++ b/layout/style/nsCSSStyleSheet.cpp
@@ -582,17 +582,17 @@ nsMediaList::SetText(const nsAString& aM
 
   PRBool htmlMode = PR_FALSE;
   if (mStyleSheet) {
     nsCOMPtr<nsIDOMNode> node;
     mStyleSheet->GetOwnerNode(getter_AddRefs(node));
     htmlMode = !!node;
   }
 
-  return parser.ParseMediaList(nsString(aMediaText), nsnull, 0,
+  return parser.ParseMediaList(aMediaText, nsnull, 0,
                                this, htmlMode);
 }
 
 PRBool
 nsMediaList::Matches(nsPresContext* aPresContext,
                      nsMediaQueryResultCacheKey* aKey)
 {
   for (PRInt32 i = 0, i_end = mArray.Length(); i < i_end; ++i) {
new file mode 100644
--- /dev/null
+++ b/layout/style/nsDOMMediaQueryList.cpp
@@ -0,0 +1,164 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** 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 nsDOMMediaQueryList.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * 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 ***** */
+
+/* implements DOM interface for querying and observing media queries */
+
+#include "nsDOMMediaQueryList.h"
+#include "nsPresContext.h"
+#include "nsIMediaList.h"
+#include "nsCSSParser.h"
+
+nsDOMMediaQueryList::nsDOMMediaQueryList(nsPresContext *aPresContext,
+                                         const nsAString &aMediaQueryList)
+  : mPresContext(aPresContext),
+    mMediaList(new nsMediaList),
+    mMatchesValid(PR_FALSE)
+{
+  PR_INIT_CLIST(this);
+
+  nsCSSParser parser;
+  parser.ParseMediaList(aMediaQueryList, nsnull, 0, mMediaList, PR_FALSE);
+}
+
+nsDOMMediaQueryList::~nsDOMMediaQueryList()
+{
+  if (mPresContext) {
+    PR_REMOVE_LINK(this);
+  }
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsDOMMediaQueryList)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsDOMMediaQueryList)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mPresContext)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSTARRAY_OF_NSCOMPTR(mListeners)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsDOMMediaQueryList)
+if (tmp->mPresContext) {
+  PR_REMOVE_LINK(tmp);
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mPresContext)
+}
+NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMARRAY(mListeners)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+DOMCI_DATA(MediaQueryList, nsDOMMediaQueryList)
+
+NS_INTERFACE_MAP_BEGIN(nsDOMMediaQueryList)
+  NS_INTERFACE_MAP_ENTRY(nsIDOMMediaQueryList)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(nsDOMMediaQueryList)
+  NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(MediaQueryList)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDOMMediaQueryList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDOMMediaQueryList)
+
+NS_IMETHODIMP
+nsDOMMediaQueryList::GetMedia(nsAString &aMedia)
+{
+  mMediaList->GetText(aMedia);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMMediaQueryList::GetMatches(PRBool *aMatches)
+{
+  if (!mMatchesValid) {
+    NS_ABORT_IF_FALSE(mListeners.Length() == 0,
+                      "when listeners present, must keep mMatches current");
+    RecomputeMatches();
+  }
+
+  *aMatches = mMatches;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMMediaQueryList::AddListener(nsIDOMMediaQueryListListener *aListener)
+{
+  if (!mMatchesValid) {
+    NS_ABORT_IF_FALSE(mListeners.Length() == 0,
+                      "when listeners present, must keep mMatches current");
+    RecomputeMatches();
+  }
+
+  if (!mListeners.Contains(aListener)) {
+    mListeners.AppendElement(aListener);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDOMMediaQueryList::RemoveListener(nsIDOMMediaQueryListListener *aListener)
+{
+  mListeners.RemoveElement(aListener);
+  NS_ABORT_IF_FALSE(!mListeners.Contains(aListener),
+                    "duplicate occurrence of listeners");
+  return NS_OK;
+}
+
+void
+nsDOMMediaQueryList::RecomputeMatches()
+{
+  if (!mPresContext) {
+    return;
+  }
+
+  mMatches = mMediaList->Matches(mPresContext, nsnull);
+  mMatchesValid = PR_TRUE;
+}
+
+void
+nsDOMMediaQueryList::MediumFeaturesChanged(NotifyList &aListenersToNotify)
+{
+  mMatchesValid = PR_FALSE;
+
+  if (mListeners.Length()) {
+    PRPackedBool oldMatches = mMatches;
+    RecomputeMatches();
+    if (mMatches != oldMatches) {
+      for (PRUint32 i = 0, i_end = mListeners.Length(); i != i_end; ++i) {
+        HandleChangeData *d = aListenersToNotify.AppendElement();
+        if (d) {
+          d->mql = this;
+          d->listener = mListeners[i];
+        }
+      }
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/layout/style/nsDOMMediaQueryList.h
@@ -0,0 +1,106 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** 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 nsDOMMediaQueryList.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * 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 ***** */
+
+/* implements DOM interface for querying and observing media queries */
+
+#ifndef nsDOMMediaQueryList_h_
+#define nsDOMMediaQueryList_h_
+
+#include "nsIDOMMediaQueryList.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsAutoPtr.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "prclist.h"
+
+class nsPresContext;
+class nsMediaList;
+
+class nsDOMMediaQueryList : public nsIDOMMediaQueryList,
+                            public PRCList
+{
+public:
+  // The caller who constructs is responsible for calling Evaluate
+  // before calling any other methods.
+  nsDOMMediaQueryList(nsPresContext *aPresContext,
+                      const nsAString &aMediaQueryList);
+private:
+  ~nsDOMMediaQueryList();
+
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(nsDOMMediaQueryList)
+
+  NS_DECL_NSIDOMMEDIAQUERYLIST
+
+  struct HandleChangeData {
+    nsRefPtr<nsDOMMediaQueryList> mql;
+    nsCOMPtr<nsIDOMMediaQueryListListener> listener;
+  };
+
+  typedef FallibleTArray< nsCOMPtr<nsIDOMMediaQueryListListener> > ListenerList;
+  typedef FallibleTArray<HandleChangeData> NotifyList;
+
+  // Appends listeners that need notification to aListenersToNotify
+  void MediumFeaturesChanged(NotifyList &aListenersToNotify);
+
+private:
+  void RecomputeMatches();
+
+  // We only need a pointer to the pres context to support lazy
+  // reevaluation following dynamic changes.  However, this lazy
+  // reevaluation is perhaps somewhat important, since some usage
+  // patterns may involve the creation of large numbers of
+  // MediaQueryList objects which almost immediately become garbage
+  // (after a single call to the .matches getter).
+  //
+  // This pointer does make us a little more dependent on cycle
+  // collection.
+  //
+  // We have a non-null mPresContext for our entire lifetime except
+  // after cycle collection unlinking.  Having a non-null mPresContext
+  // is equivalent to being in that pres context's mDOMMediaQueryLists
+  // linked list.
+  nsRefPtr<nsPresContext> mPresContext;
+
+  nsRefPtr<nsMediaList> mMediaList;
+  PRPackedBool mMatches;
+  PRPackedBool mMatchesValid;
+  ListenerList mListeners;
+};
+
+#endif /* !defined(nsDOMMediaQueryList_h_) */
--- a/layout/style/test/Makefile.in
+++ b/layout/style/test/Makefile.in
@@ -150,16 +150,17 @@ GARBAGE += css_properties.js
 		test_ident_escaping.html \
 		test_inherit_computation.html \
 		test_inherit_storage.html \
 		test_initial_computation.html \
 		test_initial_storage.html \
 		test_media_queries.html \
 		test_media_queries_dynamic.html \
 		test_media_queries_dynamic_xbl.html \
+		test_media_query_list.html \
 		test_moz_device_pixel_ratio.html \
 		test_namespace_rule.html \
 		test_of_type_selectors.xhtml \
 		test_parse_ident.html \
 		test_parse_rule.html \
 		test_parse_url.html \
 		test_pixel_lengths.html \
 		test_pointer-events.html \
--- a/layout/style/test/test_media_queries_dynamic.html
+++ b/layout/style/test/test_media_queries_dynamic.html
@@ -52,21 +52,16 @@ function run() {
   function should_apply(cs) {
     ok(query_applies(cs), cs._originalQueryText + " should apply");
   }
 
   function should_not_apply(cs) {
     ok(!query_applies(cs), cs._originalQueryText + " should not apply");
   }
 
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-  const CI = Components.interfaces;
-  var iframe_docshell = subwin.QueryInterface(CI.nsIInterfaceRequestor).getInterface(CI.nsIWebNavigation).QueryInterface(CI.nsIDocShell);
-  var iframe_mudv = iframe_docshell.contentViewer.QueryInterface(CI.nsIMarkupDocumentViewer);
-
   var content_div = document.getElementById("content");
   content_div.style.font = "-moz-initial";
   var em_size =
     getComputedStyle(content_div, "").fontSize.match(/^(\d+)px$/)[1];
 
   let width_val = 317; // pick two not-too-round numbers
   let height_val = 228;
   iframe_style.width = width_val + "px";
@@ -90,36 +85,36 @@ function run() {
                      (Math.floor(width_val/(em_size*2)) - 1) + "em)")
   ];
 
   is(wh_queries[0].fontSize, em_size + "px", "text zoom is 1.0");
   should_not_apply(wh_queries[0]);
   should_apply(wh_queries[1]);
   should_apply(wh_queries[2]);
   should_not_apply(wh_queries[3]);
-  iframe_mudv.textZoom = 2.0;
+  SpecialPowers.setTextZoom(subwin, 2.0);
   isnot(wh_queries[0].fontSize, em_size + "px", "text zoom is not 1.0");
   should_not_apply(wh_queries[4]);
   should_apply(wh_queries[5]);
   should_apply(wh_queries[6]);
   should_not_apply(wh_queries[7]);
-  iframe_mudv.textZoom = 1.0;
+  SpecialPowers.setTextZoom(subwin, 1.0);
   is(wh_queries[0].fontSize, em_size + "px", "text zoom is 1.0");
   is(subwin.innerHeight, 228, "full zoom is 1.0");
   should_not_apply(wh_queries[0]);
   should_apply(wh_queries[1]);
   should_apply(wh_queries[2]);
   should_not_apply(wh_queries[3]);
-  iframe_mudv.fullZoom = 2.0;
+  SpecialPowers.setFullZoom(subwin, 2.0);
   isnot(subwin.innerHeight, 228, "full zoom is not 1.0");
   should_not_apply(wh_queries[4]);
   should_apply(wh_queries[5]);
   should_apply(wh_queries[6]);
   should_not_apply(wh_queries[7]);
-  iframe_mudv.fullZoom = 1.0;
+  SpecialPowers.setFullZoom(subwin, 1.0);
   is(subwin.innerHeight, 228, "full zoom is 1.0");
 
   SimpleTest.finish();
 }
 
 </script>
 </pre>
 </body>
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_media_query_list.html
@@ -0,0 +1,259 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=542058
+-->
+<head>
+  <title>Test for MediaQueryList (Bug 542058)</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="run()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=542058">Mozilla Bug 542058</a>
+<iframe id="subdoc" src="about:blank"></iframe>
+<div id="content" style="display:none"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for MediaQueryList (Bug 542058) **/
+
+SimpleTest.waitForExplicitFinish();
+
+function run() {
+  var iframe = document.getElementById("subdoc");
+  var subdoc = iframe.contentDocument;
+  var subwin = iframe.contentWindow;
+  var subroot = subdoc.documentElement;
+
+  var content_div = document.getElementById("content");
+  content_div.style.font = "-moz-initial";
+  var em_size =
+    getComputedStyle(content_div, "").fontSize.match(/^(\d+)px$/)[1];
+
+  var w = Math.floor(em_size * 9.3);
+  var h = Math.floor(em_size * 4.2);
+  iframe.style.width = w + "px";
+  iframe.style.height = h + "px";
+  subroot.offsetWidth; // flush layout
+
+  function setup_mql(str) {
+    var obj = { str: str, mql: subwin.matchMedia(str), notifyCount: 0 }
+    obj.mql.addListener(function(mql) {
+                          is(mql, obj.mql,
+                             "correct argument to listener: " + obj.str);
+                          ++obj.notifyCount;
+                          // Test the last match result only on odd
+                          // notifications.
+                          if (obj.notifyCount & 1) {
+                            obj.lastOddMatchResult = mql.matches;
+                          }
+                        });
+    return obj;
+  }
+
+  var w_exact_w = setup_mql("(width: " + w + "px)");
+  var w_min_9em = setup_mql("(min-width : 9em)");
+  var w_min_10em = setup_mql("(  min-width: 10em ) ");
+  var w_max_9em = setup_mql("(max-width: 9em)");
+  var w_max_10em = setup_mql("(max-width: 10em)");
+
+  is(w_exact_w.mql.media, "(width: " + w + "px)", "serialization");
+  is(w_min_9em.mql.media, "(min-width: 9em)", "serialization");
+  is(w_min_10em.mql.media, "(min-width: 10em)", "serialization");
+  is(w_max_9em.mql.media, "(max-width: 9em)", "serialization");
+  is(w_max_10em.mql.media, "(max-width: 10em)", "serialization");
+
+  function check_match(obj, expected, desc) {
+    is(obj.mql.matches, expected,
+       obj.str + " media query list .matches " + desc);
+    if (obj.notifyCount & 1) { // odd notifications only
+      is(obj.lastOddMatchResult, expected,
+       obj.str + " media query list last notify result " + desc);
+    }
+  }
+  function check_notify(obj, expected, desc) {
+    is(obj.notifyCount, expected,
+       obj.str + " media query list .notify count " + desc);
+  }
+  check_match(w_exact_w, true, "initially");
+  check_notify(w_exact_w, 0, "initially");
+  check_match(w_min_9em, true, "initially");
+  check_notify(w_min_9em, 0, "initially");
+  check_match(w_min_10em, false, "initially");
+  check_notify(w_min_10em, 0, "initially");
+  check_match(w_max_9em, false, "initially");
+  check_notify(w_max_9em, 0, "initially");
+  check_match(w_max_10em, true, "initially");
+  check_notify(w_max_10em, 0, "initially");
+
+  var w2 = Math.floor(em_size * 10.3);
+  iframe.style.width = w2 + "px";
+  subroot.offsetWidth; // flush layout
+
+  check_match(w_exact_w, false, "after width increase to around 10.3em");
+  check_notify(w_exact_w, 1, "after width increase to around 10.3em");
+  check_match(w_min_9em, true, "after width increase to around 10.3em");
+  check_notify(w_min_9em, 0, "after width increase to around 10.3em");
+  check_match(w_min_10em, true, "after width increase to around 10.3em");
+  check_notify(w_min_10em, 1, "after width increase to around 10.3em");
+  check_match(w_max_9em, false, "after width increase to around 10.3em");
+  check_notify(w_max_9em, 0, "after width increase to around 10.3em");
+  check_match(w_max_10em, false, "after width increase to around 10.3em");
+  check_notify(w_max_10em, 1, "after width increase to around 10.3em");
+
+  var w3 = w * 2;
+  iframe.style.width = w3 + "px";
+  subroot.offsetWidth; // flush layout
+
+  check_match(w_exact_w, false, "after width double from original");
+  check_notify(w_exact_w, 1, "after width double from original");
+  check_match(w_min_9em, true, "after width double from original");
+  check_notify(w_min_9em, 0, "after width double from original");
+  check_match(w_min_10em, true, "after width double from original");
+  check_notify(w_min_10em, 1, "after width double from original");
+  check_match(w_max_9em, false, "after width double from original");
+  check_notify(w_max_9em, 0, "after width double from original");
+  check_match(w_max_10em, false, "after width double from original");
+  check_notify(w_max_10em, 1, "after width double from original");
+
+  SpecialPowers.setFullZoom(subwin, 2.0);
+  subroot.offsetWidth; // flush layout
+
+  check_match(w_exact_w, true, "after zoom");
+  check_notify(w_exact_w, 2, "after zoom");
+  check_match(w_min_9em, true, "after zoom");
+  check_notify(w_min_9em, 0, "after zoom");
+  check_match(w_min_10em, false, "after zoom");
+  check_notify(w_min_10em, 2, "after zoom");
+  check_match(w_max_9em, false, "after zoom");
+  check_notify(w_max_9em, 0, "after zoom");
+  check_match(w_max_10em, true, "after zoom");
+  check_notify(w_max_10em, 2, "after zoom");
+
+  SpecialPowers.setFullZoom(subwin, 1.0);
+
+  // Additional tests of listener mutation.
+  (function() {
+    var received = [];
+    var received_mql = [];
+    function listener1(mql) {
+      received.push(1);
+      received_mql.push(mql);
+    }
+    function listener2(mql) {
+      received.push(2);
+      received_mql.push(mql);
+    }
+
+    iframe.style.width = "200px";
+    subroot.offsetWidth; // flush layout
+
+    var mql = subwin.matchMedia("(min-width: 150px)");
+    mql.addListener(listener1);
+    mql.addListener(listener1);
+    mql.addListener(listener2);
+    is(JSON.stringify(received), "[]", "listeners before notification");
+
+    iframe.style.width = "100px";
+    subroot.offsetWidth; // flush layout
+
+    is(JSON.stringify(received), "[1,2]", "duplicate listeners removed");
+    received = [];
+    mql.removeListener(listener1);
+
+    iframe.style.width = "200px";
+    subroot.offsetWidth; // flush layout
+
+    is(JSON.stringify(received), "[2]", "listener removal");
+    received = [];
+    mql.addListener(listener1);
+
+    iframe.style.width = "100px";
+    subroot.offsetWidth; // flush layout
+
+    is(JSON.stringify(received), "[2,1]", "listeners notified in order");
+    received = [];
+    mql.addListener(listener2);
+
+    iframe.style.width = "200px";
+    subroot.offsetWidth; // flush layout
+
+    is(JSON.stringify(received), "[2,1]", "add of existing listener is no-op");
+    received = [];
+    mql.addListener(listener1);
+
+    iframe.style.width = "100px";
+    subroot.offsetWidth; // flush layout
+
+    is(JSON.stringify(received), "[2,1]", "add of existing listener is no-op");
+    mql.removeListener(listener2);
+    received = [];
+    received_mql = [];
+
+    var mql2 = subwin.matchMedia("(min-width: 160px)");
+    mql2.addListener(listener1);
+    mql.addListener(listener2);
+
+    iframe.style.width = "200px";
+    subroot.offsetWidth; // flush layout
+
+    // mql (1, 2), mql2 (1)
+    is(JSON.stringify(received), "[1,2,1]",
+       "notification of lists in order created");
+    is(received_mql[0], mql,
+       "notification of lists in order created");
+    is(received_mql[1], mql,
+       "notification of lists in order created");
+    is(received_mql[2], mql2,
+       "notification of lists in order created");
+    received = [];
+    received_mql = [];
+
+    function removing_listener(mql) {
+      received.push(3);
+      received_mql.push(mql);
+      mql.removeListener(listener2);
+      mql2.removeListener(listener1);
+    }
+
+    mql.addListener(removing_listener);
+    mql.removeListener(listener2);
+    mql.addListener(listener2); // after removing_listener (3)
+
+    iframe.style.width = "100px";
+    subroot.offsetWidth; // flush layout
+
+    // mql(1, 3, 2) mql2(1)
+    is(JSON.stringify(received), "[1,3,2,1]",
+       "listeners still notified after removed if change was before");
+    is(received_mql[0], mql,
+       "notification order (removal tests)");
+    is(received_mql[1], mql,
+       "notification order (removal tests)");
+    is(received_mql[2], mql,
+       "notification order (removal tests)");
+    is(received_mql[3], mql2,
+       "notification order (removal tests)");
+    received = [];
+    received_mql = [];
+
+    iframe.style.width = "200px";
+    subroot.offsetWidth; // flush layout
+
+    // mql(1, 3)
+    is(JSON.stringify(received), "[1,3]",
+       "listeners not notified for changes after their removal");
+    is(received_mql[0], mql,
+       "notification order (removal tests)");
+    is(received_mql[1], mql,
+       "notification order (removal tests)");
+  })();
+
+  SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/xpcom/glue/nsCycleCollectionParticipant.h
+++ b/xpcom/glue/nsCycleCollectionParticipant.h
@@ -395,16 +395,25 @@ public:
     {                                                                          \
       PRUint32 i, length = (_array).Length();                                  \
       for (i = 0; i < length; ++i)                                             \
         NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NATIVE_PTR((_array)[i],              \
                                                      _element_class,           \
                                                      _name "[i]");             \
     }
 
+#define NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSTARRAY_OF_NSCOMPTR(_field)         \
+    {                                                                          \
+      PRUint32 i, length = tmp->_field.Length();                               \
+      for (i = 0; i < length; ++i) {                                           \
+        NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, #_field "[i]");                 \
+        cb.NoteXPCOMChild(tmp->_field[i].get());                               \
+      }                                                                        \
+    }
+
 #define NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSTARRAY_MEMBER(_field,              \
                                                           _element_class)      \
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSTARRAY(tmp->_field, _element_class,    \
                                                #_field)
 
 #define NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS                       \
     TraverseScriptObjects(tmp, cb);