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 @@ -24,6 +24,7 @@ #include "composite.h" #include "cursor.h" #include "cursor.h" +#include "deleted.h" #include "effect_builtins.h" #include "effectloader.h" #include "effects.h" @@ -66,6 +67,9 @@ void testAnimations(); void testScreenEdge(); void testScreenEdgeTouch(); + void testKeepAlive_data(); + void testKeepAlive(); + private: ScriptedEffect *loadEffect(const QString &name); }; @@ -131,6 +135,7 @@ qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); @@ -350,5 +355,62 @@ QCOMPARE(effectOutputSpy.count(), 1); } +void ScriptedEffectsTest::testKeepAlive_data() +{ + QTest::addColumn("file"); + QTest::addColumn("keepAlive"); + + QTest::newRow("keep") << "keepAliveTest" << true; + QTest::newRow("don't keep") << "keepAliveTestDontKeep" << false; +} + +void ScriptedEffectsTest::testKeepAlive() +{ + // this test checks whether closed windows are kept alive + // when keepAlive property is set to true(false) + + QFETCH(QString, file); + QFETCH(bool, keepAlive); + + auto *effect = new ScriptedEffectWithDebugSpy; + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effectOutputSpy.isValid()); + QVERIFY(effect->load(file)); + + // create a window + using namespace KWayland::Client; + auto *surface = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface); + auto *shellSurface = Test::createXdgShellV6Surface(surface, surface); + QVERIFY(shellSurface); + auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // no active animations at the beginning + QCOMPARE(effect->state().count(), 0); + + // trigger windowClosed signal + surface->deleteLater(); + QVERIFY(effectOutputSpy.count() == 1 || effectOutputSpy.wait()); + + if (keepAlive) { + QCOMPARE(effect->state().count(), 1); + + QTest::qWait(500); + QCOMPARE(effect->state().count(), 1); + + QTest::qWait(500 + 100); // 100ms is extra safety margin + QCOMPARE(effect->state().count(), 0); + } else { + // the test effect doesn't keep the window alive, so it should be + // removed immediately + QSignalSpy deletedRemovedSpy(workspace(), &Workspace::deletedRemoved); + QVERIFY(deletedRemovedSpy.isValid()); + QVERIFY(deletedRemovedSpy.count() == 1 || deletedRemovedSpy.wait(100)); // 100ms is less than duration of the animation + QCOMPARE(effect->state().count(), 0); + } +} + WAYLANDTEST_MAIN(ScriptedEffectsTest) #include "scripted_effects_test.moc" diff --git a/autotests/integration/effects/scripts/keepAliveTest.js b/autotests/integration/effects/scripts/keepAliveTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/keepAliveTest.js @@ -0,0 +1,13 @@ +"use strict"; + +effects.windowClosed.connect(function (window) { + animate({ + window: window, + type: Effect.Scale, + duration: 1000, + from: 1.0, + to: 0.0 + // by default, keepAlive is set to true + }); + sendTestResponse("triggered"); +}); diff --git a/autotests/integration/effects/scripts/keepAliveTestDontKeep.js b/autotests/integration/effects/scripts/keepAliveTestDontKeep.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/keepAliveTestDontKeep.js @@ -0,0 +1,13 @@ +"use strict"; + +effects.windowClosed.connect(function (window) { + animate({ + window: window, + type: Effect.Scale, + duration: 1000, + from: 1.0, + to: 0.0, + keepAlive: false + }); + sendTestResponse("triggered"); +}); diff --git a/effects/dialogparent/package/contents/code/main.js b/effects/dialogparent/package/contents/code/main.js --- a/effects/dialogparent/package/contents/code/main.js +++ b/effects/dialogparent/package/contents/code/main.js @@ -79,6 +79,7 @@ animate({ window: w, duration: dialogParentEffect.duration, + keepAlive: false, animations: [{ type: Effect.Saturation, from: 0.4, diff --git a/libkwineffects/anidata.cpp b/libkwineffects/anidata.cpp --- a/libkwineffects/anidata.cpp +++ b/libkwineffects/anidata.cpp @@ -41,11 +41,13 @@ , windowType((NET::WindowTypeMask)0) , waitAtSource(false) , keepAtTarget(false) + , keepAlive(true) { } AniData::AniData(AnimationEffect::Attribute a, int meta_, int ms, const FPx2 &to_, - QEasingCurve curve_, int delay, const FPx2 &from_, bool waitAtSource_, bool keepAtTarget_ ) + QEasingCurve curve_, int delay, const FPx2 &from_, bool waitAtSource_, + bool keepAtTarget_, bool keepAlive_ ) : attribute(a) , curve(curve_) , from(from_) @@ -57,6 +59,7 @@ , windowType((NET::WindowTypeMask)0) , waitAtSource(waitAtSource_) , keepAtTarget(keepAtTarget_) + , keepAlive(keepAlive_) { } diff --git a/libkwineffects/anidata_p.h b/libkwineffects/anidata_p.h --- a/libkwineffects/anidata_p.h +++ b/libkwineffects/anidata_p.h @@ -31,7 +31,8 @@ public: AniData(); AniData(AnimationEffect::Attribute a, int meta, int ms, const FPx2 &to, - QEasingCurve curve, int delay, const FPx2 &from, bool waitAtSource, bool keepAtTarget = false); + QEasingCurve curve, int delay, const FPx2 &from, bool waitAtSource, + bool keepAtTarget = false, bool keepAlive = true); explicit AniData(const QString &str); inline void addTime(int t) { time += t; } inline bool isOneDimensional() const { @@ -51,6 +52,7 @@ qint64 startTime; NET::WindowTypeMask windowType; bool waitAtSource, keepAtTarget; + bool keepAlive; }; } // namespace diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -161,18 +161,19 @@ * @param shape - How the animation progresses, eg. Linear progresses constantly while Exponential start slow and becomes very fast in the end * @param delay - When the animation will start compared to "now" (the window will remain at the "from" position until then) * @param from - the starting value, the default is invalid, ie. the attribute for the window is not transformed in the beginning + * @param keepAlive - Whether closed windows should be kept alive during animation * @return an ID that you can use to cancel a running animation */ - quint64 animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2() ) - { return p_animate(w, a, meta, ms, to, curve, delay, from, false); } + quint64 animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2(), bool keepAlive = true ) + { return p_animate(w, a, meta, ms, to, curve, delay, from, false, keepAlive); } /** * Equal to ::animate() with one important difference: * The target value for the attribute is kept until you ::cancel() this animation * @return an ID that you need to use to cancel this manipulation */ - quint64 set( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2() ) - { return p_animate(w, a, meta, ms, to, curve, delay, from, true); } + quint64 set( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2(), bool keepAlive = true ) + { return p_animate(w, a, meta, ms, to, curve, delay, from, true, keepAlive); } /** * this allows to alter the target (but not type or curve) of a running animation @@ -211,7 +212,7 @@ AniMap state() const; private: - quint64 p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget ); + quint64 p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget, bool keepAlive ); QRect clipRect(const QRect &windowRect, const AniData&) const; void clipWindow(const EffectWindow *, const AniData &, WindowQuadList &) const; float interpolated( const AniData&, int i = 0 ) const; diff --git a/libkwineffects/kwinanimationeffect.cpp b/libkwineffects/kwinanimationeffect.cpp --- a/libkwineffects/kwinanimationeffect.cpp +++ b/libkwineffects/kwinanimationeffect.cpp @@ -26,6 +26,8 @@ #include #include +#include + QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2) { dbg.nospace() << fpx2[0] << "," << fpx2[1] << QString(fpx2.isValid() ? QStringLiteral(" (valid)") : QStringLiteral(" (invalid)")); @@ -216,7 +218,7 @@ } } -quint64 AnimationEffect::p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget ) +quint64 AnimationEffect::p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget, bool keepAlive ) { const bool waitAtSource = from.isValid(); validate(a, meta, &from, &to, w); @@ -237,7 +239,7 @@ AniMap::iterator it = d->m_animations.find(w); if (it == d->m_animations.end()) it = d->m_animations.insert(w, QPair, QRect>(QList(), QRect())); - it->first.append(AniData(a, meta, ms, to, curve, delay, from, waitAtSource, keepAtTarget)); + it->first.append(AniData(a, meta, ms, to, curve, delay, from, waitAtSource, keepAtTarget, keepAlive)); quint64 ret_id = ++d->m_animCounter; it->first.last().id = ret_id; it->second = QRect(); @@ -913,7 +915,19 @@ void AnimationEffect::_windowClosed( EffectWindow* w ) { Q_D(AnimationEffect); - if (d->m_animations.contains(w) && !d->m_zombies.contains(w)) { + + auto it = d->m_animations.constFind(w); + if (it == d->m_animations.constEnd()) { + return; + } + + const auto animations = (*it).first; + const bool keepAlive = std::any_of(animations.constBegin(), animations.constEnd(), + [](const AniData &anim) { + return anim.keepAlive; + }); + + if (keepAlive && !d->m_zombies.contains(w)) { w->refWindow(); d->m_zombies << w; } diff --git a/scripting/scriptedeffect.h b/scripting/scriptedeffect.h --- a/scripting/scriptedeffect.h +++ b/scripting/scriptedeffect.h @@ -96,8 +96,8 @@ public Q_SLOTS: //curve should be of type QEasingCurve::type or ScriptedEffect::EasingCurve - 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); - 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); + 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 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 keepAlive = true); bool retarget(quint64 animationId, KWin::FPx2 newTarget, int newRemainingTime = -1); 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 @@ -103,15 +103,16 @@ } struct AnimationSettings { - enum { Type = 1<<0, Curve = 1<<1, Delay = 1<<2, Duration = 1<<3 }; + enum { Type = 1<<0, Curve = 1<<1, Delay = 1<<2, Duration = 1<<3, KeepAlive = 1<<4 }; AnimationEffect::Attribute type; QEasingCurve::Type curve; FPx2 from; FPx2 to; int delay; uint duration; uint set; uint metaData; + bool keepAlive; }; AnimationSettings animationSettingsFromObject(QScriptValue &object) @@ -155,6 +156,14 @@ settings.type = static_cast(-1); } + QScriptValue keepAlive = object.property(QStringLiteral("keepAlive")); + if (keepAlive.isValid() && keepAlive.isBool()) { + settings.keepAlive = keepAlive.toBool(); + settings.set |= AnimationSettings::KeepAlive; + } else { + settings.keepAlive = true; + } + return settings; } @@ -218,6 +227,9 @@ if (!(s.set & AnimationSettings::Delay)) { s.delay = settings.at(0).delay; } + if (!(s.set & AnimationSettings::KeepAlive)) { + s.keepAlive = settings.at(0).keepAlive; + } s.metaData = 0; typedef QMap MetaTypeMap; @@ -285,7 +297,8 @@ setting.from, setting.metaData, setting.curve, - setting.delay)); + setting.delay, + setting.keepAlive)); ++i; } return array; @@ -315,7 +328,8 @@ setting.from, setting.metaData, setting.curve, - setting.delay)); + setting.delay, + setting.keepAlive)); } return engine->newVariant(animIds); @@ -581,24 +595,24 @@ } } -quint64 ScriptedEffect::animate(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from, uint metaData, int curve, int delay) +quint64 ScriptedEffect::animate(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from, uint metaData, int curve, int delay, bool keepAlive) { QEasingCurve qec; if (curve < QEasingCurve::Custom) qec.setType(static_cast(curve)); else if (curve == GaussianCurve) qec.setCustomType(qecGaussian); - return AnimationEffect::animate(w, a, metaData, ms, to, qec, delay, from); + return AnimationEffect::animate(w, a, metaData, ms, to, qec, delay, from, keepAlive); } -quint64 ScriptedEffect::set(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from, uint metaData, int curve, int delay) +quint64 ScriptedEffect::set(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from, uint metaData, int curve, int delay, bool keepAlive) { QEasingCurve qec; if (curve < QEasingCurve::Custom) qec.setType(static_cast(curve)); else if (curve == GaussianCurve) qec.setCustomType(qecGaussian); - return AnimationEffect::set(w, a, metaData, ms, to, qec, delay, from); + return AnimationEffect::set(w, a, metaData, ms, to, qec, delay, from, keepAlive); } bool ScriptedEffect::retarget(quint64 animationId, KWin::FPx2 newTarget, int newRemainingTime)