Bug 819930 - Part 1: Implement an API for emulating CSS media types. r=dbaron
authorGraeme McCutcheon <graememcc_firefox@graeme-online.co.uk>
Wed, 17 Jul 2013 16:39:19 +0100
changeset 138997 66f4834afb70e9a75546a1628096c50bfb25990c
parent 138996 ba5645d56898c8f096a8cb2f29c7bcfcf18871a7
child 138998 5c42e1391bd2a78f0289ce347b3a4f550c45de7d
push id31197
push usergraememcc_firefox@graeme-online.co.uk
push dateThu, 18 Jul 2013 09:21:44 +0000
treeherdermozilla-inbound@5c42e1391bd2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdbaron
bugs819930
milestone25.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
Bug 819930 - Part 1: Implement an API for emulating CSS media types. r=dbaron
docshell/base/nsIMarkupDocumentViewer.idl
layout/base/nsDocumentViewer.cpp
layout/base/nsPresContext.cpp
layout/base/nsPresContext.h
layout/base/tests/Makefile.in
layout/base/tests/test_emulateMedium.html
testing/specialpowers/content/specialpowersAPI.js
--- a/docshell/base/nsIMarkupDocumentViewer.idl
+++ b/docshell/base/nsIMarkupDocumentViewer.idl
@@ -18,17 +18,17 @@ interface nsIDOMNode;
 #include "nsCOMPtr.h"
 #include "nsTArray.h"
 %}
 
 interface nsIMarkupDocumentViewer;
 
 [ref] native nsIMarkupDocumentViewerTArray(nsTArray<nsCOMPtr<nsIMarkupDocumentViewer> >);
 
-[scriptable, uuid(02d37b31-e654-4b74-9bc3-14dfe0020bb3)]
+[scriptable, uuid(23326580-e6ce-4ee1-94b8-f1d9424a9977)]
 interface nsIMarkupDocumentViewer : nsISupports
 {
 
 	/*
 	Scrolls to a given DOM content node. 
 	*/
 	void scrollToNode(in nsIDOMNode node);
 
@@ -131,9 +131,22 @@ interface nsIMarkupDocumentViewer : nsIS
   /**
    * Set the maximum line width for the document.
    * NOTE: This will generate a reflow!
    *
    * @param maxLineWidth The maximum width of any line boxes on the page,
    *        in CSS pixels.
    */
   void changeMaxLineBoxWidth(in int32_t maxLineBoxWidth);
+
+  /*
+   * Render the document as if being viewed on a device with the specified
+   * media type. This will cause a reflow.
+   *
+   * @param mediaType The media type to be emulated
+   */
+  void emulateMedium(in AString aMediaType);
+
+  /*
+   * Restore the viewer's natural media type
+   */
+  void stopEmulatingMedium();
 };
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -2960,16 +2960,90 @@ nsDocumentViewer::GetAuthorStyleDisabled
   if (mPresShell) {
     *aStyleDisabled = mPresShell->GetAuthorStyleDisabled();
   } else {
     *aStyleDisabled = false;
   }
   return NS_OK;
 }
 
