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