Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant
#include "private/qsequentialanimationgroupjob_p.h"
#include "private/qpauseanimationjob_p.h"
#include "private/qanimationjobutil_p.h"
#include <QtCore/qnumeric.h>
#include <limits>
QT_BEGIN_NAMESPACE
namespace {
/*!
\internal
Returns INT_MAX in case of overflow
*/
int addSaturating(int a, int b)
{
int r;
if (qAddOverflow(a, b, &r))
return std::numeric_limits<int>::max();
return r;
}
} // namespace
QSequentialAnimationGroupJob::QSequentialAnimationGroupJob()
: QAnimationGroupJob()
, m_currentAnimation(nullptr)
, m_previousLoop(0)
{
}
QSequentialAnimationGroupJob::~QSequentialAnimationGroupJob()
{
}
bool QSequentialAnimationGroupJob::atEnd() const
{
// we try to detect if we're at the end of the group
//this is true if the following conditions are true:
// 1. we're in the last loop
// 2. the direction is forward
// 3. the current animation is the last one
// 4. the current animation has reached its end
const int animTotalCurrentTime = m_currentAnimation->currentTime();
return (m_currentLoop == m_loopCount - 1
&& m_direction == Forward
&& !m_children.next(m_currentAnimation)
&& animTotalCurrentTime == animationActualTotalDuration(m_currentAnimation));
}
int QSequentialAnimationGroupJob::animationActualTotalDuration(
const QAbstractAnimationJob *anim) const
{
int ret = anim->totalDuration();
if (ret == -1) {
int done = uncontrolledAnimationFinishTime(anim);
// If the animation has reached the end, use the uncontrolledFinished value.
if (done >= 0 && (anim->loopCount() - 1 == anim->currentLoop() || anim->state() == Stopped))
return done;
}
return ret;
}
QSequentialAnimationGroupJob::AnimationIndex QSequentialAnimationGroupJob::indexForCurrentTime() const
{
Q_ASSERT(!m_children.isEmpty());
AnimationIndex ret;
int duration = 0;
for (const QAbstractAnimationJob *anim : m_children) {
duration = animationActualTotalDuration(anim);
// 'animation' is the current animation if one of these reasons is true:
// 1. it's duration is undefined
// 2. it ends after msecs
// 3. it is the last animation (this can happen in case there is at least 1 uncontrolled animation)
// 4. it ends exactly in msecs and the direction is backwards
const int offsetPlusDuration = addSaturating(ret.timeOffset, duration);
if (duration == -1 || m_currentTime < offsetPlusDuration
|| (m_currentTime == offsetPlusDuration && m_direction == QAbstractAnimationJob::Backward)) {
ret.animation = anim;
return ret;
}
if (anim == m_currentAnimation) {
ret.afterCurrent = true;
}
// 'animation' has a non-null defined duration and is not the one at time 'msecs'.
ret.timeOffset = offsetPlusDuration;
}
// this can only happen when one of those conditions is true:
// 1. the duration of the group is undefined and we passed its actual duration
// 2. there are only 0-duration animations in the group
ret.timeOffset -= duration;
ret.animation = m_children.last();
return ret;
}
void QSequentialAnimationGroupJob::restart()
{
// restarting the group by making the first/last animation the current one
if (m_direction == Forward) {
m_previousLoop = 0;
if (m_currentAnimation == m_children.first())
activateCurrentAnimation();
else
setCurrentAnimation(m_children.first());
}
else { // direction == Backward
m_previousLoop = m_loopCount - 1;
if (m_currentAnimation == m_children.last())
activateCurrentAnimation();
else
setCurrentAnimation(m_children.last());
}
}
void QSequentialAnimationGroupJob::advanceForwards(const AnimationIndex &newAnimationIndex)
{
if (m_previousLoop < m_currentLoop) {
// we need to fast forward to the end
for (QAbstractAnimationJob *anim = m_currentAnimation; anim; anim = m_children.next(anim)) {
RETURN_IF_DELETED(setCurrentAnimation(anim, true));
RETURN_IF_DELETED(anim->setCurrentTime(animationActualTotalDuration(anim)));
}
// this will make sure the current animation is reset to the beginning
if (m_children.count() == 1) {
// we need to force activation because setCurrentAnimation will have no effect
RETURN_IF_DELETED(activateCurrentAnimation());
} else {
RETURN_IF_DELETED(setCurrentAnimation(m_children.first(), true));
}
}
// and now we need to fast forward from the current position to
for (QAbstractAnimationJob *anim = m_currentAnimation;
anim && anim != newAnimationIndex.animation; anim = m_children.next(anim)) { //### WRONG,
RETURN_IF_DELETED(setCurrentAnimation(anim, true));
RETURN_IF_DELETED(anim->setCurrentTime(animationActualTotalDuration(anim)));
}
// setting the new current animation will happen later
}
void QSequentialAnimationGroupJob::rewindForwards(const AnimationIndex &newAnimationIndex)
{
if (m_previousLoop > m_currentLoop) {
// we need to fast rewind to the beginning
for (QAbstractAnimationJob *anim = m_currentAnimation; anim;
anim = m_children.prev(anim)) {
RETURN_IF_DELETED(setCurrentAnimation(anim, true));
RETURN_IF_DELETED(anim->setCurrentTime(0));
}
// this will make sure the current animation is reset to the end
if (m_children.count() == 1) { //count == 1
// we need to force activation because setCurrentAnimation will have no effect
RETURN_IF_DELETED(activateCurrentAnimation());
} else {
RETURN_IF_DELETED(setCurrentAnimation(m_children.last(), true));
}
}
// and now we need to fast rewind from the current position to
for (QAbstractAnimationJob *anim = m_currentAnimation;
anim && anim != newAnimationIndex.animation; anim = m_children.prev(anim)) {
RETURN_IF_DELETED(setCurrentAnimation(anim, true));
RETURN_IF_DELETED(anim->setCurrentTime(0));
}
// setting the new current animation will happen later
}
int QSequentialAnimationGroupJob::duration() const
{
int ret = 0;
for (const QAbstractAnimationJob *anim : m_children) {
const int currentDuration = anim->totalDuration();
if (currentDuration == -1)
return -1; // Undetermined length
if (qAddOverflow(ret, currentDuration, &ret))
return std::numeric_limits<int>::max();
}
return ret;
}
void QSequentialAnimationGroupJob::clear()
{
m_previousLoop = 0;
QAnimationGroupJob::clear();
// clear() should call removeAnimation(), which will clear m_currentAnimation, eventually.
Q_ASSERT(m_currentAnimation == nullptr);
}
void QSequentialAnimationGroupJob::updateCurrentTime(int currentTime)
{
if (!m_currentAnimation)
return;
const QSequentialAnimationGroupJob::AnimationIndex newAnimationIndex = indexForCurrentTime();
// newAnimationIndex.index is the new current animation
if (m_previousLoop < m_currentLoop
|| (m_previousLoop == m_currentLoop && m_currentAnimation != newAnimationIndex.animation && newAnimationIndex.afterCurrent)) {
// advancing with forward direction is the same as rewinding with backwards direction
RETURN_IF_DELETED(advanceForwards(newAnimationIndex));
} else if (m_previousLoop > m_currentLoop
|| (m_previousLoop == m_currentLoop && m_currentAnimation != newAnimationIndex.animation && !newAnimationIndex.afterCurrent)) {
// rewinding with forward direction is the same as advancing with backwards direction
RETURN_IF_DELETED(rewindForwards(newAnimationIndex));
}
RETURN_IF_DELETED(setCurrentAnimation(newAnimationIndex.animation));
const int newCurrentTime = currentTime - newAnimationIndex.timeOffset;
if (m_currentAnimation) {
RETURN_IF_DELETED(m_currentAnimation->setCurrentTime(newCurrentTime));
if (atEnd()) {
//we make sure that we don't exceed the duration here
const int diff = m_currentAnimation->currentTime() - newCurrentTime;
m_currentTime = addSaturating(m_currentTime, diff);
RETURN_IF_DELETED(stop());
}
} else {
//the only case where currentAnimation could be null
//is when all animations have been removed
Q_ASSERT(m_children.isEmpty());
m_currentTime = 0;
RETURN_IF_DELETED(stop());
}
m_previousLoop = m_currentLoop;
}
void QSequentialAnimationGroupJob::updateState(QAbstractAnimationJob::State newState,
QAbstractAnimationJob::State oldState)
{
QAnimationGroupJob::updateState(newState, oldState);
if (!m_currentAnimation)
return;
switch (newState) {
case Stopped:
m_currentAnimation->stop();
break;
case Paused:
if (oldState == m_currentAnimation->state() && oldState == Running)
m_currentAnimation->pause();
else
restart();
break;
case Running:
if (oldState == m_currentAnimation->state() && oldState == Paused)
m_currentAnimation->start();
else
restart();
break;
}
}
void QSequentialAnimationGroupJob::updateDirection(QAbstractAnimationJob::Direction direction)
{
// we need to update the direction of the current animation
if (!isStopped() && m_currentAnimation)
m_currentAnimation->setDirection(direction);
}
void QSequentialAnimationGroupJob::setCurrentAnimation(
const QAbstractAnimationJob *anim, bool intermediate)
{
if (!anim) {
Q_ASSERT(m_children.isEmpty());
m_currentAnimation = nullptr;
return;
}
if (anim == m_currentAnimation)
return;
// stop the old current animation
if (m_currentAnimation)
m_currentAnimation->stop();
// Assert that the animation passed as argument is actually part of this group ...
Q_ASSERT(m_children.contains(anim));
// ... as then this const_cast is just a shortcut for looking up the non-const
// pointer in the linked list of jobs.
m_currentAnimation = const_cast<QAbstractAnimationJob *>(anim);
activateCurrentAnimation(intermediate);
}
void QSequentialAnimationGroupJob::activateCurrentAnimation(bool intermediate)
{
if (!m_currentAnimation || isStopped())
return;
m_currentAnimation->stop();
// we ensure the direction is consistent with the group's direction
m_currentAnimation->setDirection(m_direction);
// reset the finish time of the animation if it is uncontrolled
if (m_currentAnimation->totalDuration() == -1)
resetUncontrolledAnimationFinishTime(m_currentAnimation);
RETURN_IF_DELETED(m_currentAnimation->start());
if (!intermediate && isPaused())
m_currentAnimation->pause();
}
void QSequentialAnimationGroupJob::uncontrolledAnimationFinished(QAbstractAnimationJob *animation)
{
Q_UNUSED(animation);
Q_ASSERT(animation == m_currentAnimation);
setUncontrolledAnimationFinishTime(m_currentAnimation, m_currentAnimation->currentTime());
int totalTime = currentTime();
if (m_direction == Forward) {
// set the current animation to be the next one
if (auto *anim = m_children.next(m_currentAnimation))
RETURN_IF_DELETED(setCurrentAnimation(anim));
for (QAbstractAnimationJob *a = m_children.next(animation); a; a = m_children.next(a)) {
int dur = a->duration();
if (dur == -1) {
totalTime = -1;
break;
} else {
totalTime += dur;
}
}
} else {
// set the current animation to be the previous one
if (auto *anim = m_children.prev(m_currentAnimation))
RETURN_IF_DELETED(setCurrentAnimation(anim));
for (QAbstractAnimationJob *a = m_children.prev(animation); a; a = m_children.prev(a)) {
int dur = a->duration();
if (dur == -1) {
totalTime = -1;
break;
} else {
totalTime += dur;
}
}
}
if (totalTime >= 0)
setUncontrolledAnimationFinishTime(this, totalTime);
if (atEnd())
stop();
}
void QSequentialAnimationGroupJob::animationInserted(QAbstractAnimationJob *anim)
{
if (m_currentAnimation == nullptr)
RETURN_IF_DELETED(setCurrentAnimation(m_children.first())); // initialize the current animation
if (m_currentAnimation == m_children.next(anim)
&& m_currentAnimation->currentTime() == 0 && m_currentAnimation->currentLoop() == 0) {
//in this case we simply insert the animation before the current one has actually started
RETURN_IF_DELETED(setCurrentAnimation(anim));
}
// TODO
// if (index < m_currentAnimationIndex || m_currentLoop != 0) {
// qWarning("QSequentialGroup::insertAnimation only supports to add animations after the current one.");
// return; //we're not affected because it is added after the current one
// }
}
void QSequentialAnimationGroupJob::animationRemoved(QAbstractAnimationJob *anim, QAbstractAnimationJob *prev, QAbstractAnimationJob *next)
{
QAnimationGroupJob::animationRemoved(anim, prev, next);
Q_ASSERT(m_currentAnimation); // currentAnimation should always be set
bool removingCurrent = anim == m_currentAnimation;
if (removingCurrent) {
if (next)
RETURN_IF_DELETED(setCurrentAnimation(next)); //let's try to take the next one
else if (prev)
RETURN_IF_DELETED(setCurrentAnimation(prev));
else// case all animations were removed
RETURN_IF_DELETED(setCurrentAnimation(nullptr));
}
// duration of the previous animations up to the current animation
m_currentTime = 0;
for (QAbstractAnimationJob *job : m_children) {
if (job == m_currentAnimation)
break;
m_currentTime = addSaturating(m_currentTime, animationActualTotalDuration(job));
}
if (!removingCurrent) {
//the current animation is not the one being removed
//so we add its current time to the current time of this group
m_currentTime = addSaturating(m_currentTime, m_currentAnimation->currentTime());
}
//let's also update the total current time
m_totalCurrentTime = addSaturating(m_currentTime, totalDuration());
}
void QSequentialAnimationGroupJob::debugAnimation(QDebug d) const
{
d << "SequentialAnimationGroupJob(" << Qt::hex << (const void *) this << Qt::dec << ")" << "currentAnimation:" << (void *)m_currentAnimation;
debugChildren(d);
}
QT_END_NAMESPACE