diff options
Diffstat (limited to 'dom/smil/nsSMILTimeValueSpec.cpp')
-rw-r--r-- | dom/smil/nsSMILTimeValueSpec.cpp | 536 |
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; +} |