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