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 @@ -76,6 +76,8 @@ void testGrabAlreadyGrabbedWindow(); void testGrabAlreadyGrabbedWindowForced(); void testUngrab(); + void testRedirect_data(); + void testRedirect(); private: ScriptedEffect *loadEffect(const QString &name); @@ -301,13 +303,15 @@ QCOMPARE(animationsForWindow[0].to, FPx2(1.4)); QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); QCOMPARE(animationsForWindow[0].timeLine.easingCurve().type(), QEasingCurve::OutQuad); - QCOMPARE(animationsForWindow[0].keepAtTarget, false); + QCOMPARE(animationsForWindow[0].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); if (animationCount == 2) { QCOMPARE(animationsForWindow[1].timeLine.duration(), 100ms); QCOMPARE(animationsForWindow[1].to, FPx2(0.0)); QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); - QCOMPARE(animationsForWindow[1].keepAtTarget, false); + QCOMPARE(animationsForWindow[1].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); } } QCOMPARE(effectOutputSpy[0].first(), "true"); @@ -323,12 +327,14 @@ QCOMPARE(animationsForWindow[0].timeLine.duration(), 200ms); QCOMPARE(animationsForWindow[0].to, FPx2(1.5)); QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); - QCOMPARE(animationsForWindow[0].keepAtTarget, false); + QCOMPARE(animationsForWindow[0].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); if (animationCount == 2) { QCOMPARE(animationsForWindow[1].timeLine.duration(), 200ms); QCOMPARE(animationsForWindow[1].to, FPx2(1.5)); QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); - QCOMPARE(animationsForWindow[1].keepAtTarget, false); + QCOMPARE(animationsForWindow[1].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); } } c->setMinimized(false); @@ -619,5 +625,94 @@ QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value(), nullptr); } +void ScriptedEffectsTest::testRedirect_data() +{ + QTest::addColumn("file"); + QTest::addColumn("shouldTerminate"); + QTest::newRow("animate/DontTerminateAtSource") << "redirectAnimateDontTerminateTest" << false; + QTest::newRow("animate/TerminateAtSource") << "redirectAnimateTerminateTest" << true; + QTest::newRow("set/DontTerminate") << "redirectSetDontTerminateTest" << false; + QTest::newRow("set/Terminate") << "redirectSetTerminateTest" << true; +} + +void ScriptedEffectsTest::testRedirect() +{ + // this test verifies that redirect() works + + // load the test effect + auto effect = new ScriptedEffectWithDebugSpy; + QFETCH(QString, file); + QVERIFY(effect->load(file)); + + // 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 is at the source 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); + QCOMPARE(animations[0].timeLine.direction(), TimeLine::Forward); + QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms)); + } + + // minimize the test client after 250ms, when the test effect sees that + // a window was minimized, it will try to reverse animation for it + QTest::qWait(250); + + 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.direction(), TimeLine::Backward); + QVERIFY(around(animations[0].timeLine.elapsed(), 1000ms - 250ms, 50ms)); + } + + // wait for the animation to reach the start position, 100ms is an extra + // safety margin + QTest::qWait(250 + 100); + + QFETCH(bool, shouldTerminate); + if (shouldTerminate) { + const AnimationEffect::AniMap state = effect->state(); + QCOMPARE(state.count(), 0); + } else { + 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.direction(), TimeLine::Backward); + QCOMPARE(animations[0].timeLine.elapsed(), 1000ms); + QCOMPARE(animations[0].timeLine.value(), 0.0); + } +} + WAYLANDTEST_MAIN(ScriptedEffectsTest) #include "scripted_effects_test.moc" diff --git a/autotests/integration/effects/scripts/redirectAnimateDontTerminateTest.js b/autotests/integration/effects/scripts/redirectAnimateDontTerminateTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/redirectAnimateDontTerminateTest.js @@ -0,0 +1,18 @@ +effects.windowAdded.connect(function (window) { + window.animation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }) +}); + +effects.windowMinimized.connect(function (window) { + if (redirect(window.animation, Effect.Backward, Effect.DontTerminate)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } +}); diff --git a/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js b/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js @@ -0,0 +1,18 @@ +effects.windowAdded.connect(function (window) { + window.animation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }) +}); + +effects.windowMinimized.connect(function (window) { + if (redirect(window.animation, Effect.Backward, Effect.TerminateAtSource)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } +}); diff --git a/autotests/integration/effects/scripts/redirectSetDontTerminateTest.js b/autotests/integration/effects/scripts/redirectSetDontTerminateTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/redirectSetDontTerminateTest.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.0, + to: 1.0, + keepAlive: false + }); +}); + +effects.windowMinimized.connect(function (window) { + if (redirect(window.animation, Effect.Backward, Effect.DontTerminate)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } +}); diff --git a/autotests/integration/effects/scripts/redirectSetTerminateTest.js b/autotests/integration/effects/scripts/redirectSetTerminateTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/redirectSetTerminateTest.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.0, + to: 1.0, + keepAlive: false + }); +}); + +effects.windowMinimized.connect(function (window) { + if (redirect(window.animation, Effect.Backward, Effect.TerminateAtSource)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } +}); diff --git a/libkwineffects/anidata.cpp b/libkwineffects/anidata.cpp --- a/libkwineffects/anidata.cpp +++ b/libkwineffects/anidata.cpp @@ -73,13 +73,12 @@ , meta(0) , startTime(0) , waitAtSource(false) - , keepAtTarget(false) , keepAlive(true) { } AniData::AniData(AnimationEffect::Attribute a, int meta_, const FPx2 &to_, - int delay, const FPx2 &from_, bool waitAtSource_, bool keepAtTarget_, + int delay, const FPx2 &from_, bool waitAtSource_, FullScreenEffectLockPtr fullScreenEffectLock_, bool keepAlive, PreviousWindowPixmapLockPtr previousWindowPixmapLock_) : attribute(a) @@ -89,12 +88,24 @@ , startTime(AnimationEffect::clock() + delay) , fullScreenEffectLock(fullScreenEffectLock_) , waitAtSource(waitAtSource_) - , keepAtTarget(keepAtTarget_) , keepAlive(keepAlive) , previousWindowPixmapLock(previousWindowPixmapLock_) { } +bool AniData::isActive() const +{ + if (!timeLine.done()) { + return true; + } + + if (timeLine.direction() == TimeLine::Backward) { + return !(terminationFlags & AnimationEffect::TerminateAtSource); + } + + return !(terminationFlags & AnimationEffect::TerminateAtTarget); +} + static QString attributeString(KWin::AnimationEffect::Attribute attribute) { switch (attribute) { diff --git a/libkwineffects/anidata_p.h b/libkwineffects/anidata_p.h --- a/libkwineffects/anidata_p.h +++ b/libkwineffects/anidata_p.h @@ -76,9 +76,11 @@ AniData(); AniData(AnimationEffect::Attribute a, int meta, const FPx2 &to, int delay, const FPx2 &from, bool waitAtSource, - bool keepAtTarget = false, FullScreenEffectLockPtr=FullScreenEffectLockPtr(), + FullScreenEffectLockPtr=FullScreenEffectLockPtr(), bool keepAlive = true, PreviousWindowPixmapLockPtr previousWindowPixmapLock = {}); + bool isActive() const; + inline bool isOneDimensional() const { return from[0] == from[1] && to[0] == to[1]; } @@ -92,10 +94,11 @@ uint meta; qint64 startTime; QSharedPointer fullScreenEffectLock; - bool waitAtSource, keepAtTarget; + bool waitAtSource; bool keepAlive; KeepAliveLockPtr keepAliveLock; PreviousWindowPixmapLockPtr previousWindowPixmapLock; + AnimationEffect::TerminationFlags terminationFlags; }; } // namespace diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -3,6 +3,7 @@ This file is part of the KDE project. Copyright (C) 2011 Thomas Lübking +Copyright (C) 2018 Vlad Zagorodniy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -108,6 +109,37 @@ }; enum MetaType { SourceAnchor, TargetAnchor, RelativeSourceX, RelativeSourceY, RelativeTargetX, RelativeTargetY, Axis }; + + /** + * This enum type is used to specify the direction of the animation. + **/ + enum Direction { + Forward, ///< The animation goes from source to target. + Backward ///< The animation goes from target to source. + }; + Q_ENUM(Direction) + + /** + * This enum type is used to specify when the animation should be terminated. + * + * @value DontTerminate Don't terminate the animation when it reaches the source + * or the target position. + * + * @value TerminateAtSource Terminate the animation when it reaches the source + * position. An animation can reach the source position if its direction was + * changed to go backward (from target to source). + * + * @value TerminateAtTarget Terminate the animation when it reaches the target + * position. If this flag is not set, then the animation will be persistent. + **/ + enum TerminationFlag { + DontTerminate = 0x00, + TerminateAtSource = 0x01, + TerminateAtTarget = 0x02 + }; + Q_FLAGS(TerminationFlag) + Q_DECLARE_FLAGS(TerminationFlags, TerminationFlag) + /** * Whenever you intend to connect to the EffectsHandler::windowClosed() signal, do so when reimplementing the constructor. * Do *not* add private slots named _windowClosed( EffectWindow* w ) or _windowDeleted( EffectWindow* w ) !! @@ -189,6 +221,21 @@ */ bool retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime = -1); + /** + * Changes the direction of the animation. + * + * @param animationId The id of the animation. + * @param direction The new direction of the animation. + * @param terminationPolicy Whether the animation should be terminated when it + * reaches the source position after its direction was changed to go backward. + * Currently, TerminationFlag::TerminateAtTarget has no effect. + * @returns @c true if the direction of the animation was changed successfully, + * otherwise @c false. + **/ + bool redirect(quint64 animationId, + Direction direction, + TerminationFlags terminationFlags = TerminateAtSource); + /** * 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 @@ -236,6 +283,8 @@ } // namespace QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2); + Q_DECLARE_METATYPE(KWin::FPx2) +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::AnimationEffect::TerminationFlags) #endif // ANIMATION_EFFECT_H diff --git a/libkwineffects/kwinanimationeffect.cpp b/libkwineffects/kwinanimationeffect.cpp --- a/libkwineffects/kwinanimationeffect.cpp +++ b/libkwineffects/kwinanimationeffect.cpp @@ -3,6 +3,7 @@ This file is part of the KDE project. Copyright (C) 2011 Thomas Lübking +Copyright (C) 2018 Vlad Zagorodniy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -258,7 +259,6 @@ delay, // Delay from, // Source waitAtSource, // Whether the animation should be kept at source - keepAtTarget, // Whether the animation is persistent fullscreen, // Full screen effect lock keepAlive, // Keep alive flag previousPixmap // Previous window pixmap lock @@ -274,6 +274,11 @@ animation.timeLine.setSourceRedirectMode(TimeLine::RedirectMode::Strict); animation.timeLine.setTargetRedirectMode(TimeLine::RedirectMode::Relaxed); + animation.terminationFlags = TerminateAtSource; + if (!keepAtTarget) { + animation.terminationFlags |= TerminateAtTarget; + } + it->second = QRect(); d->m_animationsTouched = true; @@ -315,6 +320,42 @@ return false; // no animation found } +bool AnimationEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) +{ + 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; + } + + switch (direction) { + case Backward: + animIt->timeLine.setDirection(TimeLine::Backward); + break; + + case Forward: + animIt->timeLine.setDirection(TimeLine::Forward); + break; + } + + animIt->terminationFlags = terminationFlags & ~TerminateAtTarget; + + return true; + } + + return false; +} + bool AnimationEffect::cancel(quint64 animationId) { Q_D(AnimationEffect); @@ -364,7 +405,7 @@ anim->timeLine.update(std::chrono::milliseconds(time)); } - if (!anim->timeLine.done() || anim->keepAtTarget) { + if (anim->isActive()) { // if (anim->attribute != Brightness && anim->attribute != Saturation && anim->attribute != Opacity) // transformed = true; d->m_animated = true; diff --git a/scripting/scriptedeffect.h b/scripting/scriptedeffect.h --- a/scripting/scriptedeffect.h +++ b/scripting/scriptedeffect.h @@ -128,6 +128,7 @@ quint64 animate(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); 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 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 @@ -438,6 +438,60 @@ return QScriptValue(ok); } +QScriptValue kwinEffectRedirect(QScriptContext *context, QScriptEngine *engine) +{ + if (context->argumentCount() != 2 && context->argumentCount() != 3) { + const QString errorMessage = QStringLiteral("redirect() takes either 2 or 3 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 wrappedDirection = context->argument(1); + if (!wrappedDirection.isNumber()) { + context->throwError(QScriptContext::TypeError, QStringLiteral("Direction has invalid type")); + return engine->undefinedValue(); + } + + const auto direction = static_cast(wrappedDirection.toInt32()); + switch (direction) { + case AnimationEffect::Forward: + case AnimationEffect::Backward: + break; + + default: + context->throwError(QScriptContext::SyntaxError, QStringLiteral("Unknown direction")); + return engine->undefinedValue(); + } + + AnimationEffect::TerminationFlags terminationFlags = AnimationEffect::TerminateAtSource; + if (context->argumentCount() >= 3) { + const QScriptValue wrappedTerminationFlags = context->argument(2); + if (!wrappedTerminationFlags.isNumber()) { + context->throwError(QScriptContext::TypeError, QStringLiteral("Termination flags argument has invalid type")); + return engine->undefinedValue(); + } + + terminationFlags = static_cast(wrappedTerminationFlags.toInt32()); + } + + ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); + for (const quint64 &animationId : qAsConst(animationIds)) { + if (!effect->redirect(animationId, direction, terminationFlags)) { + return QScriptValue(false); + } + } + + return QScriptValue(true); +} + QScriptValue kwinEffectCancel(QScriptContext *context, QScriptEngine *engine) { ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); @@ -592,6 +646,11 @@ retargetFunc.setData(m_engine->newQObject(this)); m_engine->globalObject().setProperty(QStringLiteral("retarget"), retargetFunc); + // redirect + QScriptValue redirectFunc = m_engine->newFunction(kwinEffectRedirect); + redirectFunc.setData(m_engine->newQObject(this)); + m_engine->globalObject().setProperty(QStringLiteral("redirect"), redirectFunc); + // cancel... QScriptValue cancelFunc = m_engine->newFunction(kwinEffectCancel); cancelFunc.setData(m_engine->newQObject(this)); @@ -657,6 +716,11 @@ return AnimationEffect::retarget(animationId, newTarget, newRemainingTime); } +bool ScriptedEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) +{ + return AnimationEffect::redirect(animationId, direction, terminationFlags); +} + bool ScriptedEffect::isGrabbed(EffectWindow* w, ScriptedEffect::DataRole grabRole) { void *e = w->data(static_cast(grabRole)).value();