1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "private/qsequentialanimationgroupjob_p.h"
5#include "private/qpauseanimationjob_p.h"
6#include "private/qanimationjobutil_p.h"
7
8QT_BEGIN_NAMESPACE
9
10QSequentialAnimationGroupJob::QSequentialAnimationGroupJob()
11 : QAnimationGroupJob()
12 , m_currentAnimation(nullptr)
13 , m_previousLoop(0)
14{
15}
16
17QSequentialAnimationGroupJob::~QSequentialAnimationGroupJob()
18{
19}
20
21bool QSequentialAnimationGroupJob::atEnd() const
22{
23 // we try to detect if we're at the end of the group
24 //this is true if the following conditions are true:
25 // 1. we're in the last loop
26 // 2. the direction is forward
27 // 3. the current animation is the last one
28 // 4. the current animation has reached its end
29
30 const int animTotalCurrentTime = m_currentAnimation->currentTime();
31 return (m_currentLoop == m_loopCount - 1
32 && m_direction == Forward
33 && !m_children.next(current: m_currentAnimation)
34 && animTotalCurrentTime == animationActualTotalDuration(anim: m_currentAnimation));
35}
36
37int QSequentialAnimationGroupJob::animationActualTotalDuration(
38 const QAbstractAnimationJob *anim) const
39{
40 int ret = anim->totalDuration();
41 if (ret == -1) {
42 int done = uncontrolledAnimationFinishTime(anim);
43 // If the animation has reached the end, use the uncontrolledFinished value.
44 if (done >= 0 && (anim->loopCount() - 1 == anim->currentLoop() || anim->state() == Stopped))
45 return done;
46 }
47 return ret;
48}
49
50QSequentialAnimationGroupJob::AnimationIndex QSequentialAnimationGroupJob::indexForCurrentTime() const
51{
52 Q_ASSERT(!m_children.isEmpty());
53
54 AnimationIndex ret;
55 int duration = 0;
56
57 for (const QAbstractAnimationJob *anim : m_children) {
58 duration = animationActualTotalDuration(anim);
59
60 // 'animation' is the current animation if one of these reasons is true:
61 // 1. it's duration is undefined
62 // 2. it ends after msecs
63 // 3. it is the last animation (this can happen in case there is at least 1 uncontrolled animation)
64 // 4. it ends exactly in msecs and the direction is backwards
65 if (duration == -1 || m_currentTime < (ret.timeOffset + duration)
66 || (m_currentTime == (ret.timeOffset + duration) && m_direction == QAbstractAnimationJob::Backward)) {
67 ret.animation = anim;
68 return ret;
69 }
70
71 if (anim == m_currentAnimation) {
72 ret.afterCurrent = true;
73 }
74
75 // 'animation' has a non-null defined duration and is not the one at time 'msecs'.
76 ret.timeOffset += duration;
77 }
78
79 // this can only happen when one of those conditions is true:
80 // 1. the duration of the group is undefined and we passed its actual duration
81 // 2. there are only 0-duration animations in the group
82 ret.timeOffset -= duration;
83 ret.animation = m_children.last();
84 return ret;
85}
86
87void QSequentialAnimationGroupJob::restart()
88{
89 // restarting the group by making the first/last animation the current one
90 if (m_direction == Forward) {
91 m_previousLoop = 0;
92 if (m_currentAnimation == m_children.first())
93 activateCurrentAnimation();
94 else
95 setCurrentAnimation(anim: m_children.first());
96 }
97 else { // direction == Backward
98 m_previousLoop = m_loopCount - 1;
99 if (m_currentAnimation == m_children.last())
100 activateCurrentAnimation();
101 else
102 setCurrentAnimation(anim: m_children.last());
103 }
104}
105
106void QSequentialAnimationGroupJob::advanceForwards(const AnimationIndex &newAnimationIndex)
107{
108 if (m_previousLoop < m_currentLoop) {
109 // we need to fast forward to the end
110 for (QAbstractAnimationJob *anim = m_currentAnimation; anim; anim = m_children.next(current: anim)) {
111 RETURN_IF_DELETED(setCurrentAnimation(anim, true));
112 RETURN_IF_DELETED(anim->setCurrentTime(animationActualTotalDuration(anim)));
113 }
114 // this will make sure the current animation is reset to the beginning
115 if (m_children.count() == 1) {
116 // we need to force activation because setCurrentAnimation will have no effect
117 RETURN_IF_DELETED(activateCurrentAnimation());
118 } else {
119 RETURN_IF_DELETED(setCurrentAnimation(m_children.first(), true));
120 }
121 }
122
123 // and now we need to fast forward from the current position to
124 for (QAbstractAnimationJob *anim = m_currentAnimation;
125 anim && anim != newAnimationIndex.animation; anim = m_children.next(current: anim)) { //### WRONG,
126 RETURN_IF_DELETED(setCurrentAnimation(anim, true));
127 RETURN_IF_DELETED(anim->setCurrentTime(animationActualTotalDuration(anim)));
128 }
129 // setting the new current animation will happen later
130}
131
132void QSequentialAnimationGroupJob::rewindForwards(const AnimationIndex &newAnimationIndex)
133{
134 if (m_previousLoop > m_currentLoop) {
135 // we need to fast rewind to the beginning
136 for (QAbstractAnimationJob *anim = m_currentAnimation; anim;
137 anim = m_children.prev(current: anim)) {
138 RETURN_IF_DELETED(setCurrentAnimation(anim, true));
139 RETURN_IF_DELETED(anim->setCurrentTime(0));
140 }
141 // this will make sure the current animation is reset to the end
142 if (m_children.count() == 1) { //count == 1
143 // we need to force activation because setCurrentAnimation will have no effect
144 RETURN_IF_DELETED(activateCurrentAnimation());
145 } else {
146 RETURN_IF_DELETED(setCurrentAnimation(m_children.last(), true));
147 }
148 }
149
150 // and now we need to fast rewind from the current position to
151 for (QAbstractAnimationJob *anim = m_currentAnimation;
152 anim && anim != newAnimationIndex.animation; anim = m_children.prev(current: anim)) {
153 RETURN_IF_DELETED(setCurrentAnimation(anim, true));
154 RETURN_IF_DELETED(anim->setCurrentTime(0));
155 }
156 // setting the new current animation will happen later
157}
158
159int QSequentialAnimationGroupJob::duration() const
160{
161 int ret = 0;
162
163 for (const QAbstractAnimationJob *anim : m_children) {
164 const int currentDuration = anim->totalDuration();
165 if (currentDuration == -1)
166 return -1; // Undetermined length
167
168 ret += currentDuration;
169 }
170
171 return ret;
172}
173
174void QSequentialAnimationGroupJob::clear()
175{
176 m_previousLoop = 0;
177 QAnimationGroupJob::clear();
178
179 // clear() should call removeAnimation(), which will clear m_currentAnimation, eventually.
180 Q_ASSERT(m_currentAnimation == nullptr);
181}
182
183void QSequentialAnimationGroupJob::updateCurrentTime(int currentTime)
184{
185 if (!m_currentAnimation)
186 return;
187
188 const QSequentialAnimationGroupJob::AnimationIndex newAnimationIndex = indexForCurrentTime();
189
190 // newAnimationIndex.index is the new current animation
191 if (m_previousLoop < m_currentLoop
192 || (m_previousLoop == m_currentLoop && m_currentAnimation != newAnimationIndex.animation && newAnimationIndex.afterCurrent)) {
193 // advancing with forward direction is the same as rewinding with backwards direction
194 RETURN_IF_DELETED(advanceForwards(newAnimationIndex));
195
196 } else if (m_previousLoop > m_currentLoop
197 || (m_previousLoop == m_currentLoop && m_currentAnimation != newAnimationIndex.animation && !newAnimationIndex.afterCurrent)) {
198 // rewinding with forward direction is the same as advancing with backwards direction
199 RETURN_IF_DELETED(rewindForwards(newAnimationIndex));
200 }
201
202 RETURN_IF_DELETED(setCurrentAnimation(newAnimationIndex.animation));
203
204 const int newCurrentTime = currentTime - newAnimationIndex.timeOffset;
205
206 if (m_currentAnimation) {
207 RETURN_IF_DELETED(m_currentAnimation->setCurrentTime(newCurrentTime));
208 if (atEnd()) {
209 //we make sure that we don't exceed the duration here
210 m_currentTime += m_currentAnimation->currentTime() - newCurrentTime;
211 RETURN_IF_DELETED(stop());
212 }
213 } else {
214 //the only case where currentAnimation could be null
215 //is when all animations have been removed
216 Q_ASSERT(m_children.isEmpty());
217 m_currentTime = 0;
218 RETURN_IF_DELETED(stop());
219 }
220
221 m_previousLoop = m_currentLoop;
222}
223
224void QSequentialAnimationGroupJob::updateState(QAbstractAnimationJob::State newState,
225 QAbstractAnimationJob::State oldState)
226{
227 QAnimationGroupJob::updateState(newState, oldState);
228
229 if (!m_currentAnimation)
230 return;
231
232 switch (newState) {
233 case Stopped:
234 m_currentAnimation->stop();
235 break;
236 case Paused:
237 if (oldState == m_currentAnimation->state() && oldState == Running)
238 m_currentAnimation->pause();
239 else
240 restart();
241 break;
242 case Running:
243 if (oldState == m_currentAnimation->state() && oldState == Paused)
244 m_currentAnimation->start();
245 else
246 restart();
247 break;
248 }
249}
250
251void QSequentialAnimationGroupJob::updateDirection(QAbstractAnimationJob::Direction direction)
252{
253 // we need to update the direction of the current animation
254 if (!isStopped() && m_currentAnimation)
255 m_currentAnimation->setDirection(direction);
256}
257
258void QSequentialAnimationGroupJob::setCurrentAnimation(
259 const QAbstractAnimationJob *anim, bool intermediate)
260{
261 if (!anim) {
262 Q_ASSERT(m_children.isEmpty());
263 m_currentAnimation = nullptr;
264 return;
265 }
266
267 if (anim == m_currentAnimation)
268 return;
269
270 // stop the old current animation
271 if (m_currentAnimation)
272 m_currentAnimation->stop();
273
274 // Assert that the animation passed as argument is actually part of this group ...
275 Q_ASSERT(m_children.contains(anim));
276
277 // ... as then this const_cast is just a shortcut for looking up the non-const
278 // pointer in the linked list of jobs.
279 m_currentAnimation = const_cast<QAbstractAnimationJob *>(anim);
280
281 activateCurrentAnimation(intermediate);
282}
283
284void QSequentialAnimationGroupJob::activateCurrentAnimation(bool intermediate)
285{
286 if (!m_currentAnimation || isStopped())
287 return;
288
289 m_currentAnimation->stop();
290
291 // we ensure the direction is consistent with the group's direction
292 m_currentAnimation->setDirection(m_direction);
293
294 // reset the finish time of the animation if it is uncontrolled
295 if (m_currentAnimation->totalDuration() == -1)
296 resetUncontrolledAnimationFinishTime(anim: m_currentAnimation);
297
298 RETURN_IF_DELETED(m_currentAnimation->start());
299 if (!intermediate && isPaused())
300 m_currentAnimation->pause();
301}
302
303void QSequentialAnimationGroupJob::uncontrolledAnimationFinished(QAbstractAnimationJob *animation)
304{
305 Q_UNUSED(animation);
306 Q_ASSERT(animation == m_currentAnimation);
307
308 setUncontrolledAnimationFinishTime(anim: m_currentAnimation, time: m_currentAnimation->currentTime());
309
310 int totalTime = currentTime();
311 if (m_direction == Forward) {
312 // set the current animation to be the next one
313 if (auto *anim = m_children.next(current: m_currentAnimation))
314 RETURN_IF_DELETED(setCurrentAnimation(anim));
315
316 for (QAbstractAnimationJob *a = m_children.next(current: animation); a; a = m_children.next(current: a)) {
317 int dur = a->duration();
318 if (dur == -1) {
319 totalTime = -1;
320 break;
321 } else {
322 totalTime += dur;
323 }
324 }
325
326 } else {
327 // set the current animation to be the previous one
328 if (auto *anim = m_children.prev(current: m_currentAnimation))
329 RETURN_IF_DELETED(setCurrentAnimation(anim));
330
331 for (QAbstractAnimationJob *a = m_children.prev(current: animation); a; a = m_children.prev(current: a)) {
332 int dur = a->duration();
333 if (dur == -1) {
334 totalTime = -1;
335 break;
336 } else {
337 totalTime += dur;
338 }
339 }
340 }
341 if (totalTime >= 0)
342 setUncontrolledAnimationFinishTime(anim: this, time: totalTime);
343 if (atEnd())
344 stop();
345}
346
347void QSequentialAnimationGroupJob::animationInserted(QAbstractAnimationJob *anim)
348{
349 if (m_currentAnimation == nullptr)
350 RETURN_IF_DELETED(setCurrentAnimation(m_children.first())); // initialize the current animation
351
352 if (m_currentAnimation == m_children.next(current: anim)
353 && m_currentAnimation->currentTime() == 0 && m_currentAnimation->currentLoop() == 0) {
354 //in this case we simply insert the animation before the current one has actually started
355 RETURN_IF_DELETED(setCurrentAnimation(anim));
356 }
357
358// TODO
359// if (index < m_currentAnimationIndex || m_currentLoop != 0) {
360// qWarning("QSequentialGroup::insertAnimation only supports to add animations after the current one.");
361// return; //we're not affected because it is added after the current one
362// }
363}
364
365void QSequentialAnimationGroupJob::animationRemoved(QAbstractAnimationJob *anim, QAbstractAnimationJob *prev, QAbstractAnimationJob *next)
366{
367 QAnimationGroupJob::animationRemoved(anim, prev, next);
368
369 Q_ASSERT(m_currentAnimation); // currentAnimation should always be set
370
371 bool removingCurrent = anim == m_currentAnimation;
372 if (removingCurrent) {
373 if (next)
374 RETURN_IF_DELETED(setCurrentAnimation(next)); //let's try to take the next one
375 else if (prev)
376 RETURN_IF_DELETED(setCurrentAnimation(prev));
377 else// case all animations were removed
378 RETURN_IF_DELETED(setCurrentAnimation(nullptr));
379 }
380
381 // duration of the previous animations up to the current animation
382 m_currentTime = 0;
383 for (QAbstractAnimationJob *job : m_children) {
384 if (job == m_currentAnimation)
385 break;
386 m_currentTime += animationActualTotalDuration(anim: job);
387
388 }
389
390 if (!removingCurrent) {
391 //the current animation is not the one being removed
392 //so we add its current time to the current time of this group
393 m_currentTime += m_currentAnimation->currentTime();
394 }
395
396 //let's also update the total current time
397 m_totalCurrentTime = m_currentTime + m_loopCount * duration();
398}
399
400void QSequentialAnimationGroupJob::debugAnimation(QDebug d) const
401{
402 d << "SequentialAnimationGroupJob(" << Qt::hex << (const void *) this << Qt::dec << ")" << "currentAnimation:" << (void *)m_currentAnimation;
403
404 debugChildren(d);
405}
406
407QT_END_NAMESPACE
408

source code of qtdeclarative/src/qml/animations/qsequentialanimationgroupjob.cpp