Bug 485157: SMIL event timing, part 6 repeat timing, r=dholbert, sr=roc, a=roc
authorBrian Birtles <birtles@gmail.com>
Wed, 18 Aug 2010 19:20:24 +0900
changeset 50807 32b5f913f72c
parent 50806 fd27d7619098
child 50808 ca457b5758e0
push id15162
push userbbirtles@mozilla.com
push date2010-08-18 10:24 +0000
treeherdermozilla-central@ca457b5758e0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdholbert, roc, roc
bugs485157
milestone2.0b5pre
Bug 485157: SMIL event timing, part 6 repeat timing, r=dholbert, sr=roc, a=roc
content/base/src/nsGkAtomList.h
content/smil/nsSMILParserUtils.cpp
content/smil/nsSMILTimeValueSpec.cpp
content/smil/nsSMILTimeValueSpec.h
content/smil/test/Makefile.in
content/smil/test/test_smilRepeatTiming.xhtml
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -1315,16 +1315,17 @@ GK_ATOM(mozAnimateMotionDummyAttr, "_moz
 GK_ATOM(onbegin, "onbegin")
 GK_ATOM(onbeginEvent, "onbeginEvent")
 GK_ATOM(onend, "onend")
 GK_ATOM(onendEvent, "onendEvent")
 GK_ATOM(onrepeat, "onrepeat")
 GK_ATOM(onrepeatEvent, "onrepeatEvent")
 GK_ATOM(repeatCount, "repeatCount")
 GK_ATOM(repeatDur, "repeatDur")
+GK_ATOM(repeatEvent, "repeatEvent")
 GK_ATOM(restart, "restart")
 GK_ATOM(to, "to")
 GK_ATOM(XML, "XML")
 #endif
 
 #ifdef MOZ_MATHML
 // internal MathML attributes: different from columnalign_, columnlines_,
 // fontstyle_, rowalign_ and rowlines_
--- a/content/smil/nsSMILParserUtils.cpp
+++ b/content/smil/nsSMILParserUtils.cpp
@@ -55,17 +55,19 @@
 
 namespace {
 
 const PRUint32 MSEC_PER_SEC  = 1000;
 const PRUint32 MSEC_PER_MIN  = 1000 * 60;
 const PRUint32 MSEC_PER_HOUR = 1000 * 60 * 60;
 const PRInt32  DECIMAL_BASE  = 10;
 
-#define ACCESSKEY_PREFIX NS_LITERAL_STRING("accesskey(")
+// XXX SVG/SMIL Animation use 'accessKey' whilst SMIL3 uses 'accesskey'
+//     We should allow both
+#define ACCESSKEY_PREFIX NS_LITERAL_STRING("accessKey(")
 #define REPEAT_PREFIX    NS_LITERAL_STRING("repeat(")
 #define WALLCLOCK_PREFIX NS_LITERAL_STRING("wallclock(")
 
 // NS_IS_SPACE relies on isspace which may return true for \xB and \xC but
 // SMILANIM does not consider these characters to be whitespace.
 inline PRBool
 IsSpace(const PRUnichar c)
 {
@@ -358,16 +360,19 @@ ParseElementBaseTimeValueSpec(const nsAS
   // The spec will probably look something like one of these
   //
   // element-name.begin
   // element-name.event-name
   // event-name
   // element-name.repeat(3)
   // event\.name
   //
+  // Technically `repeat(3)' is permitted but the behaviour in this case is not
+  // defined (for SMIL Animation) so we don't support it here.
+  //
 
   const PRUnichar* tokenStart = aSpec.BeginReading();
   const PRUnichar* tokenEnd = GetTokenEnd(aSpec, PR_TRUE);
   nsAutoString token(Substring(tokenStart, tokenEnd));
   Unescape(token);
 
   if (token.IsEmpty())
     return NS_ERROR_FAILURE;
--- a/content/smil/nsSMILTimeValueSpec.cpp
+++ b/content/smil/nsSMILTimeValueSpec.cpp
@@ -42,16 +42,17 @@
 #include "nsSMILTimedElement.h"
 #include "nsSMILInstanceTime.h"
 #include "nsSMILParserUtils.h"
 #include "nsISMILAnimationElement.h"
 #include "nsContentUtils.h"
 #include "nsIEventListenerManager.h"
 #include "nsIDOMEventGroup.h"
 #include "nsGUIEvent.h"
+#include "nsIDOMTimeEvent.h"
 #include "nsString.h"
 
 using namespace mozilla::dom;
 
 //----------------------------------------------------------------------
 // Nested class: EventListener
 
 NS_IMPL_ISUPPORTS1(nsSMILTimeValueSpec::EventListener, nsIDOMEventListener)
@@ -112,26 +113,32 @@ nsSMILTimeValueSpec::SetSpec(const nsASt
   //   The special value "indefinite" does not yield an instance time in the
   //   begin list. It will, however yield a single instance with the value
   //   "indefinite" in an end list. This value is not removed by a reset.
   if (mParams.mType == nsSMILTimeValueSpecParams::OFFSET ||
       (!mIsBegin && mParams.mType == nsSMILTimeValueSpecParams::INDEFINITE)) {
     mOwner->AddInstanceTime(new nsSMILInstanceTime(mParams.mOffset), mIsBegin);
   }
 
+  // Fill in the event symbol to simplify handling later
+  if (mParams.mType == nsSMILTimeValueSpecParams::REPEAT) {
+    mParams.mEventSymbol = nsGkAtoms::repeatEvent;
+  }
+
   ResolveReferences(aContextNode);
 
   return rv;
 }
 
 void
 nsSMILTimeValueSpec::ResolveReferences(nsIContent* aContextNode)
 {
   if (mParams.mType != nsSMILTimeValueSpecParams::SYNCBASE &&
-      !IsEventBased())
+      mParams.mType != nsSMILTimeValueSpecParams::EVENT &&
+      mParams.mType != nsSMILTimeValueSpecParams::REPEAT)
     return;
 
   NS_ABORT_IF_FALSE(aContextNode,
       "null context node for resolving timing references against");
 
   // If we're not bound to the document yet, don't worry, we'll get called again
   // when that happens
   if (!aContextNode->IsInDoc())
@@ -140,21 +147,21 @@ nsSMILTimeValueSpec::ResolveReferences(n
   // Hold ref to the old element so that it isn't destroyed in between resetting
   // the referenced element and using the pointer to update the referenced
   // element.
   nsRefPtr<Element> oldReferencedElement = mReferencedElement.get();
 
   if (mParams.mDependentElemID) {
     mReferencedElement.ResetWithID(aContextNode,
         nsDependentAtomString(mParams.mDependentElemID));
-  } else if (IsEventBased()) {
+  } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) {
     Element* target = mOwner->GetTargetElement();
     mReferencedElement.ResetWithElement(target);
   } else {
-    NS_ABORT_IF_FALSE(PR_FALSE, "Syncbase element without ID");
+    NS_ABORT_IF_FALSE(PR_FALSE, "Syncbase or repeat spec without ID");
   }
   UpdateReferencedElement(oldReferencedElement, mReferencedElement.get());
 }
 
 PRBool
 nsSMILTimeValueSpec::IsEventBased() const
 {
   return mParams.mType == nsSMILTimeValueSpecParams::EVENT ||
@@ -252,39 +259,51 @@ nsSMILTimeValueSpec::Unlink()
 void
 nsSMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo)
 {
   if (aFrom == aTo)
     return;
 
   UnregisterFromReferencedElement(aFrom);
 
-  if (mParams.mType == nsSMILTimeValueSpecParams::SYNCBASE) {
-    nsSMILTimedElement* to = GetTimedElement(aTo);
-    if (to) {
-      to->AddDependent(*this);
+  switch (mParams.mType)
+  {
+  case nsSMILTimeValueSpecParams::SYNCBASE:
+    {
+      nsSMILTimedElement* to = GetTimedElement(aTo);
+      if (to) {
+        to->AddDependent(*this);
+      }
     }
-  } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) {
+    break;
+
+  case nsSMILTimeValueSpecParams::EVENT:
+  case nsSMILTimeValueSpecParams::REPEAT:
     RegisterEventListener(aTo);
+    break;
+
+  default:
+    // not a referencing-type or not yet supported
+    break;
   }
 }
 
 void
 nsSMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement)
 {
   if (!aElement)
     return;
 
   if (mParams.mType == nsSMILTimeValueSpecParams::SYNCBASE) {
     nsSMILTimedElement* timedElement = GetTimedElement(aElement);
     if (timedElement) {
       timedElement->RemoveDependent(*this);
     }
     mOwner->RemoveInstanceTimesForCreator(this, mIsBegin);
-  } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) {
+  } else if (IsEventBased()) {
     UnregisterEventListener(aElement);
   }
 }
 
 nsSMILTimedElement*
 nsSMILTimeValueSpec::GetTimedElement(Element* aElement)
 {
   if (!aElement)
@@ -295,18 +314,20 @@ nsSMILTimeValueSpec::GetTimedElement(Ele
     return nsnull;
 
   return &animElement->TimedElement();
 }
 
 void
 nsSMILTimeValueSpec::RegisterEventListener(Element* aTarget)
 {
-  NS_ABORT_IF_FALSE(mParams.mType == nsSMILTimeValueSpecParams::EVENT,
-    "Attempting to register event-listener for non-event nsSMILTimeValueSpec");
+  NS_ABORT_IF_FALSE(mParams.mType == nsSMILTimeValueSpecParams::EVENT ||
+                    mParams.mType == nsSMILTimeValueSpecParams::REPEAT,
+    "Attempting to register event-listener for unexpected nsSMILTimeValueSpec"
+    " type");
   NS_ABORT_IF_FALSE(mParams.mEventSymbol,
     "Attempting to register event-listener but there is no event name");
 
   if (!aTarget)
     return;
 
   if (!mEventListener) {
     mEventListener = new EventListener(this);
@@ -362,34 +383,55 @@ nsSMILTimeValueSpec::GetEventListenerMan
 
   return elm;
 }
 
 void
 nsSMILTimeValueSpec::HandleEvent(nsIDOMEvent* aEvent)
 {
   NS_ABORT_IF_FALSE(mEventListener, "Got event without an event listener");
-  NS_ABORT_IF_FALSE(mParams.mType == nsSMILTimeValueSpecParams::EVENT,
-    "Got event for non-event nsSMILTimeValueSpec");
+  NS_ABORT_IF_FALSE(IsEventBased(),
+                    "Got event for non-event nsSMILTimeValueSpec");
+  NS_ABORT_IF_FALSE(aEvent, "No event supplied");
 
   // XXX In the long run we should get the time from the event itself which will
   // store the time in global document time which we'll need to convert to our
   // time container
   nsSMILTimeContainer* container = mOwner->GetTimeContainer();
   if (!container)
     return;
 
+  if (!CheckEventDetail(aEvent))
+    return;
+
   nsSMILTime currentTime = container->GetCurrentTime();
   nsSMILTimeValue newTime(currentTime + mParams.mOffset.GetMillis());
 
   nsRefPtr<nsSMILInstanceTime> newInstance =
     new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_EVENT);
   mOwner->AddInstanceTime(newInstance, mIsBegin);
 }
 
+PRBool
+nsSMILTimeValueSpec::CheckEventDetail(nsIDOMEvent *aEvent)
+{
+  if (mParams.mType != nsSMILTimeValueSpecParams::REPEAT)
+    return PR_TRUE;
+
+  nsCOMPtr<nsIDOMTimeEvent> timeEvent = do_QueryInterface(aEvent);
+  if (!timeEvent) {
+    NS_WARNING("Received a repeat event that was not a DOMTimeEvent");
+    return PR_FALSE;
+  }
+
+  PRInt32 detail;
+  timeEvent->GetDetail(&detail);
+  return detail == mParams.mRepeatIterationOrAccessKey;
+}
+
 nsSMILTimeValue
 nsSMILTimeValueSpec::ConvertBetweenTimeContainers(
     const nsSMILTimeValue& aSrcTime,
     const nsSMILTimeContainer* aSrcContainer)
 {
   // If the source time is either indefinite or unresolved the result is going
   // to be the same
   if (!aSrcTime.IsResolved())
--- a/content/smil/nsSMILTimeValueSpec.h
+++ b/content/smil/nsSMILTimeValueSpec.h
@@ -93,16 +93,17 @@ protected:
   void UpdateReferencedElement(Element* aFrom, Element* aTo);
   void UnregisterFromReferencedElement(Element* aElement);
   nsSMILTimedElement* GetTimedElement(Element* aElement);
   void RegisterEventListener(Element* aElement);
   void UnregisterEventListener(Element* aElement);
   nsIEventListenerManager* GetEventListenerManager(Element* aElement,
       nsIDOMEventGroup** aSystemGroup);
   void HandleEvent(nsIDOMEvent* aEvent);
+  PRBool CheckEventDetail(nsIDOMEvent* aEvent);
   nsSMILTimeValue ConvertBetweenTimeContainers(const nsSMILTimeValue& aSrcTime,
                                       const nsSMILTimeContainer* aSrcContainer);
 
   nsSMILTimedElement*           mOwner;
   PRPackedBool                  mIsBegin; // Indicates if *we* are a begin spec,
                                           // not to be confused with
                                           // mParams.mSyncBegin which indicates
                                           // if we're synced with the begin of
--- a/content/smil/test/Makefile.in
+++ b/content/smil/test/Makefile.in
@@ -72,16 +72,17 @@ include $(topsrcdir)/config/rules.mk
 	  test_smilMappedAttrPaced.xhtml \
 	  test_smilReset.xhtml \
 	  test_smilRestart.xhtml \
 	  test_smilFillMode.xhtml \
 	  test_smilGetStartTime.xhtml \
 	  test_smilGetSimpleDuration.xhtml \
 	  test_smilKeySplines.xhtml \
 	  test_smilKeyTimesPacedMode.xhtml \
+	  test_smilRepeatTiming.xhtml \
 	  test_smilSetCurrentTime.xhtml \
 	  test_smilSync.xhtml \
 	  test_smilSyncbaseTarget.xhtml \
 	  test_smilSyncTransform.xhtml \
 	  test_smilTextZoom.xhtml \
 	  test_smilTimeEvents.xhtml \
 	  test_smilTiming.xhtml \
 	  test_smilTimingZeroIntervals.xhtml \
new file mode 100644
--- /dev/null
+++ b/content/smil/test/test_smilRepeatTiming.xhtml
@@ -0,0 +1,96 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=485157
+-->
+<head>
+  <title>Test repeat timing</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+  href="https://bugzilla.mozilla.org/show_bug.cgi?id=485157">Mozilla Bug
+  485157</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px">
+  <rect width="100" height="100" fill="green">
+    <set attributeName="width" to="100" dur="20s" repeatCount="5" begin="0s"
+      id="a" onrepeat="startWaiting(evt)"/>
+    <set attributeName="fill" attributeType="CSS" to="green"
+      begin="a.repeat(1)" onbegin="expectedBegin()" dur="20s"/>
+    <set attributeName="x" to="100"
+      begin="a.repeat(2)" onbegin="unexpectedBegin(this)" dur="20s"/>
+    <set attributeName="y" to="100"
+      begin="a.repeat(0)" onbegin="unexpectedBegin(this)" dur="20s"/>
+    <set attributeName="width" to="100"
+      begin="a.repeat(-1)" onbegin="unexpectedBegin(this)" dur="20s"/>
+    <set attributeName="height" to="100"
+      begin="a.repeat(a)" onbegin="unexpectedBegin(this)" dur="20s"/>
+  </rect>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test SMIL repeat timing **/
+
+/* Global Variables */
+const gTimeoutDur = 5000; // Time until we give up waiting for events in ms
+var gSvg  = document.getElementById('svg');
+var gRect = document.getElementById('circle');
+var gTimeoutID;
+var gGotBegin = false;
+
+SimpleTest.waitForExplicitFinish();
+
+function testBegin()
+{
+  gSvg.setCurrentTime(19.999);
+}
+
+function startWaiting(evt)
+{
+  is(evt.detail, 1, "Unexpected repeat event received: test broken");
+  if (gGotBegin)
+    return;
+
+  gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+}
+
+function timeoutFail()
+{
+  ok(false, "Timed out waiting for begin event");
+  finish();
+}
+
+function expectedBegin()
+{
+  is(gGotBegin, false,
+     "Got begin event more than once for non-repeating animation");
+  gGotBegin = true;
+  clearTimeout(gTimeoutID);
+  // Wait a moment before finishing in case there are erroneous events waiting
+  // to be processed.
+  setTimeout(finish, 10);
+}
+
+function unexpectedBegin(elem)
+{
+  ok(false, "Got unexpected begin from animation with spec: " +
+            elem.getAttribute('begin'));
+}
+
+function finish()
+{
+  gSvg.pauseAnimations();
+  SimpleTest.finish();
+}
+
+window.addEventListener("load", testBegin, false);
+]]>
+</script>
+</pre>
+</body>
+</html>