diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp --- a/autotests/integration/effects/scripted_effects_test.cpp +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -74,6 +74,7 @@ void testKeepAlive(); void testRedirect_data(); void testRedirect(); + void testRewind(); private: ScriptedEffect *loadEffect(const QString &name); @@ -566,5 +567,61 @@ } } +void ScriptedEffectsTest::testRewind() +{ + // this verifies that rewind works + + auto effect = new ScriptedEffectWithDebugSpy; + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effectOutputSpy.isValid()); + QVERIFY(effect->load(QStringLiteral("rewindTest"))); + + using namespace KWayland::Client; + Surface *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); + QVERIFY(shellSurface); + ShellClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + auto around = [] (std::chrono::milliseconds elapsed, + std::chrono::milliseconds pivot, + std::chrono::milliseconds margin) { + return qAbs(elapsed.count() - pivot.count()) < margin.count(); + }; + + { + const AnimationEffect::AniMap state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 1); + QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms)); + } + + c->setMinimized(true); + + { + const AnimationEffect::AniMap state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 1); + QVERIFY(around(animations[0].timeLine.elapsed(), 500ms, 50ms)); + } + + c->setMinimized(false); + + { + const AnimationEffect::AniMap state = effect->state(); + QCOMPARE(state.count(), 1); + QCOMPARE(state.firstKey(), c->effectWindow()); + const QList animations = state.first().first; + QCOMPARE(animations.count(), 1); + QVERIFY(around(animations[0].timeLine.elapsed(), 150ms, 50ms)); + } +} + WAYLANDTEST_MAIN(ScriptedEffectsTest) #include "scripted_effects_test.moc" diff --git a/autotests/integration/effects/scripts/rewindTest.js b/autotests/integration/effects/scripts/rewindTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/rewindTest.js @@ -0,0 +1,19 @@ +effects.windowAdded.connect(function (window) { + window.animation = set({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0, + to: 1, + keepAlive: false + }); +}); + +effects.windowMinimized.connect(function (window) { + rewind(window.animation, animationTime(500)); +}); + +effects.windowUnminimized.connect(function (window) { + rewind(window.animation, animationTime(150)); +}); diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -227,6 +227,17 @@ Direction direction, TerminationPolicy terminationPolicy = TerminationPolicy::TerminateAtSource); + /** + * Rewinds the animation to the specified location. + * + * @param animationId The id of the animation. + * @param elapsed The number of elapsed milliseconds where the animation + * has to be rewinded to. + * @returns @c true if the animation was rewinded successfully, otherwise + * @c false. + **/ + bool rewind(quint64 animationId, std::chrono::milliseconds elapsed); + /** * Called whenever an animation end, passes the transformed @class EffectWindow @enum Attribute and originally supplied @param meta * You can reimplement it to keep a constant transformation for the window (ie. keep it a this opacity or position) or to start another animation diff --git a/libkwineffects/kwinanimationeffect.cpp b/libkwineffects/kwinanimationeffect.cpp --- a/libkwineffects/kwinanimationeffect.cpp +++ b/libkwineffects/kwinanimationeffect.cpp @@ -356,6 +356,32 @@ return false; } +bool AnimationEffect::rewind(quint64 animationId, std::chrono::milliseconds elapsed) +{ + Q_D(AnimationEffect); + + if (animationId == d->m_justEndedAnimation) { + return false; + } + + for (auto entryIt = d->m_animations.begin(); entryIt != d->m_animations.end(); ++entryIt) { + auto animIt = std::find_if(entryIt->first.begin(), entryIt->first.end(), + [animationId] (AniData &anim) { + return anim.id == animationId; + } + ); + if (animIt == entryIt->first.end()) { + continue; + } + + animIt->timeLine.setElapsed(elapsed); + + return true; + } + + return false; +} + bool AnimationEffect::cancel(quint64 animationId) { Q_D(AnimationEffect); diff --git a/scripting/scriptedeffect.h b/scripting/scriptedeffect.h --- a/scripting/scriptedeffect.h +++ b/scripting/scriptedeffect.h @@ -106,6 +106,7 @@ quint64 set(KWin::EffectWindow *w, Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from = KWin::FPx2(), uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0, bool fullScreen = false, bool keepAlive = true); bool retarget(quint64 animationId, KWin::FPx2 newTarget, int newRemainingTime = -1); bool redirect(quint64 animationId, Direction direction, TerminationPolicy terminationPolicy = TerminationPolicy::TerminateAtSource); + bool rewind(quint64 animationId, std::chrono::milliseconds elapsed); bool cancel(quint64 animationId) { return AnimationEffect::cancel(animationId); } virtual bool borderActivated(ElectricBorder border); diff --git a/scripting/scriptedeffect.cpp b/scripting/scriptedeffect.cpp --- a/scripting/scriptedeffect.cpp +++ b/scripting/scriptedeffect.cpp @@ -501,6 +501,40 @@ return QScriptValue(true); } +QScriptValue kwinEffectRewind(QScriptContext *context, QScriptEngine *engine) +{ + if (context->argumentCount() != 2) { + const QString errorMessage = QStringLiteral("rewind() takes exactly 2 arguments (%1 given)") + .arg(context->argumentCount()); + context->throwError(QScriptContext::SyntaxError, errorMessage); + return engine->undefinedValue(); + } + + bool ok = false; + QList animationIds = animations(context->argument(0).toVariant(), &ok); + if (!ok) { + context->throwError(QScriptContext::TypeError, QStringLiteral("Argument needs to be one or several quint64")); + return engine->undefinedValue(); + } + + const QScriptValue wrappedElapsed = context->argument(1); + if (!wrappedElapsed.isNumber()) { + context->throwError(QScriptContext::TypeError, QStringLiteral("The number of elapsed milliseconds has invalid type")); + return engine->undefinedValue(); + } + + const std::chrono::milliseconds elapsed(wrappedElapsed.toUInt32()); + + ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); + for (const quint64 &animationId : qAsConst(animationIds)) { + if (!effect->rewind(animationId, elapsed)) { + return QScriptValue(false); + } + } + + return QScriptValue(true); +} + QScriptValue kwinEffectCancel(QScriptContext *context, QScriptEngine *engine) { ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); @@ -660,6 +694,11 @@ redirectFunc.setData(m_engine->newQObject(this)); m_engine->globalObject().setProperty(QStringLiteral("redirect"), redirectFunc); + // rewind + QScriptValue rewindFunc = m_engine->newFunction(kwinEffectRewind); + rewindFunc.setData(m_engine->newQObject(this)); + m_engine->globalObject().setProperty(QStringLiteral("rewind"), rewindFunc); + // cancel... QScriptValue cancelFunc = m_engine->newFunction(kwinEffectCancel); cancelFunc.setData(m_engine->newQObject(this)); @@ -730,6 +769,11 @@ return AnimationEffect::redirect(animationId, direction, terminationPolicy); } +bool ScriptedEffect::rewind(quint64 animationId, std::chrono::milliseconds elapsed) +{ + return AnimationEffect::rewind(animationId, elapsed); +} + bool ScriptedEffect::isGrabbed(EffectWindow* w, ScriptedEffect::DataRole grabRole) { void *e = w->data(static_cast(grabRole)).value();