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 <QtCore/qthreadstorage.h>
5
6#include "private/qabstractanimationjob_p.h"
7#include "private/qanimationgroupjob_p.h"
8#include "private/qanimationjobutil_p.h"
9#include "private/qqmlengine_p.h"
10#include "private/qqmlglobal_p.h"
11#include "private/qdoubleendedlist_p.h"
12
13QT_BEGIN_NAMESPACE
14
15#ifndef QT_NO_THREAD
16Q_GLOBAL_STATIC(QThreadStorage<QQmlAnimationTimer *>, animationTimer)
17#endif
18
19DEFINE_BOOL_CONFIG_OPTION(animationTickDump, QML_ANIMATION_TICK_DUMP);
20
21QAnimationJobChangeListener::~QAnimationJobChangeListener()
22{
23}
24
25QQmlAnimationTimer::QQmlAnimationTimer() :
26 QAbstractAnimationTimer(), lastTick(0),
27 currentAnimationIdx(0), insideTick(false),
28 startAnimationPending(false), stopTimerPending(false),
29 runningLeafAnimations(0)
30{
31}
32
33void QQmlAnimationTimer::unsetJobTimer(QAbstractAnimationJob *animation)
34{
35 if (!animation)
36 return;
37 if (animation->m_timer == this)
38 animation->m_timer = nullptr;
39
40 if (animation->isGroup()) {
41 QAnimationGroupJob *group = static_cast<QAnimationGroupJob *>(animation);
42 if (const auto children = group->children()) {
43 for (auto *child : *children)
44 unsetJobTimer(animation: child);
45 }
46 }
47}
48
49QQmlAnimationTimer::~QQmlAnimationTimer()
50{
51 for (const auto &animation : std::as_const(t&: animations))
52 unsetJobTimer(animation);
53 for (const auto &animation : std::as_const(t&: animationsToStart))
54 unsetJobTimer(animation);
55 for (const auto &animation : std::as_const(t&: runningPauseAnimations))
56 unsetJobTimer(animation);
57}
58
59QQmlAnimationTimer *QQmlAnimationTimer::instance(bool create)
60{
61 QQmlAnimationTimer *inst;
62 if (create && !animationTimer()->hasLocalData()) {
63 inst = new QQmlAnimationTimer;
64 animationTimer()->setLocalData(inst);
65 } else {
66 inst = animationTimer() ? animationTimer()->localData() : 0;
67 }
68 return inst;
69}
70
71QQmlAnimationTimer *QQmlAnimationTimer::instance()
72{
73 return instance(create: true);
74}
75
76void QQmlAnimationTimer::ensureTimerUpdate()
77{
78 QUnifiedTimer *instU = QUnifiedTimer::instance(create: false);
79 if (instU && isPaused)
80 instU->updateAnimationTimers();
81}
82
83void QQmlAnimationTimer::updateAnimationsTime(qint64 delta)
84{
85 //setCurrentTime can get this called again while we're the for loop. At least with pauseAnimations
86 if (insideTick)
87 return;
88
89 lastTick += delta;
90
91 //we make sure we only call update time if the time has actually changed
92 //it might happen in some cases that the time doesn't change because events are delayed
93 //when the CPU load is high
94 if (delta) {
95 insideTick = true;
96 for (currentAnimationIdx = 0; currentAnimationIdx < animations.size(); ++currentAnimationIdx) {
97 QAbstractAnimationJob *animation = animations.at(i: currentAnimationIdx);
98 int elapsed = animation->m_totalCurrentTime
99 + (animation->direction() == QAbstractAnimationJob::Forward ? delta : -delta);
100 animation->setCurrentTime(elapsed);
101 }
102 if (animationTickDump()) {
103 qDebug() << "***** Dumping Animation Tree ***** ( tick:" << lastTick << "delta:" << delta << ")";
104 for (int i = 0; i < animations.size(); ++i)
105 qDebug() << animations.at(i);
106 }
107 insideTick = false;
108 currentAnimationIdx = 0;
109 }
110}
111
112void QQmlAnimationTimer::updateAnimationTimer()
113{
114 restartAnimationTimer();
115}
116
117void QQmlAnimationTimer::restartAnimationTimer()
118{
119 if (runningLeafAnimations == 0 && !runningPauseAnimations.isEmpty())
120 QUnifiedTimer::pauseAnimationTimer(timer: this, duration: closestPauseAnimationTimeToFinish());
121 else if (isPaused)
122 QUnifiedTimer::resumeAnimationTimer(timer: this);
123 else if (!isRegistered)
124 QUnifiedTimer::startAnimationTimer(timer: this);
125}
126
127void QQmlAnimationTimer::startAnimations()
128{
129 if (!startAnimationPending)
130 return;
131 startAnimationPending = false;
132 //force timer to update, which prevents large deltas for our newly added animations
133 QUnifiedTimer::instance()->maybeUpdateAnimationsToCurrentTime();
134
135 //we transfer the waiting animations into the "really running" state
136 animations += animationsToStart;
137 animationsToStart.clear();
138 if (!animations.isEmpty())
139 restartAnimationTimer();
140}
141
142void QQmlAnimationTimer::stopTimer()
143{
144 stopTimerPending = false;
145 bool pendingStart = startAnimationPending && animationsToStart.size() > 0;
146 if (animations.isEmpty() && !pendingStart) {
147 QUnifiedTimer::resumeAnimationTimer(timer: this);
148 QUnifiedTimer::stopAnimationTimer(timer: this);
149 // invalidate the start reference time
150 lastTick = 0;
151 }
152}
153
154void QQmlAnimationTimer::registerAnimation(QAbstractAnimationJob *animation, bool isTopLevel)
155{
156 if (animation->userControlDisabled())
157 return;
158
159 registerRunningAnimation(animation);
160 if (isTopLevel) {
161 Q_ASSERT(!animation->m_hasRegisteredTimer);
162 animation->m_hasRegisteredTimer = true;
163 animationsToStart << animation;
164 if (!startAnimationPending) {
165 startAnimationPending = true;
166 QMetaObject::invokeMethod(obj: this, member: "startAnimations", c: Qt::QueuedConnection);
167 }
168 }
169}
170
171void QQmlAnimationTimer::unregisterAnimation(QAbstractAnimationJob *animation)
172{
173 unregisterRunningAnimation(animation);
174
175 if (!animation->m_hasRegisteredTimer)
176 return;
177
178 int idx = animations.indexOf(t: animation);
179 if (idx != -1) {
180 animations.removeAt(i: idx);
181 // this is needed if we unregister an animation while its running
182 if (idx <= currentAnimationIdx)
183 --currentAnimationIdx;
184
185 if (animations.isEmpty() && !stopTimerPending) {
186 stopTimerPending = true;
187 QMetaObject::invokeMethod(obj: this, member: "stopTimer", c: Qt::QueuedConnection);
188 }
189 } else {
190 animationsToStart.removeOne(t: animation);
191 }
192 animation->m_hasRegisteredTimer = false;
193}
194
195void QQmlAnimationTimer::registerRunningAnimation(QAbstractAnimationJob *animation)
196{
197 Q_ASSERT(!animation->userControlDisabled());
198
199 if (animation->m_isGroup)
200 return;
201
202 if (animation->m_isPause) {
203 runningPauseAnimations << animation;
204 } else
205 runningLeafAnimations++;
206}
207
208void QQmlAnimationTimer::unregisterRunningAnimation(QAbstractAnimationJob *animation)
209{
210 unsetJobTimer(animation);
211 if (animation->userControlDisabled())
212 return;
213
214 if (animation->m_isGroup)
215 return;
216
217 if (animation->m_isPause)
218 runningPauseAnimations.removeOne(t: animation);
219 else
220 runningLeafAnimations--;
221 Q_ASSERT(runningLeafAnimations >= 0);
222}
223
224int QQmlAnimationTimer::closestPauseAnimationTimeToFinish()
225{
226 int closestTimeToFinish = INT_MAX;
227 for (int i = 0; i < runningPauseAnimations.size(); ++i) {
228 QAbstractAnimationJob *animation = runningPauseAnimations.at(i);
229 int timeToFinish;
230
231 if (animation->direction() == QAbstractAnimationJob::Forward)
232 timeToFinish = animation->duration() - animation->currentLoopTime();
233 else
234 timeToFinish = animation->currentLoopTime();
235
236 if (timeToFinish < closestTimeToFinish)
237 closestTimeToFinish = timeToFinish;
238 }
239 return closestTimeToFinish;
240}
241
242/////////////////////////////////////////////////////////////////////////////////////////////////////////
243
244QAbstractAnimationJob::QAbstractAnimationJob()
245 : m_loopCount(1)
246 , m_group(nullptr)
247 , m_direction(QAbstractAnimationJob::Forward)
248 , m_state(QAbstractAnimationJob::Stopped)
249 , m_totalCurrentTime(0)
250 , m_currentTime(0)
251 , m_currentLoop(0)
252 , m_uncontrolledFinishTime(-1)
253 , m_currentLoopStartTime(0)
254 , m_hasRegisteredTimer(false)
255 , m_isPause(false)
256 , m_isGroup(false)
257 , m_disableUserControl(false)
258 , m_hasCurrentTimeChangeListeners(false)
259 , m_isRenderThreadJob(false)
260 , m_isRenderThreadProxy(false)
261
262{
263}
264
265QAbstractAnimationJob::~QAbstractAnimationJob()
266{
267 //we can't call stop here. Otherwise we get pure virtual calls
268 if (m_state != Stopped) {
269 State oldState = m_state;
270 m_state = Stopped;
271 stateChanged(newState: oldState, oldState: m_state);
272
273 Q_ASSERT(m_state == Stopped);
274 if (oldState == Running) {
275 if (m_timer) {
276 Q_ASSERT(QQmlAnimationTimer::instance(false) == m_timer);
277 m_timer->unregisterAnimation(animation: this);
278 }
279 }
280 Q_ASSERT(!m_hasRegisteredTimer);
281 }
282
283 if (m_group)
284 m_group->removeAnimation(animation: this);
285}
286
287void QAbstractAnimationJob::fireTopLevelAnimationLoopChanged()
288{
289 m_uncontrolledFinishTime = -1;
290 if (m_group)
291 m_currentLoopStartTime = 0;
292 topLevelAnimationLoopChanged();
293}
294
295void QAbstractAnimationJob::setState(QAbstractAnimationJob::State newState)
296{
297 if (m_state == newState)
298 return;
299
300 if (m_loopCount == 0)
301 return;
302
303 if (!m_timer) // don't create a timer just to stop the animation
304 m_timer = QQmlAnimationTimer::instance(create: newState != Stopped);
305 Q_ASSERT(m_timer || newState == Stopped);
306
307 State oldState = m_state;
308 int oldCurrentTime = m_currentTime;
309 int oldCurrentLoop = m_currentLoop;
310 Direction oldDirection = m_direction;
311
312 // check if we should Rewind
313 if ((newState == Paused || newState == Running) && oldState == Stopped) {
314 //here we reset the time if needed
315 //we don't call setCurrentTime because this might change the way the animation
316 //behaves: changing the state or changing the current value
317 m_totalCurrentTime = m_currentTime = (m_direction == Forward) ?
318 0 : (m_loopCount == -1 ? duration() : totalDuration());
319
320 // Reset uncontrolled finish time and currentLoopStartTime for this run.
321 m_uncontrolledFinishTime = -1;
322 if (!m_group)
323 m_currentLoopStartTime = m_totalCurrentTime;
324 }
325
326 m_state = newState;
327 //(un)registration of the animation must always happen before calls to
328 //virtual function (updateState) to ensure a correct state of the timer
329 bool isTopLevel = !m_group || m_group->isStopped();
330 if (oldState == Running) {
331 if (newState == Paused && m_hasRegisteredTimer)
332 m_timer->ensureTimerUpdate();
333 // the animation is not running any more
334 if (m_timer)
335 m_timer->unregisterAnimation(animation: this);
336 } else if (newState == Running) {
337 m_timer->registerAnimation(animation: this, isTopLevel);
338 }
339
340 //starting an animation qualifies as a top level loop change
341 if (newState == Running && oldState == Stopped && !m_group)
342 fireTopLevelAnimationLoopChanged();
343
344 RETURN_IF_DELETED(updateState(newState, oldState));
345
346 if (newState != m_state) //this is to be safe if updateState changes the state
347 return;
348
349 // Notify state change
350 RETURN_IF_DELETED(stateChanged(newState, oldState));
351 if (newState != m_state) //this is to be safe if updateState changes the state
352 return;
353
354 switch (m_state) {
355 case Paused:
356 break;
357 case Running:
358 {
359 // this ensures that the value is updated now that the animation is running
360 if (oldState == Stopped) {
361 m_currentLoop = 0;
362 if (isTopLevel) {
363 // currentTime needs to be updated if pauseTimer is active
364 RETURN_IF_DELETED(m_timer->ensureTimerUpdate());
365 RETURN_IF_DELETED(setCurrentTime(m_totalCurrentTime));
366 }
367 }
368 }
369 break;
370 case Stopped:
371 // Leave running state.
372 int dura = duration();
373
374 if (dura == -1 || m_loopCount < 0
375 || (oldDirection == Forward && (oldCurrentTime * (oldCurrentLoop + 1)) == (dura * m_loopCount))
376 || (oldDirection == Backward && oldCurrentTime == 0)) {
377 finished();
378 }
379 break;
380 }
381}
382
383void QAbstractAnimationJob::setDirection(Direction direction)
384{
385 if (m_direction == direction)
386 return;
387
388 if (m_state == Stopped) {
389 if (m_direction == Backward) {
390 m_currentTime = duration();
391 m_currentLoop = m_loopCount - 1;
392 } else {
393 m_currentTime = 0;
394 m_currentLoop = 0;
395 }
396 }
397
398 // the commands order below is important: first we need to setCurrentTime with the old direction,
399 // then update the direction on this and all children and finally restart the pauseTimer if needed
400 if (m_hasRegisteredTimer)
401 m_timer->ensureTimerUpdate();
402
403 m_direction = direction;
404 updateDirection(direction);
405
406 if (m_hasRegisteredTimer)
407 // needed to update the timer interval in case of a pause animation
408 m_timer->updateAnimationTimer();
409}
410
411void QAbstractAnimationJob::setLoopCount(int loopCount)
412{
413 if (m_loopCount == loopCount)
414 return;
415 m_loopCount = loopCount;
416 updateLoopCount(loopCount);
417}
418
419int QAbstractAnimationJob::totalDuration() const
420{
421 int dura = duration();
422 if (dura <= 0)
423 return dura;
424 int loopcount = loopCount();
425 if (loopcount < 0)
426 return -1;
427 return dura * loopcount;
428}
429
430void QAbstractAnimationJob::setCurrentTime(int msecs)
431{
432 msecs = qMax(a: msecs, b: 0);
433 // Calculate new time and loop.
434 int dura = duration();
435 int totalDura;
436 int oldLoop = m_currentLoop;
437
438 if (dura < 0 && m_direction == Forward) {
439 totalDura = -1;
440 if (m_uncontrolledFinishTime >= 0 && msecs >= m_uncontrolledFinishTime) {
441 msecs = m_uncontrolledFinishTime;
442 if (m_currentLoop == m_loopCount - 1) {
443 totalDura = m_uncontrolledFinishTime;
444 } else {
445 ++m_currentLoop;
446 m_currentLoopStartTime = msecs;
447 m_uncontrolledFinishTime = -1;
448 }
449 }
450 m_totalCurrentTime = msecs;
451 m_currentTime = msecs - m_currentLoopStartTime;
452 } else {
453 totalDura = dura <= 0 ? dura : ((m_loopCount < 0) ? -1 : dura * m_loopCount);
454 if (totalDura != -1)
455 msecs = qMin(a: totalDura, b: msecs);
456 m_totalCurrentTime = msecs;
457
458 // Update new values.
459 m_currentLoop = ((dura <= 0) ? 0 : (msecs / dura));
460 if (m_currentLoop == m_loopCount) {
461 //we're at the end
462 m_currentTime = qMax(a: 0, b: dura);
463 m_currentLoop = qMax(a: 0, b: m_loopCount - 1);
464 } else {
465 if (m_direction == Forward) {
466 m_currentTime = (dura <= 0) ? msecs : (msecs % dura);
467 } else {
468 m_currentTime = (dura <= 0) ? msecs : ((msecs - 1) % dura) + 1;
469 if (m_currentTime == dura)
470 --m_currentLoop;
471 }
472 }
473 }
474
475
476 if (m_currentLoop != oldLoop && !m_group) //### verify Running as well?
477 fireTopLevelAnimationLoopChanged();
478
479 RETURN_IF_DELETED(updateCurrentTime(m_currentTime));
480
481 if (m_currentLoop != oldLoop) {
482 // CurrentLoop listeners may restart the job if e.g. from has changed. Stopping a job will
483 // destroy it, so account for that here.
484 RETURN_IF_DELETED(currentLoopChanged());
485 }
486
487 // All animations are responsible for stopping the animation when their
488 // own end state is reached; in this case the animation is time driven,
489 // and has reached the end.
490 if ((m_direction == Forward && m_totalCurrentTime == totalDura)
491 || (m_direction == Backward && m_totalCurrentTime == 0)) {
492 RETURN_IF_DELETED(stop());
493 }
494
495 if (m_hasCurrentTimeChangeListeners)
496 currentTimeChanged(currentTime: m_currentTime);
497}
498
499void QAbstractAnimationJob::start()
500{
501 if (m_state == Running)
502 return;
503
504 if (QQmlEnginePrivate::designerMode()) {
505 if (state() != Stopped) {
506 m_currentTime = duration();
507 m_totalCurrentTime = totalDuration();
508 setState(Running);
509 setState(Stopped);
510 }
511 } else {
512 setState(Running);
513 }
514}
515
516void QAbstractAnimationJob::stop()
517{
518 if (m_state == Stopped)
519 return;
520 setState(Stopped);
521}
522
523void QAbstractAnimationJob::complete()
524{
525 // Simulate the full animation cycle
526 setState(Running);
527 setCurrentTime(m_direction == Forward ? duration() : 0);
528 setState(Stopped);
529}
530
531void QAbstractAnimationJob::pause()
532{
533 if (m_state == Stopped) {
534 qWarning(msg: "QAbstractAnimationJob::pause: Cannot pause a stopped animation");
535 return;
536 }
537
538 setState(Paused);
539}
540
541void QAbstractAnimationJob::resume()
542{
543 if (m_state != Paused) {
544 qWarning(msg: "QAbstractAnimationJob::resume: "
545 "Cannot resume an animation that is not paused");
546 return;
547 }
548 setState(Running);
549}
550
551void QAbstractAnimationJob::setEnableUserControl()
552{
553 m_disableUserControl = false;
554}
555
556bool QAbstractAnimationJob::userControlDisabled() const
557{
558 return m_disableUserControl;
559}
560
561void QAbstractAnimationJob::setDisableUserControl()
562{
563 m_disableUserControl = true;
564 start();
565 pause();
566}
567
568void QAbstractAnimationJob::updateState(QAbstractAnimationJob::State newState,
569 QAbstractAnimationJob::State oldState)
570{
571 Q_UNUSED(oldState);
572 Q_UNUSED(newState);
573}
574
575void QAbstractAnimationJob::updateDirection(QAbstractAnimationJob::Direction direction)
576{
577 Q_UNUSED(direction);
578}
579
580void QAbstractAnimationJob::finished()
581{
582 //TODO: update this code so it is valid to delete the animation in animationFinished
583 for (const auto &change : changeListeners) {
584 if (change.types & QAbstractAnimationJob::Completion) {
585 RETURN_IF_DELETED(change.listener->animationFinished(this));
586 }
587 }
588
589 if (m_group && (duration() == -1 || loopCount() < 0)) {
590 //this is an uncontrolled animation, need to notify the group animation we are finished
591 m_group->uncontrolledAnimationFinished(animation: this);
592 }
593}
594
595void QAbstractAnimationJob::stateChanged(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State oldState)
596{
597 for (const auto &change : changeListeners) {
598 if (change.types & QAbstractAnimationJob::StateChange) {
599 RETURN_IF_DELETED(change.listener->animationStateChanged(this, newState, oldState));
600 }
601 }
602}
603
604void QAbstractAnimationJob::currentLoopChanged()
605{
606 for (const auto &change : changeListeners) {
607 if (change.types & QAbstractAnimationJob::CurrentLoop) {
608 RETURN_IF_DELETED(change.listener->animationCurrentLoopChanged(this));
609 }
610 }
611}
612
613void QAbstractAnimationJob::currentTimeChanged(int currentTime)
614{
615 Q_ASSERT(m_hasCurrentTimeChangeListeners);
616
617 for (const auto &change : changeListeners) {
618 if (change.types & QAbstractAnimationJob::CurrentTime) {
619 RETURN_IF_DELETED(change.listener->animationCurrentTimeChanged(this, currentTime));
620 }
621 }
622}
623
624void QAbstractAnimationJob::addAnimationChangeListener(QAnimationJobChangeListener *listener, QAbstractAnimationJob::ChangeTypes changes)
625{
626 if (changes & QAbstractAnimationJob::CurrentTime)
627 m_hasCurrentTimeChangeListeners = true;
628
629 changeListeners.push_back(x: ChangeListener(listener, changes));
630}
631
632void QAbstractAnimationJob::removeAnimationChangeListener(QAnimationJobChangeListener *listener, QAbstractAnimationJob::ChangeTypes changes)
633{
634 m_hasCurrentTimeChangeListeners = false;
635
636 const auto it = std::find(first: changeListeners.begin(), last: changeListeners.end(), val: ChangeListener(listener, changes));
637 if (it != changeListeners.end())
638 changeListeners.erase(position: it);
639
640 for (const auto &change: changeListeners) {
641 if (change.types & QAbstractAnimationJob::CurrentTime) {
642 m_hasCurrentTimeChangeListeners = true;
643 break;
644 }
645 }
646}
647
648void QAbstractAnimationJob::debugAnimation(QDebug d) const
649{
650 d << "AbstractAnimationJob(" << Qt::hex << (const void *) this << Qt::dec << ") state:"
651 << m_state << "duration:" << duration();
652}
653
654QDebug operator<<(QDebug d, const QAbstractAnimationJob *job)
655{
656 if (!job) {
657 d << "AbstractAnimationJob(null)";
658 return d;
659 }
660 job->debugAnimation(d);
661 return d;
662}
663
664QT_END_NAMESPACE
665
666//#include "moc_qabstractanimation2_p.cpp"
667#include "moc_qabstractanimationjob_p.cpp"
668

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