From 396d5280756a91a624218f6ae56709c72c1c6bc0 Mon Sep 17 00:00:00 2001 From: Vlad Zagorodniy Date: Thu, 25 Oct 2018 21:02:36 +0300 Subject: [PATCH] [scripting] Introduce complete function Summary: Effects that prefer to manipulate direction of animations sometimes need to create animations in some particular state so later on they can be played backward (swapping from and to is not enough and it would be wrong). The proposed complete function lets such effects to fast-forward animations to to the target position so they can be played backwards later on. Reviewers: #kwin, davidedmundson Reviewed By: #kwin, davidedmundson Subscribers: davidedmundson, kwin Tags: #kwin Differential Revision: https://phabricator.kde.org/D16450 --- .../effects/scripted_effects_test.cpp | 70 +++++++++++++++++++ .../effects/scripts/completeTest.js | 19 +++++ libkwineffects/kwinanimationeffect.cpp | 26 +++++++ libkwineffects/kwinanimationeffect.h | 9 +++ scripting/scriptedeffect.cpp | 36 ++++++++++ scripting/scriptedeffect.h | 1 + 6 files changed, 161 insertions(+) create mode 100644 autotests/integration/effects/scripts/completeTest.js diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp index 023c479a46..919ba1f837 100644 --- a/autotests/integration/effects/scripted_effects_test.cpp +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -78,6 +78,7 @@ private Q_SLOTS: void testUngrab(); void testRedirect_data(); void testRedirect(); + void testComplete(); private: ScriptedEffect *loadEffect(const QString &name); @@ -714,5 +715,74 @@ void ScriptedEffectsTest::testRedirect() } } +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 index 0000000000..af9a70c39a --- /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.cpp b/libkwineffects/kwinanimationeffect.cpp index 4e99886826..b370486f68 100644 --- a/libkwineffects/kwinanimationeffect.cpp +++ b/libkwineffects/kwinanimationeffect.cpp @@ -356,6 +356,32 @@ bool AnimationEffect::redirect(quint64 animationId, Direction direction, Termina 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/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h index 986e19e29e..dcb21576f6 100644 --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -236,6 +236,15 @@ protected: 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/scripting/scriptedeffect.cpp b/scripting/scriptedeffect.cpp index 74046fdd88..3d6af9298a 100644 --- a/scripting/scriptedeffect.cpp +++ b/scripting/scriptedeffect.cpp @@ -492,6 +492,32 @@ QScriptValue kwinEffectRedirect(QScriptContext *context, QScriptEngine *engine) 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 @@ bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript 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 @@ bool ScriptedEffect::redirect(quint64 animationId, Direction direction, Terminat 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(); diff --git a/scripting/scriptedeffect.h b/scripting/scriptedeffect.h index a487ef198f..956f3ada8f 100644 --- a/scripting/scriptedeffect.h +++ b/scripting/scriptedeffect.h @@ -129,6 +129,7 @@ public Q_SLOTS: 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);