summaryrefslogtreecommitdiffstats
path: root/dom/smil/nsSMILTimeValueSpec.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/smil/nsSMILTimeValueSpec.cpp')
-rw-r--r--dom/smil/nsSMILTimeValueSpec.cpp536
1 files changed, 536 insertions, 0 deletions
diff --git a/dom/smil/nsSMILTimeValueSpec.cpp b/dom/smil/nsSMILTimeValueSpec.cpp
new file mode 100644
index 000000000..6948b5d58
--- /dev/null
+++ b/dom/smil/nsSMILTimeValueSpec.cpp
@@ -0,0 +1,536 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/dom/SVGAnimationElement.h"
+#include "nsSMILTimeValueSpec.h"
+#include "nsSMILInterval.h"
+#include "nsSMILTimeContainer.h"
+#include "nsSMILTimeValue.h"
+#include "nsSMILTimedElement.h"
+#include "nsSMILInstanceTime.h"
+#include "nsSMILParserUtils.h"
+#include "nsIDOMKeyEvent.h"
+#include "nsIDOMTimeEvent.h"
+#include "nsString.h"
+#include <limits>
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+//----------------------------------------------------------------------
+// Nested class: EventListener
+
+NS_IMPL_ISUPPORTS(nsSMILTimeValueSpec::EventListener, nsIDOMEventListener)
+
+NS_IMETHODIMP
+nsSMILTimeValueSpec::EventListener::HandleEvent(nsIDOMEvent* aEvent)
+{
+ if (mSpec) {
+ mSpec->HandleEvent(aEvent);
+ }
+ return NS_OK;
+}
+
+//----------------------------------------------------------------------
+// Implementation
+
+nsSMILTimeValueSpec::nsSMILTimeValueSpec(nsSMILTimedElement& aOwner,
+ bool aIsBegin)
+ : mOwner(&aOwner),
+ mIsBegin(aIsBegin),
+ mReferencedElement(this)
+{
+}
+
+nsSMILTimeValueSpec::~nsSMILTimeValueSpec()
+{
+ UnregisterFromReferencedElement(mReferencedElement.get());
+ if (mEventListener) {
+ mEventListener->Disconnect();
+ mEventListener = nullptr;
+ }
+}
+
+nsresult
+nsSMILTimeValueSpec::SetSpec(const nsAString& aStringSpec,
+ Element* aContextNode)
+{
+ nsSMILTimeValueSpecParams params;
+
+ if (!nsSMILParserUtils::ParseTimeValueSpecParams(aStringSpec, params))
+ return NS_ERROR_FAILURE;
+
+ mParams = params;
+
+ // According to SMIL 3.0:
+ // 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;
+ } else if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) {
+ mParams.mEventSymbol = nsGkAtoms::keypress;
+ }
+
+ ResolveReferences(aContextNode);
+
+ return NS_OK;
+}
+
+void
+nsSMILTimeValueSpec::ResolveReferences(nsIContent* aContextNode)
+{
+ if (mParams.mType != nsSMILTimeValueSpecParams::SYNCBASE && !IsEventBased())
+ return;
+
+ MOZ_ASSERT(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->IsInUncomposedDoc())
+ return;
+
+ // 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.
+ RefPtr<Element> oldReferencedElement = mReferencedElement.get();
+
+ if (mParams.mDependentElemID) {
+ mReferencedElement.ResetWithID(aContextNode,
+ nsDependentAtomString(mParams.mDependentElemID));
+ } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) {
+ Element* target = mOwner->GetTargetElement();
+ mReferencedElement.ResetWithElement(target);
+ } else if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) {
+ nsIDocument* doc = aContextNode->GetUncomposedDoc();
+ MOZ_ASSERT(doc, "We are in the document but current doc is null");
+ mReferencedElement.ResetWithElement(doc->GetRootElement());
+ } else {
+ MOZ_ASSERT(false, "Syncbase or repeat spec without ID");
+ }
+ UpdateReferencedElement(oldReferencedElement, mReferencedElement.get());
+}
+
+bool
+nsSMILTimeValueSpec::IsEventBased() const
+{
+ return mParams.mType == nsSMILTimeValueSpecParams::EVENT ||
+ mParams.mType == nsSMILTimeValueSpecParams::REPEAT ||
+ mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY;
+}
+
+void
+nsSMILTimeValueSpec::HandleNewInterval(nsSMILInterval& aInterval,
+ const nsSMILTimeContainer* aSrcContainer)
+{
+ const nsSMILInstanceTime& baseInstance = mParams.mSyncBegin
+ ? *aInterval.Begin() : *aInterval.End();
+ nsSMILTimeValue newTime =
+ ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer);
+
+ // Apply offset
+ if (!ApplyOffset(newTime)) {
+ NS_WARNING("New time overflows nsSMILTime, ignoring");
+ return;
+ }
+
+ // Create the instance time and register it with the interval
+ RefPtr<nsSMILInstanceTime> newInstance =
+ new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_SYNCBASE, this,
+ &aInterval);
+ mOwner->AddInstanceTime(newInstance, mIsBegin);
+}
+
+void
+nsSMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget)
+{
+ if (!IsEventBased() || mParams.mDependentElemID)
+ return;
+
+ mReferencedElement.ResetWithElement(aNewTarget);
+}
+
+void
+nsSMILTimeValueSpec::HandleChangedInstanceTime(
+ const nsSMILInstanceTime& aBaseTime,
+ const nsSMILTimeContainer* aSrcContainer,
+ nsSMILInstanceTime& aInstanceTimeToUpdate,
+ bool aObjectChanged)
+{
+ // If the instance time is fixed (e.g. because it's being used as the begin
+ // time of an active or postactive interval) we just ignore the change.
+ if (aInstanceTimeToUpdate.IsFixedTime())
+ return;
+
+ nsSMILTimeValue updatedTime =
+ ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer);
+
+ // Apply offset
+ if (!ApplyOffset(updatedTime)) {
+ NS_WARNING("Updated time overflows nsSMILTime, ignoring");
+ return;
+ }
+
+ // The timed element that owns the instance time does the updating so it can
+ // re-sort its array of instance times more efficiently
+ if (aInstanceTimeToUpdate.Time() != updatedTime || aObjectChanged) {
+ mOwner->UpdateInstanceTime(&aInstanceTimeToUpdate, updatedTime, mIsBegin);
+ }
+}
+
+void
+nsSMILTimeValueSpec::HandleDeletedInstanceTime(
+ nsSMILInstanceTime &aInstanceTime)
+{
+ mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin);
+}
+
+bool
+nsSMILTimeValueSpec::DependsOnBegin() const
+{
+ return mParams.mSyncBegin;
+}
+
+void
+nsSMILTimeValueSpec::Traverse(nsCycleCollectionTraversalCallback* aCallback)
+{
+ mReferencedElement.Traverse(aCallback);
+}
+
+void
+nsSMILTimeValueSpec::Unlink()
+{
+ UnregisterFromReferencedElement(mReferencedElement.get());
+ mReferencedElement.Unlink();
+}
+
+//----------------------------------------------------------------------
+// Implementation helpers
+
+void
+nsSMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo)
+{
+ if (aFrom == aTo)
+ return;
+
+ UnregisterFromReferencedElement(aFrom);
+
+ switch (mParams.mType)
+ {
+ case nsSMILTimeValueSpecParams::SYNCBASE:
+ {
+ nsSMILTimedElement* to = GetTimedElement(aTo);
+ if (to) {
+ to->AddDependent(*this);
+ }
+ }
+ break;
+
+ case nsSMILTimeValueSpecParams::EVENT:
+ case nsSMILTimeValueSpecParams::REPEAT:
+ case nsSMILTimeValueSpecParams::ACCESSKEY:
+ RegisterEventListener(aTo);
+ break;
+
+ default:
+ // not a referencing-type
+ 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 (IsEventBased()) {
+ UnregisterEventListener(aElement);
+ }
+}
+
+nsSMILTimedElement*
+nsSMILTimeValueSpec::GetTimedElement(Element* aElement)
+{
+ return aElement && aElement->IsNodeOfType(nsINode::eANIMATION) ?
+ &static_cast<SVGAnimationElement*>(aElement)->TimedElement() : nullptr;
+}
+
+// Indicates whether we're allowed to register an event-listener
+// when scripting is disabled.
+bool
+nsSMILTimeValueSpec::IsWhitelistedEvent()
+{
+ // The category of (SMIL-specific) "repeat(n)" events are allowed.
+ if (mParams.mType == nsSMILTimeValueSpecParams::REPEAT) {
+ return true;
+ }
+
+ // A specific list of other SMIL-related events are allowed, too.
+ if (mParams.mType == nsSMILTimeValueSpecParams::EVENT &&
+ (mParams.mEventSymbol == nsGkAtoms::repeat ||
+ mParams.mEventSymbol == nsGkAtoms::repeatEvent ||
+ mParams.mEventSymbol == nsGkAtoms::beginEvent ||
+ mParams.mEventSymbol == nsGkAtoms::endEvent)) {
+ return true;
+ }
+
+ return false;
+}
+
+void
+nsSMILTimeValueSpec::RegisterEventListener(Element* aTarget)
+{
+ MOZ_ASSERT(IsEventBased(),
+ "Attempting to register event-listener for unexpected "
+ "nsSMILTimeValueSpec type");
+ MOZ_ASSERT(mParams.mEventSymbol,
+ "Attempting to register event-listener but there is no event "
+ "name");
+
+ if (!aTarget)
+ return;
+
+ // When script is disabled, only allow registration for whitelisted events.
+ if (!aTarget->GetOwnerDocument()->IsScriptEnabled() &&
+ !IsWhitelistedEvent()) {
+ return;
+ }
+
+ if (!mEventListener) {
+ mEventListener = new EventListener(this);
+ }
+
+ EventListenerManager* elm = GetEventListenerManager(aTarget);
+ if (!elm)
+ return;
+
+ elm->AddEventListenerByType(mEventListener,
+ nsDependentAtomString(mParams.mEventSymbol),
+ AllEventsAtSystemGroupBubble());
+}
+
+void
+nsSMILTimeValueSpec::UnregisterEventListener(Element* aTarget)
+{
+ if (!aTarget || !mEventListener)
+ return;
+
+ EventListenerManager* elm = GetEventListenerManager(aTarget);
+ if (!elm)
+ return;
+
+ elm->RemoveEventListenerByType(mEventListener,
+ nsDependentAtomString(mParams.mEventSymbol),
+ AllEventsAtSystemGroupBubble());
+}
+
+EventListenerManager*
+nsSMILTimeValueSpec::GetEventListenerManager(Element* aTarget)
+{
+ MOZ_ASSERT(aTarget, "null target; can't get EventListenerManager");
+
+ nsCOMPtr<EventTarget> target;
+
+ if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) {
+ nsIDocument* doc = aTarget->GetUncomposedDoc();
+ if (!doc)
+ return nullptr;
+ nsPIDOMWindowOuter* win = doc->GetWindow();
+ if (!win)
+ return nullptr;
+ target = do_QueryInterface(win);
+ } else {
+ target = aTarget;
+ }
+ if (!target)
+ return nullptr;
+
+ return target->GetOrCreateListenerManager();
+}
+
+void
+nsSMILTimeValueSpec::HandleEvent(nsIDOMEvent* aEvent)
+{
+ MOZ_ASSERT(mEventListener, "Got event without an event listener");
+ MOZ_ASSERT(IsEventBased(),
+ "Got event for non-event nsSMILTimeValueSpec");
+ MOZ_ASSERT(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);
+ if (!ApplyOffset(newTime)) {
+ NS_WARNING("New time generated from event overflows nsSMILTime, ignoring");
+ return;
+ }
+
+ RefPtr<nsSMILInstanceTime> newInstance =
+ new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_EVENT);
+ mOwner->AddInstanceTime(newInstance, mIsBegin);
+}
+
+bool
+nsSMILTimeValueSpec::CheckEventDetail(nsIDOMEvent *aEvent)
+{
+ switch (mParams.mType)
+ {
+ case nsSMILTimeValueSpecParams::REPEAT:
+ return CheckRepeatEventDetail(aEvent);
+
+ case nsSMILTimeValueSpecParams::ACCESSKEY:
+ return CheckAccessKeyEventDetail(aEvent);
+
+ default:
+ // nothing to check
+ return true;
+ }
+}
+
+bool
+nsSMILTimeValueSpec::CheckRepeatEventDetail(nsIDOMEvent *aEvent)
+{
+ nsCOMPtr<nsIDOMTimeEvent> timeEvent = do_QueryInterface(aEvent);
+ if (!timeEvent) {
+ NS_WARNING("Received a repeat event that was not a DOMTimeEvent");
+ return false;
+ }
+
+ int32_t detail;
+ timeEvent->GetDetail(&detail);
+ return detail > 0 && (uint32_t)detail == mParams.mRepeatIterationOrAccessKey;
+}
+
+bool
+nsSMILTimeValueSpec::CheckAccessKeyEventDetail(nsIDOMEvent *aEvent)
+{
+ nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
+ if (!keyEvent) {
+ NS_WARNING("Received an accesskey event that was not a DOMKeyEvent");
+ return false;
+ }
+
+ // Ignore the key event if any modifier keys are pressed UNLESS we're matching
+ // on the charCode in which case we ignore the state of the shift and alt keys
+ // since they might be needed to generate the character in question.
+ bool isCtrl;
+ bool isMeta;
+ keyEvent->GetCtrlKey(&isCtrl);
+ keyEvent->GetMetaKey(&isMeta);
+ if (isCtrl || isMeta)
+ return false;
+
+ uint32_t code;
+ keyEvent->GetCharCode(&code);
+ if (code)
+ return code == mParams.mRepeatIterationOrAccessKey;
+
+ // Only match on the keyCode if it corresponds to some ASCII character that
+ // does not produce a charCode.
+ // In this case we can safely bail out if either alt or shift is pressed since
+ // they won't already be incorporated into the keyCode unlike the charCode.
+ bool isAlt;
+ bool isShift;
+ keyEvent->GetAltKey(&isAlt);
+ keyEvent->GetShiftKey(&isShift);
+ if (isAlt || isShift)
+ return false;
+
+ keyEvent->GetKeyCode(&code);
+ switch (code)
+ {
+ case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
+ return mParams.mRepeatIterationOrAccessKey == 0x08;
+
+ case nsIDOMKeyEvent::DOM_VK_RETURN:
+ return mParams.mRepeatIterationOrAccessKey == 0x0A ||
+ mParams.mRepeatIterationOrAccessKey == 0x0D;
+
+ case nsIDOMKeyEvent::DOM_VK_ESCAPE:
+ return mParams.mRepeatIterationOrAccessKey == 0x1B;
+
+ case nsIDOMKeyEvent::DOM_VK_DELETE:
+ return mParams.mRepeatIterationOrAccessKey == 0x7F;
+
+ default:
+ return false;
+ }
+}
+
+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.IsDefinite())
+ return aSrcTime;
+
+ // Convert from source time container to our parent time container
+ const nsSMILTimeContainer* dstContainer = mOwner->GetTimeContainer();
+ if (dstContainer == aSrcContainer)
+ return aSrcTime;
+
+ // If one of the elements is not attached to a time container then we can't do
+ // any meaningful conversion
+ if (!aSrcContainer || !dstContainer)
+ return nsSMILTimeValue(); // unresolved
+
+ nsSMILTimeValue docTime =
+ aSrcContainer->ContainerToParentTime(aSrcTime.GetMillis());
+
+ if (docTime.IsIndefinite())
+ // This will happen if the source container is paused and we have a future
+ // time. Just return the indefinite time.
+ return docTime;
+
+ MOZ_ASSERT(docTime.IsDefinite(),
+ "ContainerToParentTime gave us an unresolved or indefinite time");
+
+ return dstContainer->ParentToContainerTime(docTime.GetMillis());
+}
+
+bool
+nsSMILTimeValueSpec::ApplyOffset(nsSMILTimeValue& aTime) const
+{
+ // indefinite + offset = indefinite. Likewise for unresolved times.
+ if (!aTime.IsDefinite()) {
+ return true;
+ }
+
+ double resultAsDouble =
+ (double)aTime.GetMillis() + mParams.mOffset.GetMillis();
+ if (resultAsDouble > std::numeric_limits<nsSMILTime>::max() ||
+ resultAsDouble < std::numeric_limits<nsSMILTime>::min()) {
+ return false;
+ }
+ aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis());
+ return true;
+}