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 @@ -78,6 +78,7 @@ void testUngrab(); void testRedirect_data(); void testRedirect(); + void testComplete(); private: ScriptedEffect *loadEffect(const QString &name); @@ -714,5 +715,74 @@ } } +void ScriptedEffectsTest::testComplete() +{ + // this test verifies that complete works + + // load the test effect + auto effect = new ScriptedEffectWithDebugSpy; + QVERIFY(effect->load(QStringLiteral("completeTest"))); + + // create test client + 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(); + }; + + // initially, the test animation should be at the start position + { + 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)); + QVERIFY(!animations[0].timeLine.done()); + } + + // wait for 250ms + QTest::qWait(250); + + { + 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(), 250ms, 50ms)); + QVERIFY(!animations[0].timeLine.done()); + } + + // minimize the test client, when the test effect sees that a window was + // minimized, it will try to complete animation for it + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effectOutputSpy.isValid()); + + c->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + + { + 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); + QCOMPARE(animations[0].timeLine.elapsed(), 1000ms); + QVERIFY(animations[0].timeLine.done()); + } +} + WAYLANDTEST_MAIN(ScriptedEffectsTest) #include "scripted_effects_test.moc" diff --git a/autotests/integration/effects/scripts/completeTest.js b/autotests/integration/effects/scripts/completeTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/completeTest.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) { + if (complete(window.animation)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } +}); diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -236,6 +236,15 @@ Direction direction, TerminationFlags terminationFlags = TerminateAtSource); + /** + * Fast-forwards the animation to the target position. + * + * @param animationId The id of the animation. + * @returns @c true if the animation was fast-forwarded successfully, otherwise + * @c false. + **/ + bool complete(quint64 animationId); + /** * 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::complete(quint64 animationId) +{ + 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(animIt->timeLine.duration()); + + 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 @@ -129,6 +129,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, TerminationFlags terminationFlags = TerminateAtSource); + bool complete(quint64 animationId); 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 @@ -492,6 +492,32 @@ return QScriptValue(true); } +QScriptValue kwinEffectComplete(QScriptContext *context, QScriptEngine *engine) +{ + if (context->argumentCount() != 1) { + const QString errorMessage = QStringLiteral("complete() takes exactly 1 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(); + } + + ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); + for (const quint64 &animationId : qAsConst(animationIds)) { + if (!effect->complete(animationId)) { + return QScriptValue(false); + } + } + + return QScriptValue(true); +} + QScriptValue kwinEffectCancel(QScriptContext *context, QScriptEngine *engine) { ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); @@ -651,6 +677,11 @@ redirectFunc.setData(m_engine->newQObject(this)); m_engine->globalObject().setProperty(QStringLiteral("redirect"), redirectFunc); + // complete + QScriptValue completeFunc = m_engine->newFunction(kwinEffectComplete); + completeFunc.setData(m_engine->newQObject(this)); + m_engine->globalObject().setProperty(QStringLiteral("complete"), completeFunc); + // cancel... QScriptValue cancelFunc = m_engine->newFunction(kwinEffectCancel); cancelFunc.setData(m_engine->newQObject(this)); @@ -721,6 +752,11 @@ return AnimationEffect::redirect(animationId, direction, terminationFlags); } +bool ScriptedEffect::complete(quint64 animationId) +{ + return AnimationEffect::complete(animationId); +} + bool ScriptedEffect::isGrabbed(EffectWindow* w, ScriptedEffect::DataRole grabRole) { void *e = w->data(static_cast(grabRole)).value();