blob: 41a051a719392da9c7fd4ab157f8734ef483284e [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** 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 General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** 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-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtTest/QtTest>
#include <QtCore/qpropertyanimation.h>
#include <QtCore/qvariantanimation.h>
#include <private/qabstractanimation_p.h>
#include <QtGui/qtouchdevice.h>
#include <QtWidgets/qwidget.h>
Q_DECLARE_METATYPE(QAbstractAnimation::State)
class UncontrolledAnimation : public QPropertyAnimation
{
Q_OBJECT
public:
int duration() const { return -1; /* not time driven */ }
protected:
void updateCurrentTime(int currentTime)
{
QPropertyAnimation::updateCurrentTime(currentTime);
if (currentTime >= QPropertyAnimation::duration() || currentLoop() >= 1)
stop();
}
};
class MyObject : public QObject
{
Q_OBJECT
Q_PROPERTY(qreal x READ x WRITE setX)
public:
MyObject() : m_x(0) { }
qreal x() const { return m_x; }
void setX(qreal x) { m_x = x; }
private:
qreal m_x;
};
class DummyPropertyAnimation : public QPropertyAnimation
{
public:
DummyPropertyAnimation(QObject *parent = 0) : QPropertyAnimation(parent)
{
setTargetObject(&o);
this->setPropertyName("x");
setEndValue(100);
}
MyObject o;
};
class TestAnimationDriver : public QAnimationDriver
{
public:
TestAnimationDriver()
: QAnimationDriver()
, m_elapsed(0)
{
QUnifiedTimer::instance()->installAnimationDriver(this);
}
~TestAnimationDriver()
{
// This is to ensure that running animations are removed from the list of actual running
// animations.
QCoreApplication::sendPostedEvents();
QUnifiedTimer::instance()->uninstallAnimationDriver(this);
}
void wait(qint64 ms)
{
/*
* When QAbstractAnimation::start() is called it will end up calling
* QAnimationTimer::registerAnimation(). This will do
*
* QMetaObject::invokeMethod(inst, "startAnimations", Qt::QueuedConnection); // typeof(inst) == QAnimationTimer
*
* startAnimations() will again fire a queued connection to actually add the animation
* to the list of running animations:
*
* QMetaObject::invokeMethod(inst, "startTimers", Qt::QueuedConnection); // typeof(inst) == QUnifiedTimer
*
* We therefore have to call QCoreApplication::sendPostedEvents() twice here.
*/
QCoreApplication::sendPostedEvents();
QCoreApplication::sendPostedEvents();
// Simulates the ideal animation update freqency (approx. 60Hz)
static const int interval = 1000/60;
qint64 until = m_elapsed + ms;
while (m_elapsed < until) {
advanceAnimation(m_elapsed);
m_elapsed += interval;
}
advanceAnimation(m_elapsed);
// This is to make sure that animations that were started with DeleteWhenStopped
// will actually delete themselves within the test function.
// Normally, they won't be deleted until the main event loop is processed.
// Therefore, have to explicitly say that we want to process DeferredDelete events. Same
// trick is used by QTest::qWait().
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
qint64 elapsed() const override
{
return m_elapsed;
}
void start() override
{
d_func()->running = true;
m_elapsed = 0;
emit started();
}
void stop() override
{
d_func()->running = false;
emit stopped();
}
private:
qint64 m_elapsed;
Q_DECLARE_PRIVATE(QAnimationDriver)
};
class tst_QPropertyAnimation : public QObject
{
Q_OBJECT
public Q_SLOTS:
void initTestCase();
private slots:
void construction();
void setCurrentTime_data();
void setCurrentTime();
void statesAndSignals_data();
void statesAndSignals();
void deletion1();
void deletion2();
void deletion3();
void duration0();
void noStartValue();
void noStartValueWithLoop();
void startWhenAnotherIsRunning();
void easingcurve_data();
void easingcurve();
void startWithoutStartValue();
void startBackwardWithoutEndValue();
void playForwardBackward();
void interpolated();
void setStartEndValues_data();
void setStartEndValues();
void zeroDurationStart();
void zeroDurationForwardBackward();
void operationsInStates_data();
void operationsInStates();
void oneKeyValue();
void updateOnSetKeyValues();
void restart();
void valueChanged();
void twoAnimations();
void deletedInUpdateCurrentTime();
void totalDuration();
void zeroLoopCount();
void recursiveAnimations();
};
void tst_QPropertyAnimation::initTestCase()
{
qRegisterMetaType<QAbstractAnimation::State>("QAbstractAnimation::State");
qRegisterMetaType<QAbstractAnimation::DeletionPolicy>("QAbstractAnimation::DeletionPolicy");
}
class AnimationObject : public QObject
{
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue)
Q_PROPERTY(qreal realValue READ realValue WRITE setRealValue)
public:
AnimationObject(int startValue = 0)
: v(startValue), rv(startValue)
{ }
int value() const { return v; }
void setValue(int value) { v = value; }
qreal realValue() const { return rv; }
void setRealValue(qreal value) { rv = value; }
int v;
qreal rv;
};
void tst_QPropertyAnimation::construction()
{
QPropertyAnimation panimation;
}
void tst_QPropertyAnimation::setCurrentTime_data()
{
QTest::addColumn<int>("duration");
QTest::addColumn<int>("loopCount");
QTest::addColumn<int>("currentTime");
QTest::addColumn<int>("testCurrentTime");
QTest::addColumn<int>("testCurrentLoop");
QTest::newRow("-1") << -1 << 1 << 0 << 0 << 0;
QTest::newRow("0") << 0 << 1 << 0 << 0 << 0;
QTest::newRow("1") << 0 << 1 << 1 << 0 << 0;
QTest::newRow("2") << 0 << 2 << 1 << 0 << 0;
QTest::newRow("3") << 1 << 1 << 0 << 0 << 0;
QTest::newRow("4") << 1 << 1 << 1 << 1 << 0;
QTest::newRow("5") << 1 << 2 << 1 << 0 << 1;
QTest::newRow("6") << 1 << 2 << 2 << 1 << 1;
QTest::newRow("7") << 1 << 2 << 3 << 1 << 1;
QTest::newRow("8") << 1 << 3 << 2 << 0 << 2;
QTest::newRow("9") << 1 << 3 << 3 << 1 << 2;
QTest::newRow("a") << 10 << 1 << 0 << 0 << 0;
QTest::newRow("b") << 10 << 1 << 1 << 1 << 0;
QTest::newRow("c") << 10 << 1 << 10 << 10 << 0;
QTest::newRow("d") << 10 << 2 << 10 << 0 << 1;
QTest::newRow("e") << 10 << 2 << 11 << 1 << 1;
QTest::newRow("f") << 10 << 2 << 20 << 10 << 1;
QTest::newRow("g") << 10 << 2 << 21 << 10 << 1;
QTest::newRow("negloop 0") << 10 << -1 << 0 << 0 << 0;
QTest::newRow("negloop 1") << 10 << -1 << 10 << 0 << 1;
QTest::newRow("negloop 2") << 10 << -1 << 15 << 5 << 1;
QTest::newRow("negloop 3") << 10 << -1 << 20 << 0 << 2;
QTest::newRow("negloop 4") << 10 << -1 << 30 << 0 << 3;
}
void tst_QPropertyAnimation::setCurrentTime()
{
QFETCH(int, duration);
QFETCH(int, loopCount);
QFETCH(int, currentTime);
QFETCH(int, testCurrentTime);
QFETCH(int, testCurrentLoop);
QPropertyAnimation animation;
if (duration < 0)
QTest::ignoreMessage(QtWarningMsg, "QVariantAnimation::setDuration: cannot set a negative duration");
animation.setDuration(duration);
animation.setLoopCount(loopCount);
animation.setCurrentTime(currentTime);
QCOMPARE(animation.currentLoopTime(), testCurrentTime);
QCOMPARE(animation.currentLoop(), testCurrentLoop);
}
void tst_QPropertyAnimation::statesAndSignals_data()
{
QTest::addColumn<bool>("uncontrolled");
QTest::newRow("normal animation") << false;
QTest::newRow("animation with undefined duration") << true;
}
void tst_QPropertyAnimation::statesAndSignals()
{
QFETCH(bool, uncontrolled);
QPropertyAnimation *anim;
if (uncontrolled)
anim = new UncontrolledAnimation;
else
anim = new DummyPropertyAnimation;
anim->setDuration(100);
QSignalSpy finishedSpy(anim, &QPropertyAnimation::finished);
QSignalSpy runningSpy(anim, &QPropertyAnimation::stateChanged);
QSignalSpy currentLoopSpy(anim, &QPropertyAnimation::currentLoopChanged);
QVERIFY(finishedSpy.isValid());
QVERIFY(runningSpy.isValid());
QVERIFY(currentLoopSpy.isValid());
anim->setCurrentTime(1);
anim->setCurrentTime(100);
QCOMPARE(finishedSpy.count(), 0);
QCOMPARE(runningSpy.count(), 0);
QCOMPARE(currentLoopSpy.count(), 0);
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
anim->setLoopCount(3);
anim->setCurrentTime(101);
if (uncontrolled)
QSKIP("Uncontrolled animations don't handle looping");
QCOMPARE(currentLoopSpy.count(), 1);
QCOMPARE(anim->currentLoop(), 1);
anim->setCurrentTime(0);
QCOMPARE(currentLoopSpy.count(), 2);
QCOMPARE(anim->currentLoop(), 0);
anim->start();
QCOMPARE(anim->state(), QAnimationGroup::Running);
QCOMPARE(runningSpy.count(), 1); //anim must have started
QCOMPARE(anim->currentLoop(), 0);
runningSpy.clear();
anim->stop();
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
QCOMPARE(runningSpy.count(), 1); //anim must have stopped
QCOMPARE(finishedSpy.count(), 0);
QCOMPARE(anim->currentLoopTime(), 0);
QCOMPARE(anim->currentLoop(), 0);
QCOMPARE(currentLoopSpy.count(), 2);
runningSpy.clear();
{
TestAnimationDriver timeDriver;
anim->start();
timeDriver.wait(1000);
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
QCOMPARE(runningSpy.count(), 2); //started and stopped again
runningSpy.clear();
QCOMPARE(finishedSpy.count(), 1);
QCOMPARE(anim->currentLoopTime(), 100);
QCOMPARE(anim->currentLoop(), 2);
QCOMPARE(currentLoopSpy.count(), 4);
anim->start(); // auto-rewinds
QCOMPARE(anim->state(), QAnimationGroup::Running);
QCOMPARE(anim->currentTime(), 0);
QCOMPARE(anim->currentLoop(), 0);
QCOMPARE(currentLoopSpy.count(), 5);
QCOMPARE(runningSpy.count(), 1); // anim has started
QCOMPARE(finishedSpy.count(), 1);
QCOMPARE(anim->currentLoop(), 0);
runningSpy.clear();
timeDriver.wait(1000);
QCOMPARE(currentLoopSpy.count(), 7);
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
QCOMPARE(anim->currentLoop(), 2);
QCOMPARE(runningSpy.count(), 1); // anim has stopped
QCOMPARE(finishedSpy.count(), 2);
QCOMPARE(anim->currentLoopTime(), 100);
delete anim;
}
}
void tst_QPropertyAnimation::deletion1()
{
TestAnimationDriver timeDriver;
QObject *object = new QWidget;
QPointer<QPropertyAnimation> anim = new QPropertyAnimation(object, "minimumWidth");
//test that the animation is deleted correctly depending of the deletion flag passed in start()
QSignalSpy runningSpy(anim.data(), &QPropertyAnimation::stateChanged);
QSignalSpy finishedSpy(anim.data(), &QPropertyAnimation::finished);
QVERIFY(runningSpy.isValid());
QVERIFY(finishedSpy.isValid());
anim->setStartValue(10);
anim->setEndValue(20);
anim->setDuration(200);
anim->start();
QCOMPARE(runningSpy.count(), 1);
QCOMPARE(finishedSpy.count(), 0);
QVERIFY(anim);
QCOMPARE(anim->state(), QAnimationGroup::Running);
timeDriver.wait(100);
QVERIFY(anim);
QCOMPARE(anim->state(), QAnimationGroup::Running);
timeDriver.wait(150);
QVERIFY(anim); //The animation should not have been deleted
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
QCOMPARE(runningSpy.count(), 2);
QCOMPARE(finishedSpy.count(), 1);
anim->start(QVariantAnimation::DeleteWhenStopped);
QVERIFY(anim);
QCOMPARE(anim->state(), QAnimationGroup::Running);
timeDriver.wait(100);
QVERIFY(anim);
QCOMPARE(anim->state(), QAnimationGroup::Running);
timeDriver.wait(150);
QCOMPARE(runningSpy.count(), 4);
QCOMPARE(finishedSpy.count(), 2);
QVERIFY(!anim); //The animation must have been deleted
delete object;
}
void tst_QPropertyAnimation::deletion2()
{
TestAnimationDriver timeDriver;
//test that the animation get deleted if the object is deleted
QObject *object = new QWidget;
QPointer<QPropertyAnimation> anim = new QPropertyAnimation(object,"minimumWidth");
anim->setStartValue(10);
anim->setEndValue(20);
anim->setDuration(200);
QSignalSpy runningSpy(anim.data(), &QPropertyAnimation::stateChanged);
QSignalSpy finishedSpy(anim.data(), &QPropertyAnimation::finished);
QVERIFY(runningSpy.isValid());
QVERIFY(finishedSpy.isValid());
anim->setStartValue(10);
anim->setEndValue(20);
anim->setDuration(200);
anim->start();
timeDriver.wait(50);
QVERIFY(anim);
QCOMPARE(anim->state(), QAnimationGroup::Running);
QCOMPARE(runningSpy.count(), 1);
QCOMPARE(finishedSpy.count(), 0);
//we can't call deletaLater directly because the delete would only happen in the next loop of _this_ event loop
QTimer::singleShot(0, object, SLOT(deleteLater()));
timeDriver.wait(50);
QVERIFY(!anim->targetObject());
}
void tst_QPropertyAnimation::deletion3()
{
//test that the stopped signal is emit when the animation is destroyed
TestAnimationDriver timeDriver;
QObject *object = new QWidget;
QPropertyAnimation *anim = new QPropertyAnimation(object,"minimumWidth");
anim->setStartValue(10);
anim->setEndValue(20);
anim->setDuration(200);
QSignalSpy runningSpy(anim, &QPropertyAnimation::stateChanged);
QSignalSpy finishedSpy(anim, &QPropertyAnimation::finished);
QVERIFY(runningSpy.isValid());
QVERIFY(finishedSpy.isValid());
anim->start();
timeDriver.wait(50);
QCOMPARE(anim->state(), QAnimationGroup::Running);
QCOMPARE(runningSpy.count(), 1);
QCOMPARE(finishedSpy.count(), 0);
delete anim;
QCOMPARE(runningSpy.count(), 2);
QCOMPARE(finishedSpy.count(), 0);
}
void tst_QPropertyAnimation::duration0()
{
QObject o;
o.setProperty("ole", 42);
QCOMPARE(o.property("ole").toInt(), 42);
QPropertyAnimation animation(&o, "ole");
animation.setEndValue(43);
QVERIFY(!animation.currentValue().isValid());
QCOMPARE(animation.currentValue().toInt(), 0);
animation.setStartValue(42);
QVERIFY(animation.currentValue().isValid());
QCOMPARE(animation.currentValue().toInt(), 42);
QCOMPARE(o.property("ole").toInt(), 42);
animation.setDuration(0);
QCOMPARE(animation.currentValue().toInt(), 43); //it is at the end
animation.start();
QCOMPARE(animation.state(), QAnimationGroup::Stopped);
QCOMPARE(animation.currentTime(), 0);
QCOMPARE(o.property("ole").toInt(), 43);
}
class StartValueTester : public QObject
{
Q_OBJECT
Q_PROPERTY(int ole READ ole WRITE setOle)
public:
StartValueTester() : o(0) { }
int ole() const { return o; }
void setOle(int v) { o = v; values << v; }
int o;
QVector<int> values;
};
void tst_QPropertyAnimation::noStartValue()
{
TestAnimationDriver timeDriver;
StartValueTester o;
o.setProperty("ole", 42);
o.values.clear();
QPropertyAnimation a(&o, "ole");
a.setEndValue(420);
a.setDuration(250);
a.start();
timeDriver.wait(a.duration());
QCOMPARE(o.values.value(o.values.size() - 1, -1), 420);
QCOMPARE(o.values.first(), 42);
}
void tst_QPropertyAnimation::noStartValueWithLoop()
{
StartValueTester o;
o.setProperty("ole", 42);
o.values.clear();
QPropertyAnimation a(&o, "ole");
a.setEndValue(420);
a.setDuration(250);
a.setLoopCount(2);
a.start();
a.setCurrentTime(250);
QCOMPARE(o.values.first(), 42);
QCOMPARE(a.currentValue().toInt(), 42);
QCOMPARE(o.values.last(), 42);
a.setCurrentTime(500);
QCOMPARE(a.currentValue().toInt(), 420);
}
void tst_QPropertyAnimation::startWhenAnotherIsRunning()
{
StartValueTester o;
o.setProperty("ole", 42);
o.values.clear();
TestAnimationDriver timeDriver;
{
//normal case: the animation finishes and is deleted
QPointer<QVariantAnimation> anim = new QPropertyAnimation(&o, "ole");
anim->setEndValue(100);
QSignalSpy runningSpy(anim.data(), &QVariantAnimation::stateChanged);
QVERIFY(runningSpy.isValid());
anim->start(QVariantAnimation::DeleteWhenStopped);
timeDriver.wait(anim->duration());
QCOMPARE(runningSpy.count(), 2); //started and then stopped
QVERIFY(!anim);
}
{
QPointer<QVariantAnimation> anim = new QPropertyAnimation(&o, "ole");
anim->setEndValue(100);
QSignalSpy runningSpy(anim.data(), &QVariantAnimation::stateChanged);
QVERIFY(runningSpy.isValid());
anim->start(QVariantAnimation::DeleteWhenStopped);
timeDriver.wait(anim->duration()/2);
QPointer<QVariantAnimation> anim2 = new QPropertyAnimation(&o, "ole");
anim2->setEndValue(100);
QCOMPARE(runningSpy.count(), 1);
QCOMPARE(anim->state(), QVariantAnimation::Running);
//anim2 will interrupt anim1
QMetaObject::invokeMethod(anim2, "start", Qt::QueuedConnection, Q_ARG(QAbstractAnimation::DeletionPolicy, QVariantAnimation::DeleteWhenStopped));
timeDriver.wait(50);
QVERIFY(!anim); //anim should have been deleted
QVERIFY(anim2);
timeDriver.wait(anim2->duration());
QVERIFY(!anim2); //anim2 is finished: it should have been deleted by now
QVERIFY(!anim);
}
}
// copy from easing.cpp in case that function changes definition
static qreal easeInOutBack(qreal t)
{
qreal s = 1.70158;
qreal t_adj = 2.0f * (qreal)t;
if (t_adj < 1) {
s *= 1.525f;
return 1.0/2*(t_adj*t_adj*((s+1)*t_adj - s));
} else {
t_adj -= 2;
s *= 1.525f;
return 1.0/2*(t_adj*t_adj*((s+1)*t_adj + s) + 2);
}
}
void tst_QPropertyAnimation::easingcurve_data()
{
QTest::addColumn<int>("currentTime");
QTest::addColumn<int>("expectedvalue");
QTest::newRow("interpolation1") << 0 << 0;
QTest::newRow("interpolation2") << 1000 << 1000;
QTest::newRow("extrapolationbelow") << 250 << -99;
QTest::newRow("extrapolationabove") << 750 << 1099;
}
void tst_QPropertyAnimation::easingcurve()
{
QFETCH(int, currentTime);
QFETCH(int, expectedvalue);
QObject o;
o.setProperty("ole", 42);
QCOMPARE(o.property("ole").toInt(), 42);
QPropertyAnimation pAnimation(&o, "ole");
pAnimation.setStartValue(0);
pAnimation.setEndValue(1000);
pAnimation.setDuration(1000);
// this easingcurve assumes that we extrapolate before startValue and after endValue
QEasingCurve easingCurve;
easingCurve.setCustomType(easeInOutBack);
pAnimation.setEasingCurve(easingCurve);
pAnimation.start();
pAnimation.pause();
pAnimation.setCurrentTime(currentTime);
QCOMPARE(o.property("ole").toInt(), expectedvalue);
}
void tst_QPropertyAnimation::startWithoutStartValue()
{
TestAnimationDriver timeDriver;
QObject o;
o.setProperty("ole", 42);
QCOMPARE(o.property("ole").toInt(), 42);
QPropertyAnimation anim(&o, "ole");
anim.setEndValue(100);
anim.start();
timeDriver.wait(100);
int current = anim.currentValue().toInt();
//it is somewhere in the animation
QVERIFY(current > 42);
QVERIFY(current < 100);
timeDriver.wait(200);
QCOMPARE(anim.state(), QVariantAnimation::Stopped);
current = anim.currentValue().toInt();
QCOMPARE(current, 100);
QCOMPARE(o.property("ole").toInt(), current);
anim.setEndValue(110);
anim.start();
current = anim.currentValue().toInt();
// the default start value will reevaluate the current property
// and set it to the end value of the last iteration
QCOMPARE(current, 100);
timeDriver.wait(100);
current = anim.currentValue().toInt();
//it is somewhere in the animation
QVERIFY(current >= 100);
QVERIFY(current <= 110);
}
void tst_QPropertyAnimation::startBackwardWithoutEndValue()
{
TestAnimationDriver timeDriver;
QObject o;
o.setProperty("ole", 42);
QCOMPARE(o.property("ole").toInt(), 42);
QPropertyAnimation anim(&o, "ole");
anim.setStartValue(100);
anim.setDirection(QAbstractAnimation::Backward);
//we start without an end value
anim.start();
QCOMPARE(anim.state(), QAbstractAnimation::Running);
QCOMPARE(o.property("ole").toInt(), 42); //the initial value
timeDriver.wait(100);
int current = anim.currentValue().toInt();
//it is somewhere in the animation
QVERIFY(current > 42);
QVERIFY(current < 100);
timeDriver.wait(200);
QCOMPARE(anim.state(), QVariantAnimation::Stopped);
current = anim.currentValue().toInt();
QCOMPARE(current, 100);
QCOMPARE(o.property("ole").toInt(), current);
anim.setStartValue(110);
anim.start();
current = anim.currentValue().toInt();
// the default start value will reevaluate the current property
// and set it to the end value of the last iteration
QCOMPARE(current, 100);
timeDriver.wait(100);
current = anim.currentValue().toInt();
//it is somewhere in the animation
QVERIFY(current >= 100);
QVERIFY(current <= 110);
}
void tst_QPropertyAnimation::playForwardBackward()
{
TestAnimationDriver timeDriver;
QObject o;
o.setProperty("ole", 0);
QCOMPARE(o.property("ole").toInt(), 0);
QPropertyAnimation anim(&o, "ole");
anim.setStartValue(0);
anim.setEndValue(100);
anim.start();
timeDriver.wait(anim.duration());
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(anim.currentTime(), anim.duration());
//the animation is at the end
anim.setDirection(QVariantAnimation::Backward);
anim.start();
QCOMPARE(anim.state(), QAbstractAnimation::Running);
timeDriver.wait(anim.duration());
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(anim.currentTime(), 0);
//the direction is backward
//restarting should jump to the end
anim.start();
QCOMPARE(anim.state(), QAbstractAnimation::Running);
QCOMPARE(anim.currentTime(), anim.duration());
timeDriver.wait(anim.duration());
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(anim.currentTime(), 0);
}
struct Number
{
Number() {}
Number(int n)
: n(n) {}
bool operator==(const Number &other) const {
return n == other.n;
}
int n;
};
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(Number, Q_PRIMITIVE_TYPE);
QT_END_NAMESPACE
Q_DECLARE_METATYPE(Number)
QVariant numberInterpolator(const Number &f, const Number &t, qreal progress)
{
return QVariant::fromValue<Number>(Number(f.n + (t.n - f.n)*progress));
}
QVariant xaxisQPointInterpolator(const QPointF &f, const QPointF &t, qreal progress)
{
return QPointF(f.x() + (t.x() - f.x())*progress, f.y());
}
void tst_QPropertyAnimation::interpolated()
{
QObject o;
o.setProperty("point", QPointF()); //this will avoid warnings
o.setProperty("number", QVariant::fromValue<Number>(Number(42)));
QCOMPARE(qvariant_cast<Number>(o.property("number")), Number(42));
{
qRegisterAnimationInterpolator<Number>(numberInterpolator);
QPropertyAnimation anim(&o, "number");
anim.setStartValue(QVariant::fromValue<Number>(Number(0)));
anim.setEndValue(QVariant::fromValue<Number>(Number(100)));
anim.setDuration(1000);
anim.start();
anim.pause();
anim.setCurrentTime(100);
Number t(qvariant_cast<Number>(o.property("number")));
QCOMPARE(t, Number(10));
anim.setCurrentTime(500);
QCOMPARE(qvariant_cast<Number>(o.property("number")), Number(50));
}
{
qRegisterAnimationInterpolator<QPointF>(xaxisQPointInterpolator);
QPropertyAnimation anim(&o, "point");
anim.setStartValue(QPointF(0,0));
anim.setEndValue(QPointF(100, 100));
anim.setDuration(1000);
anim.start();
anim.pause();
anim.setCurrentTime(100);
QCOMPARE(o.property("point"), QVariant(QPointF(10, 0)));
anim.setCurrentTime(500);
QCOMPARE(o.property("point"), QVariant(QPointF(50, 0)));
}
{
// unregister it and see if we get back the default behaviour
qRegisterAnimationInterpolator<QPointF>(0);
QPropertyAnimation anim(&o, "point");
anim.setStartValue(QPointF(0,0));
anim.setEndValue(QPointF(100, 100));
anim.setDuration(1000);
anim.start();
anim.pause();
anim.setCurrentTime(100);
QCOMPARE(o.property("point").toPointF(), QPointF(10, 10));
anim.setCurrentTime(500);
QCOMPARE(o.property("point").toPointF(), QPointF(50, 50));
}
{
// Interpolate a qreal property with a int interpolator
AnimationObject o1;
o1.setRealValue(42.42);
QPropertyAnimation anim(&o1, "realValue");
anim.setStartValue(0);
anim.setEndValue(100);
anim.start();
QCOMPARE(o1.realValue(), qreal(0));
anim.setCurrentTime(250);
QCOMPARE(o1.realValue(), qreal(100));
}
}
void tst_QPropertyAnimation::setStartEndValues_data()
{
QTest::addColumn<QByteArray>("propertyName");
QTest::addColumn<QVariant>("initialValue");
QTest::addColumn<QVariant>("startValue");
QTest::addColumn<QVariant>("endValue");
QTest::newRow("dynamic property") << QByteArray("ole") << QVariant(42) << QVariant(0) << QVariant(10);
QTest::newRow("real property, with unmatching types") << QByteArray("x") << QVariant(42.) << QVariant(0) << QVariant(10.);
}
void tst_QPropertyAnimation::setStartEndValues()
{
MyObject object;
QFETCH(QByteArray, propertyName);
QFETCH(QVariant, initialValue);
QFETCH(QVariant, startValue);
QFETCH(QVariant, endValue);
//this tests the start value, end value and default start value
object.setProperty(propertyName, initialValue);
QPropertyAnimation anim(&object, propertyName);
QVariantAnimation::KeyValues values;
QCOMPARE(anim.keyValues(), values);
//let's add a start value
anim.setStartValue(startValue);
values << QVariantAnimation::KeyValue(0, startValue);
QCOMPARE(anim.keyValues(), values);
anim.setEndValue(endValue);
values << QVariantAnimation::KeyValue(1, endValue);
QCOMPARE(anim.keyValues(), values);
//now we can play with objects
QCOMPARE(object.property(propertyName).toDouble(), initialValue.toDouble());
anim.start();
QVERIFY(anim.startValue().isValid());
QCOMPARE(object.property(propertyName), anim.startValue());
anim.setCurrentTime(anim.duration()/2);
QCOMPARE(object.property(propertyName).toDouble(), (startValue.toDouble() + endValue.toDouble())/2 ); //just in the middle of the animation
anim.setCurrentTime(anim.duration()); //we go to the end of the animation
QCOMPARE(anim.state(), QAnimationGroup::Stopped); //it should have stopped
QVERIFY(anim.endValue().isValid());
QCOMPARE(object.property(propertyName), anim.endValue()); //end of the animations
//now we remove the explicit start value and test the implicit one
anim.stop();
object.setProperty(propertyName, initialValue);
//let's reset the start value
values.remove(0);
anim.setStartValue(QVariant());
QCOMPARE(anim.keyValues(), values);
QVERIFY(!anim.startValue().isValid());
anim.start();
QCOMPARE(object.property(propertyName), initialValue);
anim.setCurrentTime(anim.duration()/2);
QCOMPARE(object.property(propertyName).toDouble(), (initialValue.toDouble() + endValue.toDouble())/2 ); //just in the middle of the animation
anim.setCurrentTime(anim.duration()); //we go to the end of the animation
QCOMPARE(anim.state(), QAnimationGroup::Stopped); //it should have stopped
QVERIFY(anim.endValue().isValid());
QCOMPARE(object.property(propertyName), anim.endValue()); //end of the animations
//now we set back the startValue
anim.setStartValue(startValue);
QVERIFY(anim.startValue().isValid());
anim.start();
QCOMPARE(object.property(propertyName), startValue);
}
void tst_QPropertyAnimation::zeroDurationStart()
{
DummyPropertyAnimation anim;
QSignalSpy spy(&anim, &DummyPropertyAnimation::stateChanged);
QVERIFY(spy.isValid());
anim.setDuration(0);
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
anim.start();
//the animation stops immediately
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(spy.count(), 2);
//let's check the first state change
const QVariantList firstChange = spy.first();
//old state
QCOMPARE(qvariant_cast<QAbstractAnimation::State>(firstChange.last()), QAbstractAnimation::Stopped);
//new state
QCOMPARE(qvariant_cast<QAbstractAnimation::State>(firstChange.first()), QAbstractAnimation::Running);
//let's check the first state change
const QVariantList secondChange = spy.last();
//old state
QCOMPARE(qvariant_cast<QAbstractAnimation::State>(secondChange.last()), QAbstractAnimation::Running);
//new state
QCOMPARE(qvariant_cast<QAbstractAnimation::State>(secondChange.first()), QAbstractAnimation::Stopped);
}
void tst_QPropertyAnimation::zeroDurationForwardBackward()
{
QObject o; o.setProperty("test", 1);
QObject o2; o2.setProperty("test", 2);
QObject o3; o3.setProperty("test", 3);
QObject o4; o4.setProperty("test", 4);
QPropertyAnimation prop(&o, "test"); prop.setDuration(0); prop.setStartValue(1); prop.setEndValue(2);
prop.start();
QCOMPARE(o.property("test").toInt(), 2);
prop.setDirection(QAbstractAnimation::Backward);
prop.start();
QCOMPARE(o.property("test").toInt(), 1);
prop.setDirection(QAbstractAnimation::Forward);
QPropertyAnimation prop2(&o2, "test"); prop2.setDuration(0); prop2.setStartValue(2); prop2.setEndValue(3);
QPropertyAnimation prop3(&o3, "test"); prop3.setDuration(0); prop3.setStartValue(3); prop3.setEndValue(4);
QPropertyAnimation prop4(&o4, "test"); prop4.setDuration(0); prop4.setStartValue(4); prop4.setEndValue(5);
QSequentialAnimationGroup group;
group.addAnimation(&prop);
group.addAnimation(&prop2);
group.addAnimation(&prop3);
group.addAnimation(&prop4);
group.start();
QCOMPARE(o.property("test").toInt(), 2);
QCOMPARE(o2.property("test").toInt(), 3);
QCOMPARE(o3.property("test").toInt(), 4);
QCOMPARE(o4.property("test").toInt(), 5);
group.setDirection(QAbstractAnimation::Backward);
group.start();
QCOMPARE(o.property("test").toInt(), 1);
QCOMPARE(o2.property("test").toInt(), 2);
QCOMPARE(o3.property("test").toInt(), 3);
QCOMPARE(o4.property("test").toInt(), 4);
group.removeAnimation(&prop);
group.removeAnimation(&prop2);
group.removeAnimation(&prop3);
group.removeAnimation(&prop4);
}
#define Pause 1
#define Start 2
#define Resume 3
#define Stop 4
void tst_QPropertyAnimation::operationsInStates_data()
{
QTest::addColumn<QAbstractAnimation::State>("originState");
QTest::addColumn<int>("operation");
QTest::addColumn<QString>("expectedWarning");
QTest::addColumn<QAbstractAnimation::State>("expectedState");
QString pauseWarn(QLatin1String("QAbstractAnimation::pause: Cannot pause a stopped animation"));
QString resumeWarn(QLatin1String("QAbstractAnimation::resume: Cannot resume an animation that is not paused"));
QTest::newRow("S-pause") << QAbstractAnimation::Stopped << Pause << pauseWarn << QAbstractAnimation::Stopped;
QTest::newRow("S-start") << QAbstractAnimation::Stopped << Start << QString() << QAbstractAnimation::Running;
QTest::newRow("S-resume") << QAbstractAnimation::Stopped << Resume << resumeWarn << QAbstractAnimation::Stopped;
QTest::newRow("S-stop") << QAbstractAnimation::Stopped << Stop << QString() << QAbstractAnimation::Stopped;
QTest::newRow("P-pause") << QAbstractAnimation::Paused << Pause << QString() << QAbstractAnimation::Paused;
QTest::newRow("P-start") << QAbstractAnimation::Paused << Start << QString() << QAbstractAnimation::Running;
QTest::newRow("P-resume") << QAbstractAnimation::Paused << Resume << QString() << QAbstractAnimation::Running;
QTest::newRow("P-stop") << QAbstractAnimation::Paused << Stop << QString() << QAbstractAnimation::Stopped;
QTest::newRow("R-pause") << QAbstractAnimation::Running << Pause << QString() << QAbstractAnimation::Paused;
QTest::newRow("R-start") << QAbstractAnimation::Running << Start << QString() << QAbstractAnimation::Running;
QTest::newRow("R-resume") << QAbstractAnimation::Running << Resume << resumeWarn << QAbstractAnimation::Running;
QTest::newRow("R-stop") << QAbstractAnimation::Running << Stop << QString() << QAbstractAnimation::Stopped;
}
void tst_QPropertyAnimation::operationsInStates()
{
/**
* | pause() |start() |resume() |stop()
* ----------+------------+-----------+-----------+-------------------+
* Stopped | Stopped |Running |Stopped |Stopped |
* _| qWarning |restart |qWarning | |
* Paused | Paused |Running |Running |Stopped |
* _| | | | |
* Running | Paused |Running |Running |Stopped |
* | |restart |qWarning | |
* ----------+------------+-----------+-----------+-------------------+
**/
QFETCH(QAbstractAnimation::State, originState);
QFETCH(int, operation);
QFETCH(QString, expectedWarning);
QFETCH(QAbstractAnimation::State, expectedState);
QObject o;
o.setProperty("ole", 42);
QPropertyAnimation anim(&o, "ole");
anim.setEndValue(100);
QSignalSpy spy(&anim, &QPropertyAnimation::stateChanged);
QVERIFY(spy.isValid());
anim.stop();
switch (originState) {
case QAbstractAnimation::Stopped:
break;
case QAbstractAnimation::Paused:
anim.start();
anim.pause();
break;
case QAbstractAnimation::Running:
anim.start();
break;
}
if (!expectedWarning.isEmpty()) {
QTest::ignoreMessage(QtWarningMsg, qPrintable(expectedWarning));
}
QCOMPARE(anim.state(), originState);
switch (operation) {
case Pause:
anim.pause();
break;
case Start:
anim.start();
break;
case Resume:
anim.resume();
break;
case Stop:
anim.stop();
break;
}
QCOMPARE(anim.state(), expectedState);
}
#undef Pause
#undef Start
#undef Resume
#undef Stop
void tst_QPropertyAnimation::oneKeyValue()
{
QObject o;
o.setProperty("ole", 42);
QCOMPARE(o.property("ole").toInt(), 42);
QPropertyAnimation animation(&o, "ole");
animation.setStartValue(43);
animation.setEndValue(44);
animation.setDuration(100);
animation.setCurrentTime(0);
QVERIFY(animation.currentValue().isValid());
QCOMPARE(animation.currentValue().toInt(), 43);
QCOMPARE(o.property("ole").toInt(), 42);
// remove the last key value
animation.setKeyValueAt(1.0, QVariant());
// we will neither interpolate, nor update the current value
// since there is only one 1 key value defined
animation.setCurrentTime(100);
// the animation should not have been modified
QVERIFY(animation.currentValue().isValid());
QCOMPARE(animation.currentValue().toInt(), 43);
QCOMPARE(o.property("ole").toInt(), 42);
}
void tst_QPropertyAnimation::updateOnSetKeyValues()
{
QObject o;
o.setProperty("ole", 100);
QCOMPARE(o.property("ole").toInt(), 100);
QPropertyAnimation animation(&o, "ole");
animation.setStartValue(100);
animation.setEndValue(200);
animation.setDuration(100);
animation.setCurrentTime(50);
QCOMPARE(animation.currentValue().toInt(), 150);
animation.setKeyValueAt(0.0, 300);
QCOMPARE(animation.currentValue().toInt(), 250);
o.setProperty("ole", 100);
QPropertyAnimation animation2(&o, "ole");
QVariantAnimation::KeyValues kValues;
kValues << QVariantAnimation::KeyValue(0.0, 100) << QVariantAnimation::KeyValue(1.0, 200);
animation2.setKeyValues(kValues);
animation2.setDuration(100);
animation2.setCurrentTime(50);
QCOMPARE(animation2.currentValue().toInt(), 150);
kValues.clear();
kValues << QVariantAnimation::KeyValue(0.0, 300) << QVariantAnimation::KeyValue(1.0, 200);
animation2.setKeyValues(kValues);
QCOMPARE(animation2.currentValue().toInt(), animation.currentValue().toInt());
}
//this class will 'throw' an error in the test lib
// if the property ole is set to ErrorValue
class MyErrorObject : public QObject
{
Q_OBJECT
Q_PROPERTY(int ole READ ole WRITE setOle)
public:
static const int ErrorValue = 10000;
MyErrorObject() : m_ole(0) { }
int ole() const { return m_ole; }
void setOle(int o)
{
QVERIFY(o != ErrorValue);
m_ole = o;
}
private:
int m_ole;
};
void tst_QPropertyAnimation::restart()
{
//here we check that be restarting an animation
//it doesn't get an bogus intermediate value (end value)
//because the time is not yet reset to 0
MyErrorObject o;
o.setOle(100);
QCOMPARE(o.property("ole").toInt(), 100);
QPropertyAnimation anim(&o, "ole");
anim.setEndValue(200);
anim.start();
anim.setCurrentTime(anim.duration());
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(o.property("ole").toInt(), 200);
//we'll check that the animation never gets a wrong value when starting it
//after having changed the end value
anim.setEndValue(MyErrorObject::ErrorValue);
anim.start();
}
void tst_QPropertyAnimation::valueChanged()
{
TestAnimationDriver timeDriver;
//we check that we receive the valueChanged signal
MyErrorObject o;
o.setOle(0);
QCOMPARE(o.property("ole").toInt(), 0);
QPropertyAnimation anim(&o, "ole");
anim.setEndValue(5);
anim.setDuration(200);
QSignalSpy spy(&anim, &QPropertyAnimation::valueChanged);
QVERIFY(spy.isValid());
anim.start();
// Drive animation forward to its end
timeDriver.wait(anim.duration());
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(anim.currentTime(), anim.duration());
//let's check that the values go forward
QCOMPARE(spy.count(), 6); //we should have got everything from 0 to 5
for (int i = 0; i < spy.count(); ++i) {
QCOMPARE(qvariant_cast<QVariant>(spy.at(i).first()).toInt(), i);
}
}
//this class will help us make sure that 2 animations started
//at the same time also end at the same time
class MySyncObject : public MyErrorObject
{
Q_OBJECT
public:
MySyncObject() : anim(this, "ole")
{
anim.setEndValue(1000);
}
public slots:
void checkAnimationFinished()
{
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(ole(), 1000);
}
public:
QPropertyAnimation anim;
};
void tst_QPropertyAnimation::twoAnimations()
{
TestAnimationDriver timeDriver;
MySyncObject o1, o2;
o1.setOle(0);
o2.setOle(0);
//when the animation in o1 is finished
//the animation in o2 should stop around the same time
//We use a queued connection to check just after the tick from the common timer
//the other way is true too
QObject::connect(&o1.anim, SIGNAL(finished()),
&o2, SLOT(checkAnimationFinished()), Qt::QueuedConnection);
QObject::connect(&o2.anim, SIGNAL(finished()),
&o1, SLOT(checkAnimationFinished()), Qt::QueuedConnection);
o1.anim.start();
o2.anim.start();
timeDriver.wait(o1.anim.duration());
QCOMPARE(o1.anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(o2.anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(o1.ole(), 1000);
QCOMPARE(o2.ole(), 1000);
}
class MyComposedAnimation : public QPropertyAnimation
{
Q_OBJECT
public:
MyComposedAnimation(QObject *target, const QByteArray &propertyName, const QByteArray &innerPropertyName)
: QPropertyAnimation(target, propertyName)
{
innerAnim = new QPropertyAnimation(target, innerPropertyName);
this->setEndValue(1000);
innerAnim->setEndValue(1000);
innerAnim->setDuration(duration() + 100);
}
void start()
{
QPropertyAnimation::start();
innerAnim->start();
}
void updateState(QAbstractAnimation::State newState, QAbstractAnimation::State oldState)
{
QPropertyAnimation::updateState(newState, oldState);
if (newState == QAbstractAnimation::Stopped)
delete innerAnim;
}
public:
QPropertyAnimation *innerAnim;
};
void tst_QPropertyAnimation::deletedInUpdateCurrentTime()
{
TestAnimationDriver timeDriver;
// this test case reproduces an animation being deleted in the updateCurrentTime of
// another animation(was causing segfault).
// the deleted animation must have been started after the animation that is deleting.
AnimationObject o;
o.setValue(0);
o.setRealValue(0.0);
MyComposedAnimation composedAnimation(&o, "value", "realValue");
composedAnimation.start();
QCOMPARE(composedAnimation.state(), QAbstractAnimation::Running);
timeDriver.wait(composedAnimation.duration());
QCOMPARE(composedAnimation.state(), QAbstractAnimation::Stopped);
QCOMPARE(o.value(), 1000);
}
void tst_QPropertyAnimation::totalDuration()
{
QPropertyAnimation anim;
QCOMPARE(anim.totalDuration(), 250);
anim.setLoopCount(2);
QCOMPARE(anim.totalDuration(), 2*250);
anim.setLoopCount(-1);
QCOMPARE(anim.totalDuration(), -1);
anim.setDuration(0);
QCOMPARE(anim.totalDuration(), 0);
}
void tst_QPropertyAnimation::zeroLoopCount()
{
DummyPropertyAnimation* anim;
anim = new DummyPropertyAnimation;
anim->setStartValue(0);
anim->setDuration(20);
anim->setLoopCount(0);
QSignalSpy runningSpy(anim, &QPropertyAnimation::stateChanged);
QSignalSpy finishedSpy(anim, &QPropertyAnimation::finished);
QVERIFY(runningSpy.isValid());
QVERIFY(finishedSpy.isValid());
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
QCOMPARE(anim->currentValue().toInt(), 0);
QCOMPARE(runningSpy.count(), 0);
QCOMPARE(finishedSpy.count(), 0);
anim->start();
QCOMPARE(anim->state(), QAnimationGroup::Stopped);
QCOMPARE(anim->currentValue().toInt(), 0);
QCOMPARE(runningSpy.count(), 0);
QCOMPARE(finishedSpy.count(), 0);
}
class RecursiveObject : public QObject
{
Q_OBJECT
Q_PROPERTY(qreal x READ x WRITE setX)
Q_PROPERTY(qreal y READ y WRITE setY)
public:
RecursiveObject() : m_x(0), m_y(0) {
animation.setTargetObject(this);
animation.setPropertyName("y");
animation.setDuration(30);
}
qreal x() const { return m_x; }
void setX(qreal x) {
m_x = x;
animation.setEndValue(x);
animation.start();
}
qreal y() const { return m_y; }
void setY(qreal y) { m_y = y; }
qreal m_x;
qreal m_y;
QPropertyAnimation animation;
};
void tst_QPropertyAnimation::recursiveAnimations()
{
TestAnimationDriver timeDriver;
RecursiveObject o;
QPropertyAnimation anim;
anim.setTargetObject(&o);
anim.setPropertyName("x");
anim.setDuration(30);
anim.setEndValue(4000);
anim.start();
timeDriver.wait(anim.duration() + o.animation.duration());
QCOMPARE(anim.state(), QAbstractAnimation::Stopped);
QCOMPARE(o.animation.state(), QAbstractAnimation::Stopped);
QCOMPARE(o.y(), qreal(4000));
}
QTEST_MAIN(tst_QPropertyAnimation)
#include "tst_qpropertyanimation.moc"