diff options
Diffstat (limited to 'dom/smil/nsSMILTimedElement.cpp')
-rw-r--r-- | dom/smil/nsSMILTimedElement.cpp | 2444 |
1 files changed, 2444 insertions, 0 deletions
diff --git a/dom/smil/nsSMILTimedElement.cpp b/dom/smil/nsSMILTimedElement.cpp new file mode 100644 index 000000000..120536be0 --- /dev/null +++ b/dom/smil/nsSMILTimedElement.cpp @@ -0,0 +1,2444 @@ +/* -*- 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/DebugOnly.h" + +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAutoPtr.h" +#include "nsSMILTimedElement.h" +#include "nsAttrValueInlines.h" +#include "nsSMILAnimationFunction.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILParserUtils.h" +#include "nsSMILTimeContainer.h" +#include "nsGkAtoms.h" +#include "nsReadableUtils.h" +#include "nsMathUtils.h" +#include "nsThreadUtils.h" +#include "nsIPresShell.h" +#include "prdtoa.h" +#include "plstr.h" +#include "prtime.h" +#include "nsString.h" +#include "mozilla/AutoRestore.h" +#include "nsCharSeparatedTokenizer.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Helper class: InstanceTimeComparator + +// Upon inserting an instance time into one of our instance time lists we assign +// it a serial number. This allows us to sort the instance times in such a way +// that where we have several equal instance times, the ones added later will +// sort later. This means that when we call UpdateCurrentInterval during the +// waiting state we won't unnecessarily change the begin instance. +// +// The serial number also means that every instance time has an unambiguous +// position in the array so we can use RemoveElementSorted and the like. +bool +nsSMILTimedElement::InstanceTimeComparator::Equals( + const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const +{ + MOZ_ASSERT(aElem1 && aElem2, + "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + MOZ_ASSERT(aElem1 == aElem2 || aElem1->Serial() != aElem2->Serial(), + "Serial numbers are not unique"); + + return aElem1->Serial() == aElem2->Serial(); +} + +bool +nsSMILTimedElement::InstanceTimeComparator::LessThan( + const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const +{ + MOZ_ASSERT(aElem1 && aElem2, + "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + + int8_t cmp = aElem1->Time().CompareTo(aElem2->Time()); + return cmp == 0 ? aElem1->Serial() < aElem2->Serial() : cmp < 0; +} + +//---------------------------------------------------------------------- +// Helper class: AsyncTimeEventRunner + +namespace +{ + class AsyncTimeEventRunner : public Runnable + { + protected: + RefPtr<nsIContent> mTarget; + EventMessage mMsg; + int32_t mDetail; + + public: + AsyncTimeEventRunner(nsIContent* aTarget, EventMessage aMsg, + int32_t aDetail) + : mTarget(aTarget) + , mMsg(aMsg) + , mDetail(aDetail) + { + } + + NS_IMETHOD Run() override + { + InternalSMILTimeEvent event(true, mMsg); + event.mDetail = mDetail; + + nsPresContext* context = nullptr; + nsIDocument* doc = mTarget->GetUncomposedDoc(); + if (doc) { + nsCOMPtr<nsIPresShell> shell = doc->GetShell(); + if (shell) { + context = shell->GetPresContext(); + } + } + + return EventDispatcher::Dispatch(mTarget, context, &event); + } + }; +} // namespace + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdateBatcher + +// Stack-based helper class to set the mDeferIntervalUpdates flag on an +// nsSMILTimedElement and perform the UpdateCurrentInterval when the object is +// destroyed. +// +// If several of these objects are allocated on the stack, the update will not +// be performed until the last object for a given nsSMILTimedElement is +// destroyed. +class MOZ_STACK_CLASS nsSMILTimedElement::AutoIntervalUpdateBatcher +{ +public: + explicit AutoIntervalUpdateBatcher(nsSMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement), + mDidSetFlag(!aTimedElement.mDeferIntervalUpdates) + { + mTimedElement.mDeferIntervalUpdates = true; + } + + ~AutoIntervalUpdateBatcher() + { + if (!mDidSetFlag) + return; + + mTimedElement.mDeferIntervalUpdates = false; + + if (mTimedElement.mDoDeferredUpdate) { + mTimedElement.mDoDeferredUpdate = false; + mTimedElement.UpdateCurrentInterval(); + } + } + +private: + nsSMILTimedElement& mTimedElement; + bool mDidSetFlag; +}; + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdater + +// Stack-based helper class to call UpdateCurrentInterval when it is destroyed +// which helps avoid bugs where we forget to call UpdateCurrentInterval in the +// case of early returns (e.g. due to parse errors). +// +// This can be safely used in conjunction with AutoIntervalUpdateBatcher; any +// calls to UpdateCurrentInterval made by this class will simply be deferred if +// there is an AutoIntervalUpdateBatcher on the stack. +class MOZ_STACK_CLASS nsSMILTimedElement::AutoIntervalUpdater +{ +public: + explicit AutoIntervalUpdater(nsSMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement) { } + + ~AutoIntervalUpdater() + { + mTimedElement.UpdateCurrentInterval(); + } + +private: + nsSMILTimedElement& mTimedElement; +}; + +//---------------------------------------------------------------------- +// Templated helper functions + +// Selectively remove elements from an array of type +// nsTArray<RefPtr<nsSMILInstanceTime> > with O(n) performance. +template <class TestFunctor> +void +nsSMILTimedElement::RemoveInstanceTimes(InstanceTimeList& aArray, + TestFunctor& aTest) +{ + InstanceTimeList newArray; + for (uint32_t i = 0; i < aArray.Length(); ++i) { + nsSMILInstanceTime* item = aArray[i].get(); + if (aTest(item, i)) { + // As per bugs 665334 and 669225 we should be careful not to remove the + // instance time that corresponds to the previous interval's end time. + // + // Most functors supplied here fulfil this condition by checking if the + // instance time is marked as "ShouldPreserve" and if so, not deleting it. + // + // However, when filtering instance times, we sometimes need to drop even + // instance times marked as "ShouldPreserve". In that case we take special + // care not to delete the end instance time of the previous interval. + MOZ_ASSERT(!GetPreviousInterval() || item != GetPreviousInterval()->End(), + "Removing end instance time of previous interval"); + item->Unlink(); + } else { + newArray.AppendElement(item); + } + } + aArray.Clear(); + aArray.SwapElements(newArray); +} + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable nsSMILTimedElement::sFillModeTable[] = { + {"remove", FILL_REMOVE}, + {"freeze", FILL_FREEZE}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILTimedElement::sRestartModeTable[] = { + {"always", RESTART_ALWAYS}, + {"whenNotActive", RESTART_WHENNOTACTIVE}, + {"never", RESTART_NEVER}, + {nullptr, 0} +}; + +const nsSMILMilestone nsSMILTimedElement::sMaxMilestone(INT64_MAX, false); + +// The thresholds at which point we start filtering intervals and instance times +// indiscriminately. +// See FilterIntervals and FilterInstanceTimes. +const uint8_t nsSMILTimedElement::sMaxNumIntervals = 20; +const uint8_t nsSMILTimedElement::sMaxNumInstanceTimes = 100; + +// Detect if we arrive in some sort of undetected recursive syncbase dependency +// relationship +const uint8_t nsSMILTimedElement::sMaxUpdateIntervalRecursionDepth = 20; + +//---------------------------------------------------------------------- +// Ctor, dtor + +nsSMILTimedElement::nsSMILTimedElement() +: + mAnimationElement(nullptr), + mFillMode(FILL_REMOVE), + mRestartMode(RESTART_ALWAYS), + mInstanceSerialIndex(0), + mClient(nullptr), + mCurrentInterval(nullptr), + mCurrentRepeatIteration(0), + mPrevRegisteredMilestone(sMaxMilestone), + mElementState(STATE_STARTUP), + mSeekState(SEEK_NOT_SEEKING), + mDeferIntervalUpdates(false), + mDoDeferredUpdate(false), + mIsDisabled(false), + mDeleteCount(0), + mUpdateIntervalRecursionDepth(0) +{ + mSimpleDur.SetIndefinite(); + mMin.SetMillis(0L); + mMax.SetIndefinite(); +} + +nsSMILTimedElement::~nsSMILTimedElement() +{ + // Unlink all instance times from dependent intervals + for (uint32_t i = 0; i < mBeginInstances.Length(); ++i) { + mBeginInstances[i]->Unlink(); + } + mBeginInstances.Clear(); + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + mEndInstances[i]->Unlink(); + } + mEndInstances.Clear(); + + // Notify anyone listening to our intervals that they're gone + // (We shouldn't get any callbacks from this because all our instance times + // are now disassociated with any intervals) + ClearIntervals(); + + // The following assertions are important in their own right (for checking + // correct behavior) but also because AutoIntervalUpdateBatcher holds pointers + // to class so if they fail there's the possibility we might have dangling + // pointers. + MOZ_ASSERT(!mDeferIntervalUpdates, + "Interval updates should no longer be blocked when an " + "nsSMILTimedElement disappears"); + MOZ_ASSERT(!mDoDeferredUpdate, + "There should no longer be any pending updates when an " + "nsSMILTimedElement disappears"); +} + +void +nsSMILTimedElement::SetAnimationElement(SVGAnimationElement* aElement) +{ + MOZ_ASSERT(aElement, "NULL owner element"); + MOZ_ASSERT(!mAnimationElement, "Re-setting owner"); + mAnimationElement = aElement; +} + +nsSMILTimeContainer* +nsSMILTimedElement::GetTimeContainer() +{ + return mAnimationElement ? mAnimationElement->GetTimeContainer() : nullptr; +} + +dom::Element* +nsSMILTimedElement::GetTargetElement() +{ + return mAnimationElement ? + mAnimationElement->GetTargetElementContent() : + nullptr; +} + +//---------------------------------------------------------------------- +// nsIDOMElementTimeControl methods +// +// The definition of the ElementTimeControl interface differs between SMIL +// Animation and SVG 1.1. In SMIL Animation all methods have a void return +// type and the new instance time is simply added to the list and restart +// semantics are applied as with any other instance time. In the SVG definition +// the methods return a bool depending on the restart mode. +// +// This inconsistency has now been addressed by an erratum in SVG 1.1: +// +// http://www.w3.org/2003/01/REC-SVG11-20030114-errata#elementtimecontrol-interface +// +// which favours the definition in SMIL, i.e. instance times are just added +// without first checking the restart mode. + +nsresult +nsSMILTimedElement::BeginElementAt(double aOffsetSeconds) +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return NS_ERROR_FAILURE; + + nsSMILTime currentTime = container->GetCurrentTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true); +} + +nsresult +nsSMILTimedElement::EndElementAt(double aOffsetSeconds) +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return NS_ERROR_FAILURE; + + nsSMILTime currentTime = container->GetCurrentTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false); +} + +//---------------------------------------------------------------------- +// nsSVGAnimationElement methods + +nsSMILTimeValue +nsSMILTimedElement::GetStartTime() const +{ + return mElementState == STATE_WAITING || mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin()->Time() + : nsSMILTimeValue(); +} + +//---------------------------------------------------------------------- +// Hyperlinking support + +nsSMILTimeValue +nsSMILTimedElement::GetHyperlinkTime() const +{ + nsSMILTimeValue hyperlinkTime; // Default ctor creates unresolved time + + if (mElementState == STATE_ACTIVE) { + hyperlinkTime = mCurrentInterval->Begin()->Time(); + } else if (!mBeginInstances.IsEmpty()) { + hyperlinkTime = mBeginInstances[0]->Time(); + } + + return hyperlinkTime; +} + +//---------------------------------------------------------------------- +// nsSMILTimedElement + +void +nsSMILTimedElement::AddInstanceTime(nsSMILInstanceTime* aInstanceTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to add null instance time"); + + // Event-sensitivity: If an element is not active (but the parent time + // container is), then events are only handled for begin specifications. + if (mElementState != STATE_ACTIVE && !aIsBegin && + aInstanceTime->IsDynamic()) + { + // No need to call Unlink here--dynamic instance times shouldn't be linked + // to anything that's going to miss them + MOZ_ASSERT(!aInstanceTime->GetBaseInterval(), + "Dynamic instance time has a base interval--we probably need " + "to unlink it if we're not going to use it"); + return; + } + + aInstanceTime->SetSerial(++mInstanceSerialIndex); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + RefPtr<nsSMILInstanceTime>* inserted = + instanceList.InsertElementSorted(aInstanceTime, InstanceTimeComparator()); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time"); + return; + } + + UpdateCurrentInterval(); +} + +void +nsSMILTimedElement::UpdateInstanceTime(nsSMILInstanceTime* aInstanceTime, + nsSMILTimeValue& aUpdatedTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to update null instance time"); + + // The reason we update the time here and not in the nsSMILTimeValueSpec is + // that it means we *could* re-sort more efficiently by doing a sorted remove + // and insert but currently this doesn't seem to be necessary given how + // infrequently we get these change notices. + aInstanceTime->DependentUpdate(aUpdatedTime); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + instanceList.Sort(InstanceTimeComparator()); + + // Generally speaking, UpdateCurrentInterval makes changes to the current + // interval and sends changes notices itself. However, in this case because + // instance times are shared between the instance time list and the intervals + // we are effectively changing the current interval outside + // UpdateCurrentInterval so we need to explicitly signal that we've made + // a change. + // + // This wouldn't be necessary if we cloned instance times on adding them to + // the current interval but this introduces other complications (particularly + // detecting which instance time is being used to define the begin of the + // current interval when doing a Reset). + bool changedCurrentInterval = mCurrentInterval && + (mCurrentInterval->Begin() == aInstanceTime || + mCurrentInterval->End() == aInstanceTime); + + UpdateCurrentInterval(changedCurrentInterval); +} + +void +nsSMILTimedElement::RemoveInstanceTime(nsSMILInstanceTime* aInstanceTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to remove null instance time"); + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return; + } + + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + mozilla::DebugOnly<bool> found = + instanceList.RemoveElementSorted(aInstanceTime, InstanceTimeComparator()); + MOZ_ASSERT(found, "Couldn't find instance time to delete"); + + UpdateCurrentInterval(); +} + +namespace +{ + class MOZ_STACK_CLASS RemoveByCreator + { + public: + explicit RemoveByCreator(const nsSMILTimeValueSpec* aCreator) : mCreator(aCreator) + { } + + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + if (aInstanceTime->GetCreator() != mCreator) + return false; + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return false; + } + + return true; + } + + private: + const nsSMILTimeValueSpec* mCreator; + }; +} // namespace + +void +nsSMILTimedElement::RemoveInstanceTimesForCreator( + const nsSMILTimeValueSpec* aCreator, bool aIsBegin) +{ + MOZ_ASSERT(aCreator, "Creator not set"); + + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + RemoveByCreator removeByCreator(aCreator); + RemoveInstanceTimes(instances, removeByCreator); + + UpdateCurrentInterval(); +} + +void +nsSMILTimedElement::SetTimeClient(nsSMILAnimationFunction* aClient) +{ + // + // No need to check for nullptr. A nullptr parameter simply means to remove the + // previous client which we do by setting to nullptr anyway. + // + + mClient = aClient; +} + +void +nsSMILTimedElement::SampleAt(nsSMILTime aContainerTime) +{ + if (mIsDisabled) + return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + DoSampleAt(aContainerTime, false); +} + +void +nsSMILTimedElement::SampleEndAt(nsSMILTime aContainerTime) +{ + if (mIsDisabled) + return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + // If the current interval changes, we don't bother trying to remove any old + // milestones we'd registered. So it's possible to get a call here to end an + // interval at a time that no longer reflects the end of the current interval. + // + // For now we just check that we're actually in an interval but note that the + // initial sample we use to initialise the model is an end sample. This is + // because we want to resolve all the instance times before committing to an + // initial interval. Therefore an end sample from the startup state is also + // acceptable. + if (mElementState == STATE_ACTIVE || mElementState == STATE_STARTUP) { + DoSampleAt(aContainerTime, true); // End sample + } else { + // Even if this was an unnecessary milestone sample we want to be sure that + // our next real milestone is registered. + RegisterMilestone(); + } +} + +void +nsSMILTimedElement::DoSampleAt(nsSMILTime aContainerTime, bool aEndOnly) +{ + MOZ_ASSERT(mAnimationElement, + "Got sample before being registered with an animation element"); + MOZ_ASSERT(GetTimeContainer(), + "Got sample without being registered with a time container"); + + // This could probably happen if we later implement externalResourcesRequired + // (bug 277955) and whilst waiting for those resources (and the animation to + // start) we transfer a node from another document fragment that has already + // started. In such a case we might receive milestone samples registered with + // the already active container. + if (GetTimeContainer()->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) + return; + + // We use an end-sample to start animation since an end-sample lets us + // tentatively create an interval without committing to it (by transitioning + // to the ACTIVE state) and this is necessary because we might have + // dependencies on other animations that are yet to start. After these + // other animations start, it may be necessary to revise our initial interval. + // + // However, sometimes instead of an end-sample we can get a regular sample + // during STARTUP state. This can happen, for example, if we register + // a milestone before time t=0 and are then re-bound to the tree (which sends + // us back to the STARTUP state). In such a case we should just ignore the + // sample and wait for our real initial sample which will be an end-sample. + if (mElementState == STATE_STARTUP && !aEndOnly) + return; + + bool finishedSeek = false; + if (GetTimeContainer()->IsSeeking() && mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? + SEEK_FORWARD_FROM_ACTIVE : + SEEK_FORWARD_FROM_INACTIVE; + } else if (mSeekState != SEEK_NOT_SEEKING && + !GetTimeContainer()->IsSeeking()) { + finishedSeek = true; + } + + bool stateChanged; + nsSMILTimeValue sampleTime(aContainerTime); + + do { +#ifdef DEBUG + // Check invariant + if (mElementState == STATE_STARTUP || mElementState == STATE_POSTACTIVE) { + MOZ_ASSERT(!mCurrentInterval, + "Shouldn't have current interval in startup or postactive " + "states"); + } else { + MOZ_ASSERT(mCurrentInterval, + "Should have current interval in waiting and active states"); + } +#endif + + stateChanged = false; + + switch (mElementState) + { + case STATE_STARTUP: + { + nsSMILInterval firstInterval; + mElementState = GetNextInterval(nullptr, nullptr, nullptr, firstInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + stateChanged = true; + if (mElementState == STATE_WAITING) { + mCurrentInterval = new nsSMILInterval(firstInterval); + NotifyNewInterval(); + } + } + break; + + case STATE_WAITING: + { + if (mCurrentInterval->Begin()->Time() <= sampleTime) { + mElementState = STATE_ACTIVE; + mCurrentInterval->FixBegin(); + if (mClient) { + mClient->Activate(mCurrentInterval->Begin()->Time().GetMillis()); + } + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + if (HasPlayed()) { + Reset(); // Apply restart behaviour + // The call to Reset() may mean that the end point of our current + // interval should be changed and so we should update the interval + // now. However, calling UpdateCurrentInterval could result in the + // interval getting deleted (perhaps through some web of syncbase + // dependencies) therefore we make updating the interval the last + // thing we do. There is no guarantee that mCurrentInterval is set + // after this. + UpdateCurrentInterval(); + } + stateChanged = true; + } + } + break; + + case STATE_ACTIVE: + { + // Ending early will change the interval but we don't notify dependents + // of the change until we have closed off the current interval (since we + // don't want dependencies to un-end our early end). + bool didApplyEarlyEnd = ApplyEarlyEnd(sampleTime); + + if (mCurrentInterval->End()->Time() <= sampleTime) { + nsSMILInterval newInterval; + mElementState = + GetNextInterval(mCurrentInterval, nullptr, nullptr, newInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + if (mClient) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + mCurrentInterval->FixEnd(); + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + mCurrentRepeatIteration = 0; + mOldIntervals.AppendElement(mCurrentInterval.forget()); + SampleFillValue(); + if (mElementState == STATE_WAITING) { + mCurrentInterval = new nsSMILInterval(newInterval); + } + // We are now in a consistent state to dispatch notifications + if (didApplyEarlyEnd) { + NotifyChangedInterval( + mOldIntervals[mOldIntervals.Length() - 1], false, true); + } + if (mElementState == STATE_WAITING) { + NotifyNewInterval(); + } + FilterHistory(); + stateChanged = true; + } else { + MOZ_ASSERT(!didApplyEarlyEnd, + "We got an early end, but didn't end"); + nsSMILTime beginTime = mCurrentInterval->Begin()->Time().GetMillis(); + NS_ASSERTION(aContainerTime >= beginTime, + "Sample time should not precede current interval"); + nsSMILTime activeTime = aContainerTime - beginTime; + + // The 'min' attribute can cause the active interval to be longer than + // the 'repeating interval'. + // In that extended period we apply the fill mode. + if (GetRepeatDuration() <= nsSMILTimeValue(activeTime)) { + if (mClient && mClient->IsActive()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + SampleFillValue(); + } else { + SampleSimpleTime(activeTime); + + // We register our repeat times as milestones (except when we're + // seeking) so we should get a sample at exactly the time we repeat. + // (And even when we are seeking we want to update + // mCurrentRepeatIteration so we do that first before testing the + // seek state.) + uint32_t prevRepeatIteration = mCurrentRepeatIteration; + if ( + ActiveTimeToSimpleTime(activeTime, mCurrentRepeatIteration)==0 && + mCurrentRepeatIteration != prevRepeatIteration && + mCurrentRepeatIteration && + mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILRepeatEvent, + static_cast<int32_t>(mCurrentRepeatIteration)); + } + } + } + } + break; + + case STATE_POSTACTIVE: + break; + } + + // Generally we continue driving the state machine so long as we have changed + // state. However, for end samples we only drive the state machine as far as + // the waiting or postactive state because we don't want to commit to any new + // interval (by transitioning to the active state) until all the end samples + // have finished and we then have complete information about the available + // instance times upon which to base our next interval. + } while (stateChanged && (!aEndOnly || (mElementState != STATE_WAITING && + mElementState != STATE_POSTACTIVE))); + + if (finishedSeek) { + DoPostSeek(); + } + RegisterMilestone(); +} + +void +nsSMILTimedElement::HandleContainerTimeChange() +{ + // In future we could possibly introduce a separate change notice for time + // container changes and only notify those dependents who live in other time + // containers. For now we don't bother because when we re-resolve the time in + // the nsSMILTimeValueSpec we'll check if anything has changed and if not, we + // won't go any further. + if (mElementState == STATE_WAITING || mElementState == STATE_ACTIVE) { + NotifyChangedInterval(mCurrentInterval, false, false); + } +} + +namespace +{ + bool + RemoveNonDynamic(nsSMILInstanceTime* aInstanceTime) + { + // Generally dynamically-generated instance times (DOM calls, event-based + // times) are not associated with their creator nsSMILTimeValueSpec since + // they may outlive them. + MOZ_ASSERT(!aInstanceTime->IsDynamic() || !aInstanceTime->GetCreator(), + "Dynamic instance time should be unlinked from its creator"); + return !aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve(); + } +} // namespace + +void +nsSMILTimedElement::Rewind() +{ + MOZ_ASSERT(mAnimationElement, + "Got rewind request before being attached to an animation " + "element"); + + // It's possible to get a rewind request whilst we're already in the middle of + // a backwards seek. This can happen when we're performing tree surgery and + // seeking containers at the same time because we can end up requesting + // a local rewind on an element after binding it to a new container and then + // performing a rewind on that container as a whole without sampling in + // between. + // + // However, it should currently be impossible to get a rewind in the middle of + // a forwards seek since forwards seeks are detected and processed within the + // same (re)sample. + if (mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? + SEEK_BACKWARD_FROM_ACTIVE : + SEEK_BACKWARD_FROM_INACTIVE; + } + MOZ_ASSERT(mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE, + "Rewind in the middle of a forwards seek?"); + + ClearTimingState(RemoveNonDynamic); + RebuildTimingState(RemoveNonDynamic); + + MOZ_ASSERT(!mCurrentInterval, + "Current interval is set at end of rewind"); +} + +namespace +{ + bool + RemoveAll(nsSMILInstanceTime* aInstanceTime) + { + return true; + } +} // namespace + +bool +nsSMILTimedElement::SetIsDisabled(bool aIsDisabled) +{ + if (mIsDisabled == aIsDisabled) + return false; + + if (aIsDisabled) { + mIsDisabled = true; + ClearTimingState(RemoveAll); + } else { + RebuildTimingState(RemoveAll); + mIsDisabled = false; + } + return true; +} + +namespace +{ + bool + RemoveNonDOM(nsSMILInstanceTime* aInstanceTime) + { + return !aInstanceTime->FromDOM() && !aInstanceTime->ShouldPreserve(); + } +} // namespace + +bool +nsSMILTimedElement::SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, + Element* aContextNode, + nsresult* aParseResult) +{ + bool foundMatch = true; + nsresult parseResult = NS_OK; + + if (aAttribute == nsGkAtoms::begin) { + parseResult = SetBeginSpec(aValue, aContextNode, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + parseResult = SetSimpleDuration(aValue); + } else if (aAttribute == nsGkAtoms::end) { + parseResult = SetEndSpec(aValue, aContextNode, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + parseResult = SetFillMode(aValue); + } else if (aAttribute == nsGkAtoms::max) { + parseResult = SetMax(aValue); + } else if (aAttribute == nsGkAtoms::min) { + parseResult = SetMin(aValue); + } else if (aAttribute == nsGkAtoms::repeatCount) { + parseResult = SetRepeatCount(aValue); + } else if (aAttribute == nsGkAtoms::repeatDur) { + parseResult = SetRepeatDur(aValue); + } else if (aAttribute == nsGkAtoms::restart) { + parseResult = SetRestart(aValue); + } else { + foundMatch = false; + } + + if (foundMatch) { + aResult.SetTo(aValue); + if (aParseResult) { + *aParseResult = parseResult; + } + } + + return foundMatch; +} + +bool +nsSMILTimedElement::UnsetAttr(nsIAtom* aAttribute) +{ + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::begin) { + UnsetBeginSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + UnsetSimpleDuration(); + } else if (aAttribute == nsGkAtoms::end) { + UnsetEndSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + UnsetFillMode(); + } else if (aAttribute == nsGkAtoms::max) { + UnsetMax(); + } else if (aAttribute == nsGkAtoms::min) { + UnsetMin(); + } else if (aAttribute == nsGkAtoms::repeatCount) { + UnsetRepeatCount(); + } else if (aAttribute == nsGkAtoms::repeatDur) { + UnsetRepeatDur(); + } else if (aAttribute == nsGkAtoms::restart) { + UnsetRestart(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +//---------------------------------------------------------------------- +// Setters and unsetters + +nsresult +nsSMILTimedElement::SetBeginSpec(const nsAString& aBeginSpec, + Element* aContextNode, + RemovalTestFunction aRemove) +{ + return SetBeginOrEndSpec(aBeginSpec, aContextNode, true /*isBegin*/, + aRemove); +} + +void +nsSMILTimedElement::UnsetBeginSpec(RemovalTestFunction aRemove) +{ + ClearSpecs(mBeginSpecs, mBeginInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetEndSpec(const nsAString& aEndSpec, + Element* aContextNode, + RemovalTestFunction aRemove) +{ + return SetBeginOrEndSpec(aEndSpec, aContextNode, false /*!isBegin*/, + aRemove); +} + +void +nsSMILTimedElement::UnsetEndSpec(RemovalTestFunction aRemove) +{ + ClearSpecs(mEndSpecs, mEndInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetSimpleDuration(const nsAString& aDurSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& dur = nsSMILParserUtils::TrimWhitespace(aDurSpec); + + // SVG-specific: "For SVG's animation elements, if "media" is specified, the + // attribute will be ignored." (SVG 1.1, section 19.2.6) + if (dur.EqualsLiteral("media") || dur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(dur, &duration) || + duration.GetMillis() == 0L) { + mSimpleDur.SetIndefinite(); + return NS_ERROR_FAILURE; + } + } + // mSimpleDur should never be unresolved. ParseClockValue will either set + // duration to resolved or will return false. + MOZ_ASSERT(duration.IsResolved(), + "Setting unresolved simple duration"); + + mSimpleDur = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetSimpleDuration() +{ + mSimpleDur.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetMin(const nsAString& aMinSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& min = nsSMILParserUtils::TrimWhitespace(aMinSpec); + + if (min.EqualsLiteral("media")) { + duration.SetMillis(0L); + } else { + if (!nsSMILParserUtils::ParseClockValue(min, &duration)) { + mMin.SetMillis(0L); + return NS_ERROR_FAILURE; + } + } + + MOZ_ASSERT(duration.GetMillis() >= 0L, "Invalid duration"); + + mMin = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetMin() +{ + mMin.SetMillis(0L); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetMax(const nsAString& aMaxSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& max = nsSMILParserUtils::TrimWhitespace(aMaxSpec); + + if (max.EqualsLiteral("media") || max.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(max, &duration) || + duration.GetMillis() == 0L) { + mMax.SetIndefinite(); + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(duration.GetMillis() > 0L, "Invalid duration"); + } + + mMax = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetMax() +{ + mMax.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRestart(const nsAString& aRestartSpec) +{ + nsAttrValue temp; + bool parseResult + = temp.ParseEnumValue(aRestartSpec, sRestartModeTable, true); + mRestartMode = parseResult + ? nsSMILRestartMode(temp.GetEnumValue()) + : RESTART_ALWAYS; + UpdateCurrentInterval(); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetRestart() +{ + mRestartMode = RESTART_ALWAYS; + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRepeatCount(const nsAString& aRepeatCountSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILRepeatCount newRepeatCount; + + if (nsSMILParserUtils::ParseRepeatCount(aRepeatCountSpec, newRepeatCount)) { + mRepeatCount = newRepeatCount; + return NS_OK; + } + mRepeatCount.Unset(); + return NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetRepeatCount() +{ + mRepeatCount.Unset(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRepeatDur(const nsAString& aRepeatDurSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + + const nsAString& repeatDur = + nsSMILParserUtils::TrimWhitespace(aRepeatDurSpec); + + if (repeatDur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(repeatDur, &duration)) { + mRepeatDur.SetUnresolved(); + return NS_ERROR_FAILURE; + } + } + + mRepeatDur = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetRepeatDur() +{ + mRepeatDur.SetUnresolved(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetFillMode(const nsAString& aFillModeSpec) +{ + uint16_t previousFillMode = mFillMode; + + nsAttrValue temp; + bool parseResult = + temp.ParseEnumValue(aFillModeSpec, sFillModeTable, true); + mFillMode = parseResult + ? nsSMILFillMode(temp.GetEnumValue()) + : FILL_REMOVE; + + // Update fill mode of client + if (mFillMode != previousFillMode && HasClientInFillRange()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + SampleFillValue(); + } + + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetFillMode() +{ + uint16_t previousFillMode = mFillMode; + mFillMode = FILL_REMOVE; + if (previousFillMode == FILL_FREEZE && HasClientInFillRange()) { + mClient->Inactivate(false); + } +} + +void +nsSMILTimedElement::AddDependent(nsSMILTimeValueSpec& aDependent) +{ + // There's probably no harm in attempting to register a dependent + // nsSMILTimeValueSpec twice, but we're not expecting it to happen. + MOZ_ASSERT(!mTimeDependents.GetEntry(&aDependent), + "nsSMILTimeValueSpec is already registered as a dependency"); + mTimeDependents.PutEntry(&aDependent); + + // Add current interval. We could add historical intervals too but that would + // cause unpredictable results since some intervals may have been filtered. + // SMIL doesn't say what to do here so for simplicity and consistency we + // simply add the current interval if there is one. + // + // It's not necessary to call SyncPauseTime since we're dealing with + // historical instance times not newly added ones. + if (mCurrentInterval) { + aDependent.HandleNewInterval(*mCurrentInterval, GetTimeContainer()); + } +} + +void +nsSMILTimedElement::RemoveDependent(nsSMILTimeValueSpec& aDependent) +{ + mTimeDependents.RemoveEntry(&aDependent); +} + +bool +nsSMILTimedElement::IsTimeDependent(const nsSMILTimedElement& aOther) const +{ + const nsSMILInstanceTime* thisBegin = GetEffectiveBeginInstance(); + const nsSMILInstanceTime* otherBegin = aOther.GetEffectiveBeginInstance(); + + if (!thisBegin || !otherBegin) + return false; + + return thisBegin->IsDependentOn(*otherBegin); +} + +void +nsSMILTimedElement::BindToTree(nsIContent* aContextNode) +{ + // Reset previously registered milestone since we may be registering with + // a different time container now. + mPrevRegisteredMilestone = sMaxMilestone; + + // If we were already active then clear all our timing information and start + // afresh + if (mElementState != STATE_STARTUP) { + mSeekState = SEEK_NOT_SEEKING; + Rewind(); + } + + // Scope updateBatcher to last only for the ResolveReferences calls: + { + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Resolve references to other parts of the tree + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->ResolveReferences(aContextNode); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->ResolveReferences(aContextNode); + } + } + + RegisterMilestone(); +} + +void +nsSMILTimedElement::HandleTargetElementChange(Element* aNewTarget) +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->HandleTargetElementChange(aNewTarget); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->HandleTargetElementChange(aNewTarget); + } +} + +void +nsSMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILTimeValueSpec* beginSpec = mBeginSpecs[i]; + MOZ_ASSERT(beginSpec, + "null nsSMILTimeValueSpec in list of begin specs"); + beginSpec->Traverse(aCallback); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + nsSMILTimeValueSpec* endSpec = mEndSpecs[j]; + MOZ_ASSERT(endSpec, "null nsSMILTimeValueSpec in list of end specs"); + endSpec->Traverse(aCallback); + } +} + +void +nsSMILTimedElement::Unlink() +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Remove dependencies on other elements + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILTimeValueSpec* beginSpec = mBeginSpecs[i]; + MOZ_ASSERT(beginSpec, + "null nsSMILTimeValueSpec in list of begin specs"); + beginSpec->Unlink(); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + nsSMILTimeValueSpec* endSpec = mEndSpecs[j]; + MOZ_ASSERT(endSpec, "null nsSMILTimeValueSpec in list of end specs"); + endSpec->Unlink(); + } + + ClearIntervals(); + + // Make sure we don't notify other elements of new intervals + mTimeDependents.Clear(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult +nsSMILTimedElement::SetBeginOrEndSpec(const nsAString& aSpec, + Element* aContextNode, + bool aIsBegin, + RemovalTestFunction aRemove) +{ + TimeValueSpecList& timeSpecsList = aIsBegin ? mBeginSpecs : mEndSpecs; + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + + ClearSpecs(timeSpecsList, instances, aRemove); + + AutoIntervalUpdateBatcher updateBatcher(*this); + + nsCharSeparatedTokenizer tokenizer(aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_OK; + while (tokenizer.hasMoreTokens() && NS_SUCCEEDED(rv)) { + nsAutoPtr<nsSMILTimeValueSpec> + spec(new nsSMILTimeValueSpec(*this, aIsBegin)); + rv = spec->SetSpec(tokenizer.nextToken(), aContextNode); + if (NS_SUCCEEDED(rv)) { + timeSpecsList.AppendElement(spec.forget()); + } + } + + if (NS_FAILED(rv)) { + ClearSpecs(timeSpecsList, instances, aRemove); + } + + return rv; +} + +namespace +{ + // Adaptor functor for RemoveInstanceTimes that allows us to use function + // pointers instead. + // Without this we'd have to either templatize ClearSpecs and all its callers + // or pass bool flags around to specify which removal function to use here. + class MOZ_STACK_CLASS RemoveByFunction + { + public: + explicit RemoveByFunction(nsSMILTimedElement::RemovalTestFunction aFunction) + : mFunction(aFunction) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + return mFunction(aInstanceTime); + } + + private: + nsSMILTimedElement::RemovalTestFunction mFunction; + }; +} // namespace + +void +nsSMILTimedElement::ClearSpecs(TimeValueSpecList& aSpecs, + InstanceTimeList& aInstances, + RemovalTestFunction aRemove) +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + for (uint32_t i = 0; i < aSpecs.Length(); ++i) { + aSpecs[i]->Unlink(); + } + aSpecs.Clear(); + + RemoveByFunction removeByFunction(aRemove); + RemoveInstanceTimes(aInstances, removeByFunction); +} + +void +nsSMILTimedElement::ClearIntervals() +{ + if (mElementState != STATE_STARTUP) { + mElementState = STATE_POSTACTIVE; + } + mCurrentRepeatIteration = 0; + ResetCurrentInterval(); + + // Remove old intervals + for (int32_t i = mOldIntervals.Length() - 1; i >= 0; --i) { + mOldIntervals[i]->Unlink(); + } + mOldIntervals.Clear(); +} + +bool +nsSMILTimedElement::ApplyEarlyEnd(const nsSMILTimeValue& aSampleTime) +{ + // This should only be called within DoSampleAt as a helper function + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Unexpected state to try to apply an early end"); + + bool updated = false; + + // Only apply an early end if we're not already ending. + if (mCurrentInterval->End()->Time() > aSampleTime) { + nsSMILInstanceTime* earlyEnd = CheckForEarlyEnd(aSampleTime); + if (earlyEnd) { + if (earlyEnd->IsDependent()) { + // Generate a new instance time for the early end since the + // existing instance time is part of some dependency chain that we + // don't want to participate in. + RefPtr<nsSMILInstanceTime> newEarlyEnd = + new nsSMILInstanceTime(earlyEnd->Time()); + mCurrentInterval->SetEnd(*newEarlyEnd); + } else { + mCurrentInterval->SetEnd(*earlyEnd); + } + updated = true; + } + } + return updated; +} + +namespace +{ + class MOZ_STACK_CLASS RemoveReset + { + public: + explicit RemoveReset(const nsSMILInstanceTime* aCurrentIntervalBegin) + : mCurrentIntervalBegin(aCurrentIntervalBegin) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + // SMIL 3.0 section 5.4.3, 'Resetting element state': + // Any instance times associated with past Event-values, Repeat-values, + // Accesskey-values or added via DOM method calls are removed from the + // dependent begin and end instance times lists. In effect, all events + // and DOM methods calls in the past are cleared. This does not apply to + // an instance time that defines the begin of the current interval. + return aInstanceTime->IsDynamic() && + !aInstanceTime->ShouldPreserve() && + (!mCurrentIntervalBegin || aInstanceTime != mCurrentIntervalBegin); + } + + private: + const nsSMILInstanceTime* mCurrentIntervalBegin; + }; +} // namespace + +void +nsSMILTimedElement::Reset() +{ + RemoveReset resetBegin(mCurrentInterval ? mCurrentInterval->Begin() : nullptr); + RemoveInstanceTimes(mBeginInstances, resetBegin); + + RemoveReset resetEnd(nullptr); + RemoveInstanceTimes(mEndInstances, resetEnd); +} + +void +nsSMILTimedElement::ClearTimingState(RemovalTestFunction aRemove) +{ + mElementState = STATE_STARTUP; + ClearIntervals(); + + UnsetBeginSpec(aRemove); + UnsetEndSpec(aRemove); + + if (mClient) { + mClient->Inactivate(false); + } +} + +void +nsSMILTimedElement::RebuildTimingState(RemovalTestFunction aRemove) +{ + MOZ_ASSERT(mAnimationElement, + "Attempting to enable a timed element not attached to an " + "animation element"); + MOZ_ASSERT(mElementState == STATE_STARTUP, + "Rebuilding timing state from non-startup state"); + + if (mAnimationElement->HasAnimAttr(nsGkAtoms::begin)) { + nsAutoString attValue; + mAnimationElement->GetAnimAttr(nsGkAtoms::begin, attValue); + SetBeginSpec(attValue, mAnimationElement, aRemove); + } + + if (mAnimationElement->HasAnimAttr(nsGkAtoms::end)) { + nsAutoString attValue; + mAnimationElement->GetAnimAttr(nsGkAtoms::end, attValue); + SetEndSpec(attValue, mAnimationElement, aRemove); + } + + mPrevRegisteredMilestone = sMaxMilestone; + RegisterMilestone(); +} + +void +nsSMILTimedElement::DoPostSeek() +{ + // Finish backwards seek + if (mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE) { + // Previously some dynamic instance times may have been marked to be + // preserved because they were endpoints of an historic interval (which may + // or may not have been filtered). Now that we've finished a seek we should + // clear that flag for those instance times whose intervals are no longer + // historic. + UnpreserveInstanceTimes(mBeginInstances); + UnpreserveInstanceTimes(mEndInstances); + + // Now that the times have been unmarked perform a reset. This might seem + // counter-intuitive when we're only doing a seek within an interval but + // SMIL seems to require this. SMIL 3.0, 'Hyperlinks and timing': + // Resolved end times associated with events, Repeat-values, + // Accesskey-values or added via DOM method calls are cleared when seeking + // to time earlier than the resolved end time. + Reset(); + UpdateCurrentInterval(); + } + + switch (mSeekState) + { + case SEEK_FORWARD_FROM_ACTIVE: + case SEEK_BACKWARD_FROM_ACTIVE: + if (mElementState != STATE_ACTIVE) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + break; + + case SEEK_FORWARD_FROM_INACTIVE: + case SEEK_BACKWARD_FROM_INACTIVE: + if (mElementState == STATE_ACTIVE) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + break; + + case SEEK_NOT_SEEKING: + /* Do nothing */ + break; + } + + mSeekState = SEEK_NOT_SEEKING; +} + +void +nsSMILTimedElement::UnpreserveInstanceTimes(InstanceTimeList& aList) +{ + const nsSMILInterval* prevInterval = GetPreviousInterval(); + const nsSMILInstanceTime* cutoff = mCurrentInterval ? + mCurrentInterval->Begin() : + prevInterval ? prevInterval->Begin() : nullptr; + uint32_t count = aList.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILInstanceTime* instance = aList[i].get(); + if (!cutoff || cutoff->Time().CompareTo(instance->Time()) < 0) { + instance->UnmarkShouldPreserve(); + } + } +} + +void +nsSMILTimedElement::FilterHistory() +{ + // We should filter the intervals first, since instance times still used in an + // interval won't be filtered. + FilterIntervals(); + FilterInstanceTimes(mBeginInstances); + FilterInstanceTimes(mEndInstances); +} + +void +nsSMILTimedElement::FilterIntervals() +{ + // We can filter old intervals that: + // + // a) are not the previous interval; AND + // b) are not in the middle of a dependency chain; AND + // c) are not the first interval + // + // Condition (a) is necessary since the previous interval is used for applying + // fill effects and updating the current interval. + // + // Condition (b) is necessary since even if this interval itself is not + // active, it may be part of a dependency chain that includes active + // intervals. Such chains are used to establish priorities within the + // animation sandwich. + // + // Condition (c) is necessary to support hyperlinks that target animations + // since in some cases the defined behavior is to seek the document back to + // the first resolved begin time. Presumably the intention here is not + // actually to use the first resolved begin time, the + // _the_first_resolved_begin_time_that_produced_an_interval. That is, + // if we have begin="-5s; -3s; 1s; 3s" with a duration on 1s, we should seek + // to 1s. The spec doesn't say this but I'm pretty sure that is the intention. + // It seems negative times were simply not considered. + // + // Although the above conditions allow us to safely filter intervals for most + // scenarios they do not cover all cases and there will still be scenarios + // that generate intervals indefinitely. In such a case we simply set + // a maximum number of intervals and drop any intervals beyond that threshold. + + uint32_t threshold = mOldIntervals.Length() > sMaxNumIntervals ? + mOldIntervals.Length() - sMaxNumIntervals : + 0; + IntervalList filteredList; + for (uint32_t i = 0; i < mOldIntervals.Length(); ++i) + { + nsSMILInterval* interval = mOldIntervals[i].get(); + if (i != 0 && /*skip first interval*/ + i + 1 < mOldIntervals.Length() && /*skip previous interval*/ + (i < threshold || !interval->IsDependencyChainLink())) { + interval->Unlink(true /*filtered, not deleted*/); + } else { + filteredList.AppendElement(mOldIntervals[i].forget()); + } + } + mOldIntervals.Clear(); + mOldIntervals.SwapElements(filteredList); +} + +namespace +{ + class MOZ_STACK_CLASS RemoveFiltered + { + public: + explicit RemoveFiltered(nsSMILTimeValue aCutoff) : mCutoff(aCutoff) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + // We can filter instance times that: + // a) Precede the end point of the previous interval; AND + // b) Are NOT syncbase times that might be updated to a time after the end + // point of the previous interval; AND + // c) Are NOT fixed end points in any remaining interval. + return aInstanceTime->Time() < mCutoff && + aInstanceTime->IsFixedTime() && + !aInstanceTime->ShouldPreserve(); + } + + private: + nsSMILTimeValue mCutoff; + }; + + class MOZ_STACK_CLASS RemoveBelowThreshold + { + public: + RemoveBelowThreshold(uint32_t aThreshold, + nsTArray<const nsSMILInstanceTime *>& aTimesToKeep) + : mThreshold(aThreshold), + mTimesToKeep(aTimesToKeep) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t aIndex) + { + return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime); + } + + private: + uint32_t mThreshold; + nsTArray<const nsSMILInstanceTime *>& mTimesToKeep; + }; +} // namespace + +void +nsSMILTimedElement::FilterInstanceTimes(InstanceTimeList& aList) +{ + if (GetPreviousInterval()) { + RemoveFiltered removeFiltered(GetPreviousInterval()->End()->Time()); + RemoveInstanceTimes(aList, removeFiltered); + } + + // As with intervals it is possible to create a document that, even despite + // our most aggressive filtering, will generate instance times indefinitely + // (e.g. cyclic dependencies with TimeEvents---we can't filter such times as + // they're unpredictable due to the possibility of seeking the document which + // may prevent some events from being generated). Therefore we introduce + // a hard cutoff at which point we just drop the oldest instance times. + if (aList.Length() > sMaxNumInstanceTimes) { + uint32_t threshold = aList.Length() - sMaxNumInstanceTimes; + // There are a few instance times we should keep though, notably: + // - the current interval begin time, + // - the previous interval end time (see note in RemoveInstanceTimes) + // - the first interval begin time (see note in FilterIntervals) + nsTArray<const nsSMILInstanceTime *> timesToKeep; + if (mCurrentInterval) { + timesToKeep.AppendElement(mCurrentInterval->Begin()); + } + const nsSMILInterval* prevInterval = GetPreviousInterval(); + if (prevInterval) { + timesToKeep.AppendElement(prevInterval->End()); + } + if (!mOldIntervals.IsEmpty()) { + timesToKeep.AppendElement(mOldIntervals[0]->Begin()); + } + RemoveBelowThreshold removeBelowThreshold(threshold, timesToKeep); + RemoveInstanceTimes(aList, removeBelowThreshold); + } +} + +// +// This method is based on the pseudocode given in the SMILANIM spec. +// +// See: +// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#Timing-BeginEnd-LC-Start +// +bool +nsSMILTimedElement::GetNextInterval(const nsSMILInterval* aPrevInterval, + const nsSMILInterval* aReplacedInterval, + const nsSMILInstanceTime* aFixedBeginTime, + nsSMILInterval& aResult) const +{ + MOZ_ASSERT(!aFixedBeginTime || aFixedBeginTime->Time().IsDefinite(), + "Unresolved or indefinite begin time given for interval start"); + static const nsSMILTimeValue zeroTime(0L); + + if (mRestartMode == RESTART_NEVER && aPrevInterval) + return false; + + // Calc starting point + nsSMILTimeValue beginAfter; + bool prevIntervalWasZeroDur = false; + if (aPrevInterval) { + beginAfter = aPrevInterval->End()->Time(); + prevIntervalWasZeroDur + = aPrevInterval->End()->Time() == aPrevInterval->Begin()->Time(); + } else { + beginAfter.SetMillis(INT64_MIN); + } + + RefPtr<nsSMILInstanceTime> tempBegin; + RefPtr<nsSMILInstanceTime> tempEnd; + + while (true) { + // Calculate begin time + if (aFixedBeginTime) { + if (aFixedBeginTime->Time() < beginAfter) { + return false; + } + // our ref-counting is not const-correct + tempBegin = const_cast<nsSMILInstanceTime*>(aFixedBeginTime); + } else if ((!mAnimationElement || + !mAnimationElement->HasAnimAttr(nsGkAtoms::begin)) && + beginAfter <= zeroTime) { + tempBegin = new nsSMILInstanceTime(nsSMILTimeValue(0)); + } else { + int32_t beginPos = 0; + do { + tempBegin = + GetNextGreaterOrEqual(mBeginInstances, beginAfter, beginPos); + if (!tempBegin || !tempBegin->Time().IsDefinite()) { + return false; + } + // If we're updating the current interval then skip any begin time that is + // dependent on the current interval's begin time. e.g. + // <animate id="a" begin="b.begin; a.begin+2s"... + // If b's interval disappears whilst 'a' is in the waiting state the begin + // time at "a.begin+2s" should be skipped since 'a' never begun. + } while (aReplacedInterval && + tempBegin->GetBaseTime() == aReplacedInterval->Begin()); + } + MOZ_ASSERT(tempBegin && tempBegin->Time().IsDefinite() && + tempBegin->Time() >= beginAfter, + "Got a bad begin time while fetching next interval"); + + // Calculate end time + { + int32_t endPos = 0; + do { + tempEnd = + GetNextGreaterOrEqual(mEndInstances, tempBegin->Time(), endPos); + + // SMIL doesn't allow for coincident zero-duration intervals, so if the + // previous interval was zero-duration, and tempEnd is going to give us + // another zero duration interval, then look for another end to use + // instead. + if (tempEnd && prevIntervalWasZeroDur && + tempEnd->Time() == beginAfter) { + tempEnd = GetNextGreater(mEndInstances, tempBegin->Time(), endPos); + } + // As above with begin times, avoid creating self-referential loops + // between instance times by checking that the newly found end instance + // time is not already dependent on the end of the current interval. + } while (tempEnd && aReplacedInterval && + tempEnd->GetBaseTime() == aReplacedInterval->End()); + + if (!tempEnd) { + // If all the ends are before the beginning we have a bad interval + // UNLESS: + // a) We never had any end attribute to begin with (the SMIL pseudocode + // places this condition earlier in the flow but that fails to allow + // for DOM calls when no "indefinite" condition is given), OR + // b) We never had any end instance times to begin with, OR + // c) We have end events which leave the interval open-ended. + bool openEndedIntervalOk = mEndSpecs.IsEmpty() || + mEndInstances.IsEmpty() || + EndHasEventConditions(); + + // The above conditions correspond with the SMIL pseudocode but SMIL + // doesn't address self-dependent instance times which we choose to + // ignore. + // + // Therefore we add a qualification of (b) above that even if + // there are end instance times but they all depend on the end of the + // current interval we should act as if they didn't exist and allow the + // open-ended interval. + // + // In the following condition we don't use |= because it doesn't provide + // short-circuit behavior. + openEndedIntervalOk = openEndedIntervalOk || + (aReplacedInterval && + AreEndTimesDependentOn(aReplacedInterval->End())); + + if (!openEndedIntervalOk) { + return false; // Bad interval + } + } + + nsSMILTimeValue intervalEnd = tempEnd + ? tempEnd->Time() : nsSMILTimeValue(); + nsSMILTimeValue activeEnd = CalcActiveEnd(tempBegin->Time(), intervalEnd); + + if (!tempEnd || intervalEnd != activeEnd) { + tempEnd = new nsSMILInstanceTime(activeEnd); + } + } + MOZ_ASSERT(tempEnd, "Failed to get end point for next interval"); + + // When we choose the interval endpoints, we don't allow coincident + // zero-duration intervals, so if we arrive here and we have a zero-duration + // interval starting at the same point as a previous zero-duration interval, + // then it must be because we've applied constraints to the active duration. + // In that case, we will potentially run into an infinite loop, so we break + // it by searching for the next interval that starts AFTER our current + // zero-duration interval. + if (prevIntervalWasZeroDur && tempEnd->Time() == beginAfter) { + if (prevIntervalWasZeroDur) { + beginAfter.SetMillis(tempBegin->Time().GetMillis() + 1); + prevIntervalWasZeroDur = false; + continue; + } + } + prevIntervalWasZeroDur = tempBegin->Time() == tempEnd->Time(); + + // Check for valid interval + if (tempEnd->Time() > zeroTime || + (tempBegin->Time() == zeroTime && tempEnd->Time() == zeroTime)) { + aResult.Set(*tempBegin, *tempEnd); + return true; + } + + if (mRestartMode == RESTART_NEVER) { + // tempEnd <= 0 so we're going to loop which effectively means restarting + return false; + } + + beginAfter = tempEnd->Time(); + } + NS_NOTREACHED("Hmm... we really shouldn't be here"); + + return false; +} + +nsSMILInstanceTime* +nsSMILTimedElement::GetNextGreater(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const +{ + nsSMILInstanceTime* result = nullptr; + while ((result = GetNextGreaterOrEqual(aList, aBase, aPosition)) && + result->Time() == aBase) { } + return result; +} + +nsSMILInstanceTime* +nsSMILTimedElement::GetNextGreaterOrEqual(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const +{ + nsSMILInstanceTime* result = nullptr; + int32_t count = aList.Length(); + + for (; aPosition < count && !result; ++aPosition) { + nsSMILInstanceTime* val = aList[aPosition].get(); + MOZ_ASSERT(val, "NULL instance time in list"); + if (val->Time() >= aBase) { + result = val; + } + } + + return result; +} + +/** + * @see SMILANIM 3.3.4 + */ +nsSMILTimeValue +nsSMILTimedElement::CalcActiveEnd(const nsSMILTimeValue& aBegin, + const nsSMILTimeValue& aEnd) const +{ + nsSMILTimeValue result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in CalcActiveEnd"); + MOZ_ASSERT(aBegin.IsDefinite(), + "Indefinite or unresolved begin time in CalcActiveEnd"); + + result = GetRepeatDuration(); + + if (aEnd.IsDefinite()) { + nsSMILTime activeDur = aEnd.GetMillis() - aBegin.GetMillis(); + + if (result.IsDefinite()) { + result.SetMillis(std::min(result.GetMillis(), activeDur)); + } else { + result.SetMillis(activeDur); + } + } + + result = ApplyMinAndMax(result); + + if (result.IsDefinite()) { + nsSMILTime activeEnd = result.GetMillis() + aBegin.GetMillis(); + result.SetMillis(activeEnd); + } + + return result; +} + +nsSMILTimeValue +nsSMILTimedElement::GetRepeatDuration() const +{ + nsSMILTimeValue multipliedDuration; + if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) { + multipliedDuration.SetMillis( + nsSMILTime(mRepeatCount * double(mSimpleDur.GetMillis()))); + } else { + multipliedDuration.SetIndefinite(); + } + + nsSMILTimeValue repeatDuration; + + if (mRepeatDur.IsResolved()) { + repeatDuration = std::min(multipliedDuration, mRepeatDur); + } else if (mRepeatCount.IsSet()) { + repeatDuration = multipliedDuration; + } else { + repeatDuration = mSimpleDur; + } + + return repeatDuration; +} + +nsSMILTimeValue +nsSMILTimedElement::ApplyMinAndMax(const nsSMILTimeValue& aDuration) const +{ + if (!aDuration.IsResolved()) { + return aDuration; + } + + if (mMax < mMin) { + return aDuration; + } + + nsSMILTimeValue result; + + if (aDuration > mMax) { + result = mMax; + } else if (aDuration < mMin) { + result = mMin; + } else { + result = aDuration; + } + + return result; +} + +nsSMILTime +nsSMILTimedElement::ActiveTimeToSimpleTime(nsSMILTime aActiveTime, + uint32_t& aRepeatIteration) +{ + nsSMILTime result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in ActiveTimeToSimpleTime"); + MOZ_ASSERT(aActiveTime >= 0, "Expecting non-negative active time"); + // Note that a negative aActiveTime will give us a negative value for + // aRepeatIteration, which is bad because aRepeatIteration is unsigned + + if (mSimpleDur.IsIndefinite() || mSimpleDur.GetMillis() == 0L) { + aRepeatIteration = 0; + result = aActiveTime; + } else { + result = aActiveTime % mSimpleDur.GetMillis(); + aRepeatIteration = (uint32_t)(aActiveTime / mSimpleDur.GetMillis()); + } + + return result; +} + +// +// Although in many cases it would be possible to check for an early end and +// adjust the current interval well in advance the SMIL Animation spec seems to +// indicate that we should only apply an early end at the latest possible +// moment. In particular, this paragraph from section 3.6.8: +// +// 'If restart is set to "always", then the current interval will end early if +// there is an instance time in the begin list that is before (i.e. earlier +// than) the defined end for the current interval. Ending in this manner will +// also send a changed time notice to all time dependents for the current +// interval end.' +// +nsSMILInstanceTime* +nsSMILTimedElement::CheckForEarlyEnd( + const nsSMILTimeValue& aContainerTime) const +{ + MOZ_ASSERT(mCurrentInterval, + "Checking for an early end but the current interval is not set"); + if (mRestartMode != RESTART_ALWAYS) + return nullptr; + + int32_t position = 0; + nsSMILInstanceTime* nextBegin = + GetNextGreater(mBeginInstances, mCurrentInterval->Begin()->Time(), + position); + + if (nextBegin && + nextBegin->Time() > mCurrentInterval->Begin()->Time() && + nextBegin->Time() < mCurrentInterval->End()->Time() && + nextBegin->Time() <= aContainerTime) { + return nextBegin; + } + + return nullptr; +} + +void +nsSMILTimedElement::UpdateCurrentInterval(bool aForceChangeNotice) +{ + // Check if updates are currently blocked (batched) + if (mDeferIntervalUpdates) { + mDoDeferredUpdate = true; + return; + } + + // We adopt the convention of not resolving intervals until the first + // sample. Otherwise, every time each attribute is set we'll re-resolve the + // current interval and notify all our time dependents of the change. + // + // The disadvantage of deferring resolving the interval is that DOM calls to + // to getStartTime will throw an INVALID_STATE_ERR exception until the + // document timeline begins since the start time has not yet been resolved. + if (mElementState == STATE_STARTUP) + return; + + // Although SMIL gives rules for detecting cycles in change notifications, + // some configurations can lead to create-delete-create-delete-etc. cycles + // which SMIL does not consider. + // + // In order to provide consistent behavior in such cases, we detect two + // deletes in a row and then refuse to create any further intervals. That is, + // we say the configuration is invalid. + if (mDeleteCount > 1) { + // When we update the delete count we also set the state to post active, so + // if we're not post active here then something other than + // UpdateCurrentInterval has updated the element state in between and all + // bets are off. + MOZ_ASSERT(mElementState == STATE_POSTACTIVE, + "Expected to be in post-active state after performing double " + "delete"); + return; + } + + // Check that we aren't stuck in infinite recursion updating some syncbase + // dependencies. Generally such situations should be detected in advance and + // the chain broken in a sensible and predictable manner, so if we're hitting + // this assertion we need to work out how to detect the case that's causing + // it. In release builds, just bail out before we overflow the stack. + AutoRestore<uint8_t> depthRestorer(mUpdateIntervalRecursionDepth); + if (++mUpdateIntervalRecursionDepth > sMaxUpdateIntervalRecursionDepth) { + MOZ_ASSERT(false, + "Update current interval recursion depth exceeded threshold"); + return; + } + + // If the interval is active the begin time is fixed. + const nsSMILInstanceTime* beginTime = mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin() + : nullptr; + nsSMILInterval updatedInterval; + if (GetNextInterval(GetPreviousInterval(), mCurrentInterval, + beginTime, updatedInterval)) { + + if (mElementState == STATE_POSTACTIVE) { + + MOZ_ASSERT(!mCurrentInterval, + "In postactive state but the interval has been set"); + mCurrentInterval = new nsSMILInterval(updatedInterval); + mElementState = STATE_WAITING; + NotifyNewInterval(); + + } else { + + bool beginChanged = false; + bool endChanged = false; + + if (mElementState != STATE_ACTIVE && + !updatedInterval.Begin()->SameTimeAndBase( + *mCurrentInterval->Begin())) { + mCurrentInterval->SetBegin(*updatedInterval.Begin()); + beginChanged = true; + } + + if (!updatedInterval.End()->SameTimeAndBase(*mCurrentInterval->End())) { + mCurrentInterval->SetEnd(*updatedInterval.End()); + endChanged = true; + } + + if (beginChanged || endChanged || aForceChangeNotice) { + NotifyChangedInterval(mCurrentInterval, beginChanged, endChanged); + } + } + + // There's a chance our next milestone has now changed, so update the time + // container + RegisterMilestone(); + } else { // GetNextInterval failed: Current interval is no longer valid + if (mElementState == STATE_ACTIVE) { + // The interval is active so we can't just delete it, instead trim it so + // that begin==end. + if (!mCurrentInterval->End()->SameTimeAndBase(*mCurrentInterval->Begin())) + { + mCurrentInterval->SetEnd(*mCurrentInterval->Begin()); + NotifyChangedInterval(mCurrentInterval, false, true); + } + // The transition to the postactive state will take place on the next + // sample (along with firing end events, clearing intervals etc.) + RegisterMilestone(); + } else if (mElementState == STATE_WAITING) { + AutoRestore<uint8_t> deleteCountRestorer(mDeleteCount); + ++mDeleteCount; + mElementState = STATE_POSTACTIVE; + ResetCurrentInterval(); + } + } +} + +void +nsSMILTimedElement::SampleSimpleTime(nsSMILTime aActiveTime) +{ + if (mClient) { + uint32_t repeatIteration; + nsSMILTime simpleTime = + ActiveTimeToSimpleTime(aActiveTime, repeatIteration); + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +void +nsSMILTimedElement::SampleFillValue() +{ + if (mFillMode != FILL_FREEZE || !mClient) + return; + + nsSMILTime activeTime; + + if (mElementState == STATE_WAITING || mElementState == STATE_POSTACTIVE) { + const nsSMILInterval* prevInterval = GetPreviousInterval(); + MOZ_ASSERT(prevInterval, + "Attempting to sample fill value but there is no previous " + "interval"); + MOZ_ASSERT(prevInterval->End()->Time().IsDefinite() && + prevInterval->End()->IsFixedTime(), + "Attempting to sample fill value but the endpoint of the " + "previous interval is not resolved and fixed"); + + activeTime = prevInterval->End()->Time().GetMillis() - + prevInterval->Begin()->Time().GetMillis(); + + // If the interval's repeat duration was shorter than its active duration, + // use the end of the repeat duration to determine the frozen animation's + // state. + nsSMILTimeValue repeatDuration = GetRepeatDuration(); + if (repeatDuration.IsDefinite()) { + activeTime = std::min(repeatDuration.GetMillis(), activeTime); + } + } else { + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Attempting to sample fill value when we're in an unexpected state " + "(probably STATE_STARTUP)"); + + // If we are being asked to sample the fill value while active we *must* + // have a repeat duration shorter than the active duration so use that. + MOZ_ASSERT(GetRepeatDuration().IsDefinite(), + "Attempting to sample fill value of an active animation with " + "an indefinite repeat duration"); + activeTime = GetRepeatDuration().GetMillis(); + } + + uint32_t repeatIteration; + nsSMILTime simpleTime = + ActiveTimeToSimpleTime(activeTime, repeatIteration); + + if (simpleTime == 0L && repeatIteration) { + mClient->SampleLastValue(--repeatIteration); + } else { + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +nsresult +nsSMILTimedElement::AddInstanceTimeFromCurrentTime(nsSMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin) +{ + double offset = aOffsetSeconds * PR_MSEC_PER_SEC; + + // Check we won't overflow the range of nsSMILTime + if (aCurrentTime + NS_round(offset) > INT64_MAX) + return NS_ERROR_ILLEGAL_VALUE; + + nsSMILTimeValue timeVal(aCurrentTime + int64_t(NS_round(offset))); + + RefPtr<nsSMILInstanceTime> instanceTime = + new nsSMILInstanceTime(timeVal, nsSMILInstanceTime::SOURCE_DOM); + + AddInstanceTime(instanceTime, aIsBegin); + + return NS_OK; +} + +void +nsSMILTimedElement::RegisterMilestone() +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return; + MOZ_ASSERT(mAnimationElement, + "Got a time container without an owning animation element"); + + nsSMILMilestone nextMilestone; + if (!GetNextMilestone(nextMilestone)) + return; + + // This method is called every time we might possibly have updated our + // current interval, but since nsSMILTimeContainer makes no attempt to filter + // out redundant milestones we do some rudimentary filtering here. It's not + // perfect, but unnecessary samples are fairly cheap. + if (nextMilestone >= mPrevRegisteredMilestone) + return; + + container->AddMilestone(nextMilestone, *mAnimationElement); + mPrevRegisteredMilestone = nextMilestone; +} + +bool +nsSMILTimedElement::GetNextMilestone(nsSMILMilestone& aNextMilestone) const +{ + // Return the next key moment in our lifetime. + // + // XXX It may be possible in future to optimise this so that we only register + // for milestones if: + // a) We have time dependents, or + // b) We are dependent on events or syncbase relationships, or + // c) There are registered listeners for our events + // + // Then for the simple case where everything uses offset values we could + // ignore milestones altogether. + // + // We'd need to be careful, however, that if one of those conditions became + // true in between samples that we registered our next milestone at that + // point. + + switch (mElementState) + { + case STATE_STARTUP: + // All elements register for an initial end sample at t=0 where we resolve + // our initial interval. + aNextMilestone.mIsEnd = true; // Initial sample should be an end sample + aNextMilestone.mTime = 0; + return true; + + case STATE_WAITING: + MOZ_ASSERT(mCurrentInterval, + "In waiting state but the current interval has not been set"); + aNextMilestone.mIsEnd = false; + aNextMilestone.mTime = mCurrentInterval->Begin()->Time().GetMillis(); + return true; + + case STATE_ACTIVE: + { + // Work out what comes next: the interval end or the next repeat iteration + nsSMILTimeValue nextRepeat; + if (mSeekState == SEEK_NOT_SEEKING && mSimpleDur.IsDefinite()) { + nsSMILTime nextRepeatActiveTime = + (mCurrentRepeatIteration + 1) * mSimpleDur.GetMillis(); + // Check that the repeat fits within the repeat duration + if (nsSMILTimeValue(nextRepeatActiveTime) < GetRepeatDuration()) { + nextRepeat.SetMillis(mCurrentInterval->Begin()->Time().GetMillis() + + nextRepeatActiveTime); + } + } + nsSMILTimeValue nextMilestone = + std::min(mCurrentInterval->End()->Time(), nextRepeat); + + // Check for an early end before that time + nsSMILInstanceTime* earlyEnd = CheckForEarlyEnd(nextMilestone); + if (earlyEnd) { + aNextMilestone.mIsEnd = true; + aNextMilestone.mTime = earlyEnd->Time().GetMillis(); + return true; + } + + // Apply the previously calculated milestone + if (nextMilestone.IsDefinite()) { + aNextMilestone.mIsEnd = nextMilestone != nextRepeat; + aNextMilestone.mTime = nextMilestone.GetMillis(); + return true; + } + + return false; + } + + case STATE_POSTACTIVE: + return false; + } + MOZ_CRASH("Invalid element state"); +} + +void +nsSMILTimedElement::NotifyNewInterval() +{ + MOZ_ASSERT(mCurrentInterval, + "Attempting to notify dependents of a new interval but the " + "interval is not set"); + + nsSMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + for (auto iter = mTimeDependents.Iter(); !iter.Done(); iter.Next()) { + nsSMILInterval* interval = mCurrentInterval; + // It's possible that in notifying one new time dependent of a new interval + // that a chain reaction is triggered which results in the original + // interval disappearing. If that's the case we can skip sending further + // notifications. + if (!interval) { + break; + } + nsSMILTimeValueSpec* spec = iter.Get()->GetKey(); + spec->HandleNewInterval(*interval, container); + } +} + +void +nsSMILTimedElement::NotifyChangedInterval(nsSMILInterval* aInterval, + bool aBeginObjectChanged, + bool aEndObjectChanged) +{ + MOZ_ASSERT(aInterval, "Null interval for change notification"); + + nsSMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + // Copy the instance times list since notifying the instance times can result + // in a chain reaction whereby our own interval gets deleted along with its + // instance times. + InstanceTimeList times; + aInterval->GetDependentTimes(times); + + for (uint32_t i = 0; i < times.Length(); ++i) { + times[i]->HandleChangedInterval(container, aBeginObjectChanged, + aEndObjectChanged); + } +} + +void +nsSMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) +{ + if (!mAnimationElement) + return; + + nsCOMPtr<nsIRunnable> event = + new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail); + NS_DispatchToMainThread(event); +} + +const nsSMILInstanceTime* +nsSMILTimedElement::GetEffectiveBeginInstance() const +{ + switch (mElementState) + { + case STATE_STARTUP: + return nullptr; + + case STATE_ACTIVE: + return mCurrentInterval->Begin(); + + case STATE_WAITING: + case STATE_POSTACTIVE: + { + const nsSMILInterval* prevInterval = GetPreviousInterval(); + return prevInterval ? prevInterval->Begin() : nullptr; + } + } + MOZ_CRASH("Invalid element state"); +} + +const nsSMILInterval* +nsSMILTimedElement::GetPreviousInterval() const +{ + return mOldIntervals.IsEmpty() + ? nullptr + : mOldIntervals[mOldIntervals.Length()-1].get(); +} + +bool +nsSMILTimedElement::HasClientInFillRange() const +{ + // Returns true if we have a client that is in the range where it will fill + return mClient && + ((mElementState != STATE_ACTIVE && HasPlayed()) || + (mElementState == STATE_ACTIVE && !mClient->IsActive())); +} + +bool +nsSMILTimedElement::EndHasEventConditions() const +{ + for (uint32_t i = 0; i < mEndSpecs.Length(); ++i) { + if (mEndSpecs[i]->IsEventBased()) + return true; + } + return false; +} + +bool +nsSMILTimedElement::AreEndTimesDependentOn( + const nsSMILInstanceTime* aBase) const +{ + if (mEndInstances.IsEmpty()) + return false; + + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + if (mEndInstances[i]->GetBaseTime() != aBase) { + return false; + } + } + return true; +} + |