| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtQml module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include <QtCore/qthreadstorage.h> |
| |
| #include "private/qabstractanimationjob_p.h" |
| #include "private/qanimationgroupjob_p.h" |
| #include "private/qanimationjobutil_p.h" |
| #include "private/qqmlengine_p.h" |
| #include "private/qqmlglobal_p.h" |
| |
| QT_BEGIN_NAMESPACE |
| |
| #ifndef QT_NO_THREAD |
| Q_GLOBAL_STATIC(QThreadStorage<QQmlAnimationTimer *>, animationTimer) |
| #endif |
| |
| DEFINE_BOOL_CONFIG_OPTION(animationTickDump, QML_ANIMATION_TICK_DUMP); |
| |
| QAnimationJobChangeListener::~QAnimationJobChangeListener() |
| { |
| } |
| |
| QQmlAnimationTimer::QQmlAnimationTimer() : |
| QAbstractAnimationTimer(), lastTick(0), |
| currentAnimationIdx(0), insideTick(false), |
| startAnimationPending(false), stopTimerPending(false), |
| runningLeafAnimations(0) |
| { |
| } |
| |
| QQmlAnimationTimer *QQmlAnimationTimer::instance(bool create) |
| { |
| QQmlAnimationTimer *inst; |
| if (create && !animationTimer()->hasLocalData()) { |
| inst = new QQmlAnimationTimer; |
| animationTimer()->setLocalData(inst); |
| } else { |
| inst = animationTimer() ? animationTimer()->localData() : 0; |
| } |
| return inst; |
| } |
| |
| QQmlAnimationTimer *QQmlAnimationTimer::instance() |
| { |
| return instance(true); |
| } |
| |
| void QQmlAnimationTimer::ensureTimerUpdate() |
| { |
| QUnifiedTimer *instU = QUnifiedTimer::instance(false); |
| if (instU && isPaused) |
| instU->updateAnimationTimers(-1); |
| } |
| |
| void QQmlAnimationTimer::updateAnimationsTime(qint64 delta) |
| { |
| //setCurrentTime can get this called again while we're the for loop. At least with pauseAnimations |
| if (insideTick) |
| return; |
| |
| lastTick += delta; |
| |
| //we make sure we only call update time if the time has actually changed |
| //it might happen in some cases that the time doesn't change because events are delayed |
| //when the CPU load is high |
| if (delta) { |
| insideTick = true; |
| for (currentAnimationIdx = 0; currentAnimationIdx < animations.count(); ++currentAnimationIdx) { |
| QAbstractAnimationJob *animation = animations.at(currentAnimationIdx); |
| int elapsed = animation->m_totalCurrentTime |
| + (animation->direction() == QAbstractAnimationJob::Forward ? delta : -delta); |
| animation->setCurrentTime(elapsed); |
| } |
| if (animationTickDump()) { |
| qDebug() << "***** Dumping Animation Tree ***** ( tick:" << lastTick << "delta:" << delta << ")"; |
| for (int i = 0; i < animations.count(); ++i) |
| qDebug() << animations.at(i); |
| } |
| insideTick = false; |
| currentAnimationIdx = 0; |
| } |
| } |
| |
| void QQmlAnimationTimer::updateAnimationTimer() |
| { |
| restartAnimationTimer(); |
| } |
| |
| void QQmlAnimationTimer::restartAnimationTimer() |
| { |
| if (runningLeafAnimations == 0 && !runningPauseAnimations.isEmpty()) |
| QUnifiedTimer::pauseAnimationTimer(this, closestPauseAnimationTimeToFinish()); |
| else if (isPaused) |
| QUnifiedTimer::resumeAnimationTimer(this); |
| else if (!isRegistered) |
| QUnifiedTimer::startAnimationTimer(this); |
| } |
| |
| void QQmlAnimationTimer::startAnimations() |
| { |
| if (!startAnimationPending) |
| return; |
| startAnimationPending = false; |
| //force timer to update, which prevents large deltas for our newly added animations |
| QUnifiedTimer::instance()->maybeUpdateAnimationsToCurrentTime(); |
| |
| //we transfer the waiting animations into the "really running" state |
| animations += animationsToStart; |
| animationsToStart.clear(); |
| if (!animations.isEmpty()) |
| restartAnimationTimer(); |
| } |
| |
| void QQmlAnimationTimer::stopTimer() |
| { |
| stopTimerPending = false; |
| bool pendingStart = startAnimationPending && animationsToStart.size() > 0; |
| if (animations.isEmpty() && !pendingStart) { |
| QUnifiedTimer::resumeAnimationTimer(this); |
| QUnifiedTimer::stopAnimationTimer(this); |
| // invalidate the start reference time |
| lastTick = 0; |
| } |
| } |
| |
| void QQmlAnimationTimer::registerAnimation(QAbstractAnimationJob *animation, bool isTopLevel) |
| { |
| if (animation->userControlDisabled()) |
| return; |
| |
| registerRunningAnimation(animation); |
| if (isTopLevel) { |
| Q_ASSERT(!animation->m_hasRegisteredTimer); |
| animation->m_hasRegisteredTimer = true; |
| animationsToStart << animation; |
| if (!startAnimationPending) { |
| startAnimationPending = true; |
| QMetaObject::invokeMethod(this, "startAnimations", Qt::QueuedConnection); |
| } |
| } |
| } |
| |
| void QQmlAnimationTimer::unregisterAnimation(QAbstractAnimationJob *animation) |
| { |
| unregisterRunningAnimation(animation); |
| |
| if (!animation->m_hasRegisteredTimer) |
| return; |
| |
| int idx = animations.indexOf(animation); |
| if (idx != -1) { |
| animations.removeAt(idx); |
| // this is needed if we unregister an animation while its running |
| if (idx <= currentAnimationIdx) |
| --currentAnimationIdx; |
| |
| if (animations.isEmpty() && !stopTimerPending) { |
| stopTimerPending = true; |
| QMetaObject::invokeMethod(this, "stopTimer", Qt::QueuedConnection); |
| } |
| } else { |
| animationsToStart.removeOne(animation); |
| } |
| animation->m_hasRegisteredTimer = false; |
| } |
| |
| void QQmlAnimationTimer::registerRunningAnimation(QAbstractAnimationJob *animation) |
| { |
| Q_ASSERT(!animation->userControlDisabled()); |
| |
| if (animation->m_isGroup) |
| return; |
| |
| if (animation->m_isPause) { |
| runningPauseAnimations << animation; |
| } else |
| runningLeafAnimations++; |
| } |
| |
| void QQmlAnimationTimer::unregisterRunningAnimation(QAbstractAnimationJob *animation) |
| { |
| if (animation->userControlDisabled()) |
| return; |
| |
| if (animation->m_isGroup) |
| return; |
| |
| if (animation->m_isPause) |
| runningPauseAnimations.removeOne(animation); |
| else |
| runningLeafAnimations--; |
| Q_ASSERT(runningLeafAnimations >= 0); |
| } |
| |
| int QQmlAnimationTimer::closestPauseAnimationTimeToFinish() |
| { |
| int closestTimeToFinish = INT_MAX; |
| for (int i = 0; i < runningPauseAnimations.size(); ++i) { |
| QAbstractAnimationJob *animation = runningPauseAnimations.at(i); |
| int timeToFinish; |
| |
| if (animation->direction() == QAbstractAnimationJob::Forward) |
| timeToFinish = animation->duration() - animation->currentLoopTime(); |
| else |
| timeToFinish = animation->currentLoopTime(); |
| |
| if (timeToFinish < closestTimeToFinish) |
| closestTimeToFinish = timeToFinish; |
| } |
| return closestTimeToFinish; |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| QAbstractAnimationJob::QAbstractAnimationJob() |
| : m_loopCount(1) |
| , m_group(nullptr) |
| , m_direction(QAbstractAnimationJob::Forward) |
| , m_state(QAbstractAnimationJob::Stopped) |
| , m_totalCurrentTime(0) |
| , m_currentTime(0) |
| , m_currentLoop(0) |
| , m_uncontrolledFinishTime(-1) |
| , m_currentLoopStartTime(0) |
| , m_nextSibling(nullptr) |
| , m_previousSibling(nullptr) |
| , m_hasRegisteredTimer(false) |
| , m_isPause(false) |
| , m_isGroup(false) |
| , m_disableUserControl(false) |
| , m_hasCurrentTimeChangeListeners(false) |
| , m_isRenderThreadJob(false) |
| , m_isRenderThreadProxy(false) |
| |
| { |
| } |
| |
| QAbstractAnimationJob::~QAbstractAnimationJob() |
| { |
| //we can't call stop here. Otherwise we get pure virtual calls |
| if (m_state != Stopped) { |
| State oldState = m_state; |
| m_state = Stopped; |
| stateChanged(oldState, m_state); |
| |
| Q_ASSERT(m_state == Stopped); |
| if (oldState == Running) { |
| Q_ASSERT(QQmlAnimationTimer::instance() == m_timer); |
| m_timer->unregisterAnimation(this); |
| } |
| Q_ASSERT(!m_hasRegisteredTimer); |
| } |
| |
| if (m_group) |
| m_group->removeAnimation(this); |
| } |
| |
| void QAbstractAnimationJob::fireTopLevelAnimationLoopChanged() |
| { |
| m_uncontrolledFinishTime = -1; |
| if (m_group) |
| m_currentLoopStartTime = 0; |
| topLevelAnimationLoopChanged(); |
| } |
| |
| void QAbstractAnimationJob::setState(QAbstractAnimationJob::State newState) |
| { |
| if (m_state == newState) |
| return; |
| |
| if (m_loopCount == 0) |
| return; |
| |
| if (!m_timer) |
| m_timer = QQmlAnimationTimer::instance(); |
| |
| State oldState = m_state; |
| int oldCurrentTime = m_currentTime; |
| int oldCurrentLoop = m_currentLoop; |
| Direction oldDirection = m_direction; |
| |
| // check if we should Rewind |
| if ((newState == Paused || newState == Running) && oldState == Stopped) { |
| //here we reset the time if needed |
| //we don't call setCurrentTime because this might change the way the animation |
| //behaves: changing the state or changing the current value |
| m_totalCurrentTime = m_currentTime = (m_direction == Forward) ? |
| 0 : (m_loopCount == -1 ? duration() : totalDuration()); |
| |
| // Reset uncontrolled finish time and currentLoopStartTime for this run. |
| m_uncontrolledFinishTime = -1; |
| if (!m_group) |
| m_currentLoopStartTime = m_totalCurrentTime; |
| } |
| |
| m_state = newState; |
| //(un)registration of the animation must always happen before calls to |
| //virtual function (updateState) to ensure a correct state of the timer |
| bool isTopLevel = !m_group || m_group->isStopped(); |
| if (oldState == Running) { |
| if (newState == Paused && m_hasRegisteredTimer) |
| m_timer->ensureTimerUpdate(); |
| //the animation, is not running any more |
| m_timer->unregisterAnimation(this); |
| } else if (newState == Running) { |
| m_timer->registerAnimation(this, isTopLevel); |
| } |
| |
| //starting an animation qualifies as a top level loop change |
| if (newState == Running && oldState == Stopped && !m_group) |
| fireTopLevelAnimationLoopChanged(); |
| |
| RETURN_IF_DELETED(updateState(newState, oldState)); |
| |
| if (newState != m_state) //this is to be safe if updateState changes the state |
| return; |
| |
| // Notify state change |
| RETURN_IF_DELETED(stateChanged(newState, oldState)); |
| if (newState != m_state) //this is to be safe if updateState changes the state |
| return; |
| |
| switch (m_state) { |
| case Paused: |
| break; |
| case Running: |
| { |
| // this ensures that the value is updated now that the animation is running |
| if (oldState == Stopped) { |
| m_currentLoop = 0; |
| if (isTopLevel) { |
| // currentTime needs to be updated if pauseTimer is active |
| RETURN_IF_DELETED(m_timer->ensureTimerUpdate()); |
| RETURN_IF_DELETED(setCurrentTime(m_totalCurrentTime)); |
| } |
| } |
| } |
| break; |
| case Stopped: |
| // Leave running state. |
| int dura = duration(); |
| |
| if (dura == -1 || m_loopCount < 0 |
| || (oldDirection == Forward && (oldCurrentTime * (oldCurrentLoop + 1)) == (dura * m_loopCount)) |
| || (oldDirection == Backward && oldCurrentTime == 0)) { |
| finished(); |
| } |
| break; |
| } |
| } |
| |
| void QAbstractAnimationJob::setDirection(Direction direction) |
| { |
| if (m_direction == direction) |
| return; |
| |
| if (m_state == Stopped) { |
| if (m_direction == Backward) { |
| m_currentTime = duration(); |
| m_currentLoop = m_loopCount - 1; |
| } else { |
| m_currentTime = 0; |
| m_currentLoop = 0; |
| } |
| } |
| |
| // the commands order below is important: first we need to setCurrentTime with the old direction, |
| // then update the direction on this and all children and finally restart the pauseTimer if needed |
| if (m_hasRegisteredTimer) |
| m_timer->ensureTimerUpdate(); |
| |
| m_direction = direction; |
| updateDirection(direction); |
| |
| if (m_hasRegisteredTimer) |
| // needed to update the timer interval in case of a pause animation |
| m_timer->updateAnimationTimer(); |
| } |
| |
| void QAbstractAnimationJob::setLoopCount(int loopCount) |
| { |
| m_loopCount = loopCount; |
| } |
| |
| int QAbstractAnimationJob::totalDuration() const |
| { |
| int dura = duration(); |
| if (dura <= 0) |
| return dura; |
| int loopcount = loopCount(); |
| if (loopcount < 0) |
| return -1; |
| return dura * loopcount; |
| } |
| |
| void QAbstractAnimationJob::setCurrentTime(int msecs) |
| { |
| msecs = qMax(msecs, 0); |
| // Calculate new time and loop. |
| int dura = duration(); |
| int totalDura; |
| int oldLoop = m_currentLoop; |
| |
| if (dura < 0 && m_direction == Forward) { |
| totalDura = -1; |
| if (m_uncontrolledFinishTime >= 0 && msecs >= m_uncontrolledFinishTime) { |
| msecs = m_uncontrolledFinishTime; |
| if (m_currentLoop == m_loopCount - 1) { |
| totalDura = m_uncontrolledFinishTime; |
| } else { |
| ++m_currentLoop; |
| m_currentLoopStartTime = msecs; |
| m_uncontrolledFinishTime = -1; |
| } |
| } |
| m_totalCurrentTime = msecs; |
| m_currentTime = msecs - m_currentLoopStartTime; |
| } else { |
| totalDura = dura <= 0 ? dura : ((m_loopCount < 0) ? -1 : dura * m_loopCount); |
| if (totalDura != -1) |
| msecs = qMin(totalDura, msecs); |
| m_totalCurrentTime = msecs; |
| |
| // Update new values. |
| m_currentLoop = ((dura <= 0) ? 0 : (msecs / dura)); |
| if (m_currentLoop == m_loopCount) { |
| //we're at the end |
| m_currentTime = qMax(0, dura); |
| m_currentLoop = qMax(0, m_loopCount - 1); |
| } else { |
| if (m_direction == Forward) { |
| m_currentTime = (dura <= 0) ? msecs : (msecs % dura); |
| } else { |
| m_currentTime = (dura <= 0) ? msecs : ((msecs - 1) % dura) + 1; |
| if (m_currentTime == dura) |
| --m_currentLoop; |
| } |
| } |
| } |
| |
| |
| if (m_currentLoop != oldLoop && !m_group) //### verify Running as well? |
| fireTopLevelAnimationLoopChanged(); |
| |
| RETURN_IF_DELETED(updateCurrentTime(m_currentTime)); |
| |
| if (m_currentLoop != oldLoop) |
| currentLoopChanged(); |
| |
| // All animations are responsible for stopping the animation when their |
| // own end state is reached; in this case the animation is time driven, |
| // and has reached the end. |
| if ((m_direction == Forward && m_totalCurrentTime == totalDura) |
| || (m_direction == Backward && m_totalCurrentTime == 0)) { |
| RETURN_IF_DELETED(stop()); |
| } |
| |
| if (m_hasCurrentTimeChangeListeners) |
| currentTimeChanged(m_currentTime); |
| } |
| |
| void QAbstractAnimationJob::start() |
| { |
| if (m_state == Running) |
| return; |
| |
| if (QQmlEnginePrivate::designerMode()) { |
| if (state() != Stopped) { |
| m_currentTime = duration(); |
| m_totalCurrentTime = totalDuration(); |
| setState(Running); |
| setState(Stopped); |
| } |
| } else { |
| setState(Running); |
| } |
| } |
| |
| void QAbstractAnimationJob::stop() |
| { |
| if (m_state == Stopped) |
| return; |
| setState(Stopped); |
| } |
| |
| void QAbstractAnimationJob::pause() |
| { |
| if (m_state == Stopped) { |
| qWarning("QAbstractAnimationJob::pause: Cannot pause a stopped animation"); |
| return; |
| } |
| |
| setState(Paused); |
| } |
| |
| void QAbstractAnimationJob::resume() |
| { |
| if (m_state != Paused) { |
| qWarning("QAbstractAnimationJob::resume: " |
| "Cannot resume an animation that is not paused"); |
| return; |
| } |
| setState(Running); |
| } |
| |
| void QAbstractAnimationJob::setEnableUserControl() |
| { |
| m_disableUserControl = false; |
| } |
| |
| bool QAbstractAnimationJob::userControlDisabled() const |
| { |
| return m_disableUserControl; |
| } |
| |
| void QAbstractAnimationJob::setDisableUserControl() |
| { |
| m_disableUserControl = true; |
| start(); |
| pause(); |
| } |
| |
| void QAbstractAnimationJob::updateState(QAbstractAnimationJob::State newState, |
| QAbstractAnimationJob::State oldState) |
| { |
| Q_UNUSED(oldState); |
| Q_UNUSED(newState); |
| } |
| |
| void QAbstractAnimationJob::updateDirection(QAbstractAnimationJob::Direction direction) |
| { |
| Q_UNUSED(direction); |
| } |
| |
| void QAbstractAnimationJob::finished() |
| { |
| //TODO: update this code so it is valid to delete the animation in animationFinished |
| for (const auto &change : changeListeners) { |
| if (change.types & QAbstractAnimationJob::Completion) { |
| RETURN_IF_DELETED(change.listener->animationFinished(this)); |
| } |
| } |
| |
| if (m_group && (duration() == -1 || loopCount() < 0)) { |
| //this is an uncontrolled animation, need to notify the group animation we are finished |
| m_group->uncontrolledAnimationFinished(this); |
| } |
| } |
| |
| void QAbstractAnimationJob::stateChanged(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State oldState) |
| { |
| for (const auto &change : changeListeners) { |
| if (change.types & QAbstractAnimationJob::StateChange) { |
| RETURN_IF_DELETED(change.listener->animationStateChanged(this, newState, oldState)); |
| } |
| } |
| } |
| |
| void QAbstractAnimationJob::currentLoopChanged() |
| { |
| for (const auto &change : changeListeners) { |
| if (change.types & QAbstractAnimationJob::CurrentLoop) { |
| RETURN_IF_DELETED(change.listener->animationCurrentLoopChanged(this)); |
| } |
| } |
| } |
| |
| void QAbstractAnimationJob::currentTimeChanged(int currentTime) |
| { |
| Q_ASSERT(m_hasCurrentTimeChangeListeners); |
| |
| for (const auto &change : changeListeners) { |
| if (change.types & QAbstractAnimationJob::CurrentTime) { |
| RETURN_IF_DELETED(change.listener->animationCurrentTimeChanged(this, currentTime)); |
| } |
| } |
| } |
| |
| void QAbstractAnimationJob::addAnimationChangeListener(QAnimationJobChangeListener *listener, QAbstractAnimationJob::ChangeTypes changes) |
| { |
| if (changes & QAbstractAnimationJob::CurrentTime) |
| m_hasCurrentTimeChangeListeners = true; |
| |
| changeListeners.push_back(ChangeListener(listener, changes)); |
| } |
| |
| void QAbstractAnimationJob::removeAnimationChangeListener(QAnimationJobChangeListener *listener, QAbstractAnimationJob::ChangeTypes changes) |
| { |
| m_hasCurrentTimeChangeListeners = false; |
| |
| const auto it = std::find(changeListeners.begin(), changeListeners.end(), ChangeListener(listener, changes)); |
| if (it != changeListeners.end()) |
| changeListeners.erase(it); |
| |
| for (const auto &change: changeListeners) { |
| if (change.types & QAbstractAnimationJob::CurrentTime) { |
| m_hasCurrentTimeChangeListeners = true; |
| break; |
| } |
| } |
| } |
| |
| void QAbstractAnimationJob::debugAnimation(QDebug d) const |
| { |
| d << "AbstractAnimationJob(" << Qt::hex << (const void *) this << Qt::dec << ") state:" |
| << m_state << "duration:" << duration(); |
| } |
| |
| QDebug operator<<(QDebug d, const QAbstractAnimationJob *job) |
| { |
| if (!job) { |
| d << "AbstractAnimationJob(null)"; |
| return d; |
| } |
| job->debugAnimation(d); |
| return d; |
| } |
| |
| QT_END_NAMESPACE |
| |
| //#include "moc_qabstractanimation2_p.cpp" |
| #include "moc_qabstractanimationjob_p.cpp" |