+static bool
+ExtResourceEmulateMedium(nsIDocument* aDocument, void* aClosure)
+{
+  nsIPresShell* shell = aDocument->GetShell();
+  if (shell) {
+    nsPresContext* ctxt = shell->GetPresContext();
+    if (ctxt) {
+      const nsAString* mediaType = static_cast<nsAString*>(aClosure);
+      ctxt->EmulateMedium(*mediaType);
+    }
+  }
+
+  return true;
+}
+
+static void
+ChildEmulateMedium(nsIMarkupDocumentViewer* aChild, void* aClosure)
+{
+  const nsAString* mediaType = static_cast<nsAString*>(aClosure);
+  aChild->EmulateMedium(*mediaType);
+}
+
+NS_IMETHODIMP
+nsDocumentViewer::EmulateMedium(const nsAString& aMediaType)
+{
+  if (mPresContext) {
+    mPresContext->EmulateMedium(aMediaType);
+  }
+  CallChildren(ChildEmulateMedium, const_cast<nsAString*>(&aMediaType));
+
+  if (mDocument) {
+    mDocument->EnumerateExternalResources(ExtResourceEmulateMedium,
+                                          const_cast<nsAString*>(&aMediaType));
+  }
+
+  return NS_OK;
+}
+
+static bool
+ExtResourceStopEmulatingMedium(nsIDocument* aDocument, void* aClosure)
+{
+  nsIPresShell* shell = aDocument->GetShell();
+  if (shell) {
+    nsPresContext* ctxt = shell->GetPresContext();
+    if (ctxt) {
+      ctxt->StopEmulatingMedium();
+    }
+  }
+
+  return true;
+}
+
+static void
+ChildStopEmulatingMedium(nsIMarkupDocumentViewer* aChild, void* aClosure)
+{
+  aChild->StopEmulatingMedium();
+}
+
+NS_IMETHODIMP
+nsDocumentViewer::StopEmulatingMedium()
+{
+  if (mPresContext) {
+    mPresContext->StopEmulatingMedium();
+  }
+  CallChildren(ChildStopEmulatingMedium, nullptr);
+
+  if (mDocument) {
+    mDocument->EnumerateExternalResources(ExtResourceStopEmulatingMedium,
+                                          nullptr);
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 nsDocumentViewer::GetDefaultCharacterSet(nsACString& aDefaultCharacterSet)
 {
   if (mDefaultCharacterSet.IsEmpty())
   {
     const nsAdoptingCString& defCharset =
       Preferences::GetLocalizedCString("intl.charset.default");
 
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -228,16 +228,17 @@ nsPresContext::nsPresContext(nsIDocument
   mBodyTextColor = mDefaultColor;
 
   if (aType == eContext_Galley) {
     mMedium = nsGkAtoms::screen;
   } else {
     mMedium = nsGkAtoms::print;
     mPaginated = true;
   }
+  mMediaEmulated = mMedium;
 
   if (!IsDynamic()) {
     mImageAnimationMode = imgIContainer::kDontAnimMode;
     mNeverAnimate = true;
   } else {
     mImageAnimationMode = imgIContainer::kNormalAnimMode;
     mNeverAnimate = false;
   }
@@ -1687,16 +1688,40 @@ nsPresContext::UIResolutionChangedIntern
     AppUnitsPerDevPixelChanged();
   }
 
   mDocument->EnumerateSubDocuments(UIResolutionChangedSubdocumentCallback,
                                    nullptr);
 }
 
 void
+nsPresContext::EmulateMedium(const nsAString& aMediaType)
+{
+  nsIAtom* previousMedium = Medium();
+  mIsEmulatingMedia = true;
+
+  nsAutoString mediaType;
+  nsContentUtils::ASCIIToLower(aMediaType, mediaType);
+
+  mMediaEmulated = do_GetAtom(mediaType);
+  if (mMediaEmulated != previousMedium && mShell) {
+    MediaFeatureValuesChanged(eRebuildStyleIfNeeded, nsChangeHint(0));
+  }
+}
+
+void nsPresContext::StopEmulatingMedium()
+{
+  nsIAtom* previousMedium = Medium();
+  mIsEmulatingMedia = false;
+  if (Medium() != previousMedium) {
+    MediaFeatureValuesChanged(eRebuildStyleIfNeeded, nsChangeHint(0));
+  }
+}
+
+void
 nsPresContext::RebuildAllStyleData(nsChangeHint aExtraHint)
 {
   if (!mShell) {
     // We must have been torn down. Nothing to do here.
     return;
   }
 
   mUsesRootEMUnits = false;
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -300,17 +300,32 @@ public:
 #else
   void SetImageAnimationMode(uint16_t aMode)
   { SetImageAnimationModeExternal(aMode); }
 #endif
 
   /** 
    * Get medium of presentation
    */
-  nsIAtom* Medium() { return mMedium; }
+  nsIAtom* Medium() {
+    if (!mIsEmulatingMedia)
+      return mMedium;
+    return mMediaEmulated;
+  }
+
+  /*
+   * Render the document as if being viewed on a device with the specified
+   * media type.
+   */
+  void EmulateMedium(const nsAString& aMediaType);
+
+  /*
+   * Restore the viewer's natural medium
+   */
+  void StopEmulatingMedium();
 
   void* AllocateFromShell(size_t aSize)
   {
     if (mShell)
       return mShell->AllocateMisc(aSize);
     return nullptr;
   }
 
@@ -1146,16 +1161,17 @@ protected:
                                             // since there is no dependency
                                             // from gfx back to layout.
   nsRefPtr<nsEventStateManager> mEventManager;
   nsRefPtr<nsRefreshDriver> mRefreshDriver;
   nsRefPtr<nsTransitionManager> mTransitionManager;
   nsRefPtr<nsAnimationManager> mAnimationManager;
   nsIAtom*              mMedium;        // initialized by subclass ctors;
                                         // weak pointer to static atom
+  nsCOMPtr<nsIAtom> mMediaEmulated;
 
   nsILinkHandler*       mLinkHandler;   // [WEAK]
 
   // Formerly mLangGroup; moving from charset-oriented langGroup to
   // maintaining actual language settings everywhere (see bug 524107).
   // This may in fact hold a langGroup such as x-western rather than
   // a specific language, however (e.g, if it is inferred from the
   // charset rather than explicitly specified as a lang attribute).
@@ -1255,16 +1271,17 @@ protected:
   unsigned              mIsRootPaginatedDocument : 1;
   unsigned              mPrefBidiDirection : 1;
   unsigned              mPrefScrollbarSide : 2;
   unsigned              mPendingSysColorChanged : 1;
   unsigned              mPendingThemeChanged : 1;
   unsigned              mPendingUIResolutionChanged : 1;
   unsigned              mPendingMediaFeatureValuesChanged : 1;
   unsigned              mPrefChangePendingNeedsReflow : 1;
+  unsigned              mIsEmulatingMedia : 1;
   // True if the requests in mInvalidateRequestsSinceLastPaint cover the
   // entire viewport
   unsigned              mAllInvalidated : 1;
 
   // Are we currently drawing an SVG glyph?
   unsigned              mIsGlyph : 1;
 
   // Does the associated document use root-em (rem) units?
--- a/layout/base/tests/Makefile.in
+++ b/layout/base/tests/Makefile.in
@@ -144,16 +144,17 @@ MOCHITEST_FILES =	\
 		test_bug842853.html \
 		test_bug842853-2.html \
 		  file_bug842853.sjs \
 		  file_bug842853.html \
 		test_bug849219.html \
 		test_bug851485.html \
 		test_bug851445.html \
 		  bug851445_helper.html \
+		test_emulateMedium.html \
 		$(NULL)
 
 ifeq (,$(filter gonk,$(MOZ_WIDGET_TOOLKIT)))
 # THESE TESTS (BELOW) DO NOT RUN ON B2G
 MOCHITEST_FILES += \
 		test_bug858459.html \
 		$(NULL)
 # THESE TESTS (ABOVE) DO NOT RUN ON B2G
new file mode 100644
--- /dev/null
+++ b/layout/base/tests/test_emulateMedium.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=819930
+-->
+  <head>
+    <meta charset="utf-8">
+    <title>Test for Bug 819930</title>
+    <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+    <style>
+      @media braille {
+        body {
+          background-color: rgb(255, 255, 0);
+        }
+      }
+
+      @media embossed {
+        body {
+          background-color: rgb(210, 180, 140);
+        }
+      }
+
+      @media handheld {
+        body {
+          background-color: rgb(0, 255, 0);
+        }
+      }
+
+      @media print {
+        body {
+          background-color: rgb(0, 255, 255);
+        }
+      }
+
+      @media projection {
+        body {
+          background-color: rgb(30, 144, 255);
+        }
+      }
+
+      @media screen {
+        body {
+          background-color: green;
+        }
+      }
+
+      @media speech {
+        body {
+          background-color: rgb(192, 192, 192);
+        }
+      }
+
+      @media tty {
+        body {
+          background-color: rgb(255, 192, 203);
+        }
+      }
+
+      @media tv {
+        body {
+          background-color: rgb(75, 0, 130);
+        }
+      }
+    </style>
+  </head>
+  <body>
+    <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=819930">Mozilla Bug 819930</a>
+    <p id="display"></p>
+
+    <div id="content" style="display: none"></div>
+    <pre id="test">
+      <script type="application/javascript;version=1.7">
+        let tests = [{name: 'braille', value: 'rgb(255, 255, 0)'},
+                     {name: 'embossed', value: 'rgb(210, 180, 140)'},
+                     {name: 'handheld', value: 'rgb(0, 255, 0)'},
+                     {name: 'print', value: 'rgb(0, 255, 255)'},
+                     {name: 'projection', value: 'rgb(30, 144, 255)'},
+                     {name: 'speech', value: 'rgb(192, 192, 192)'},
+                     {name: 'tty', value: 'rgb(255, 192, 203)'},
+                     {name: 'tv', value: 'rgb(75, 0, 130)'}];
+
+        let originalColor = 'rgb(0, 128, 0)';
+        let body = document.body;
+
+        let getColor = function() {
+          return window.getComputedStyle(body)
+                       .getPropertyValue('background-color');
+        };
+
+        tests.forEach(function(test) {
+          // Emulate the given media
+          SpecialPowers.emulateMedium(window, test.name);
+          is(getColor(), test.value, 'emulating ' + test.name + ' produced ' +
+             'correct rendering');
+
+          // Do the @media screen rules get applied after ending the emulation?
+          SpecialPowers.stopEmulatingMedium(window);
+          is(getColor(), originalColor, 'Ending ' + test.name +
+             ' emulation restores style for original medium');
+
+          // CSS media types are case-insensitive; we should be too.
+          SpecialPowers.emulateMedium(window, test.name.toUpperCase());
+          is(getColor(), test.value,
+             test.name + ' emulation is case-insensitive');
+          SpecialPowers.stopEmulatingMedium(window);
+        });
+
+        // Emulating screen should produce the same rendering as when there is
+        // no emulation in effect
+        SpecialPowers.emulateMedium(window, 'screen');
+        is(getColor(), originalColor,
+           'Emulating screen produces original rendering');
+        SpecialPowers.stopEmulatingMedium(window);
+
+        // Screen should be case-insensitive too
+        SpecialPowers.emulateMedium(window, 'SCREEN');
+        is(getColor(), originalColor, 'screen emulation is case-insensitive');
+        SpecialPowers.stopEmulatingMedium(window);
+
+        // An invalid parameter shouldn't fail. Given the CSS rules above,
+        // an invalid parameter should result in a different rendering from any
+        // produced thus far
+        try {
+          SpecialPowers.emulateMedium(window, 'clay');
+          let invalid = getColor();
+          tests.push({name: 'screen', value: 'green'});
+          tests.forEach(function(test) {
+            isnot(invalid, test.value, 'Emulating invalid type differs from ' +
+               test.name);
+          });
+        } catch (e) {
+          ok(false, 'Supplying invalid type to emulateMedium shouldn\'t throw');
+        }
+
+        SpecialPowers.stopEmulatingMedium(window);
+      </script>
+    </pre>
+  </body>
+</html>
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -1108,16 +1108,23 @@ SpecialPowersAPI.prototype = {
   },
   getTextZoom: function(window) {
     return this._getMUDV(window).textZoom;
   },
   setTextZoom: function(window, zoom) {
     this._getMUDV(window).textZoom = zoom;
   },
 
+  emulateMedium: function(window, mediaType) {
+    this._getMUDV(window).emulateMedium(mediaType);
+  },
+  stopEmulatingMedium: function(window) {
+    this._getMUDV(window).stopEmulatingMedium();
+  },
+
   createSystemXHR: function() {
     return this.wrap(Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest));
   },
 
   snapshotWindowWithOptions: function (win, rect, bgcolor, options) {
     var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     if (rect === undefined) {
       rect = { top: win.scrollY, left: win.scrollX,