[scripting] Introduce redirect function

Summary:
Consider current implementation of the Squash effect: if a window was
minimized, an animation will be started; if the window is unminimized
and the animation is still active (that can happen when user clicks on
app's icon really fast), the animation will be stopped and a new one will
be created. Such behavior can lead to rapid jumps in the observed
"animation".

A better approach would be first try to **reverse** the already active
animation, and if that attempt wasn't successful, start a new animation.

This patch introduces a new function to the scripted effects API that
lets JavaScript effects to control direction of animations. The
prototype of the function looks as follows:

    redirect(<animation id(s)>, <direction>, [<termination policy>])

the first argument is an animation id or a list of animation ids, the
second argument specifies the new direction of the animation or
animations if a list of ids was passed as the first argument. The
third argument specifies whether the animation(s) should be terminated
when it(they) reaches the source position, currently it's relevant only
for animations that are created with set() function. The termination
policy argument is optional, by default it's Effect.TerminateAtSource.

We can use this function to fix issues with rapid jumps in the Squash
effect. Also, redirect() lets us to write effects for simple animations
in slightly different style: first, we have to start the main animation
(e.g. for the Dialog Parent effect, it would be dimming of main windows)
and then change direction of the animation depending on external events,
e.g. when the Desktop Cube effect is activated.

Reviewers: #kwin, davidedmundson

Reviewed By: #kwin, davidedmundson

Subscribers: davidedmundson, abetts, kwin

Tags: #kwin

Differential Revision: https://phabricator.kde.org/D16449
icc-effect-5.17.5
Vlad Zagorodniy 2018-10-24 22:58:29 +03:00
parent ee72569647
commit 5e104fbc12
11 changed files with 349 additions and 11 deletions

View File

@ -76,6 +76,8 @@ private Q_SLOTS:
void testGrabAlreadyGrabbedWindow();
void testGrabAlreadyGrabbedWindowForced();
void testUngrab();
void testRedirect_data();
void testRedirect();
private:
ScriptedEffect *loadEffect(const QString &name);
@ -301,13 +303,15 @@ void ScriptedEffectsTest::testAnimations()
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 @@ void ScriptedEffectsTest::testAnimations()
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 @@ void ScriptedEffectsTest::testUngrab()
QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value<void *>(), nullptr);
}
void ScriptedEffectsTest::testRedirect_data()
{
QTest::addColumn<QString>("file");
QTest::addColumn<bool>("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<AniData> 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<AniData> 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<AniData> 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"

View File

@ -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');
}
});

View File

@ -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');
}
});

View File

@ -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');
}
});

View File

@ -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');
}
});

View File

@ -73,13 +73,12 @@ AniData::AniData()
, 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 @@ AniData::AniData(AnimationEffect::Attribute a, int meta_, const FPx2 &to_,
, 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) {

View File

@ -76,9 +76,11 @@ public:
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 @@ public:
uint meta;
qint64 startTime;
QSharedPointer<FullScreenEffectLock> fullScreenEffectLock;
bool waitAtSource, keepAtTarget;
bool waitAtSource;
bool keepAlive;
KeepAliveLockPtr keepAliveLock;
PreviousWindowPixmapLockPtr previousWindowPixmapLock;
AnimationEffect::TerminationFlags terminationFlags;
};
} // namespace

View File

@ -3,6 +3,7 @@
This file is part of the KDE project.
Copyright (C) 2011 Thomas Lübking <thomas.luebking@web.de>
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com>
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 @@ quint64 AnimationEffect::p_animate( EffectWindow *w, Attribute a, uint meta, int
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 @@ quint64 AnimationEffect::p_animate( EffectWindow *w, Attribute a, uint meta, int
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 @@ bool AnimationEffect::retarget(quint64 animationId, FPx2 newTarget, int newRemai
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 @@ void AnimationEffect::prePaintScreen( ScreenPrePaintData& data, int time )
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;

View File

@ -3,6 +3,7 @@
This file is part of the KDE project.
Copyright (C) 2011 Thomas Lübking <thomas.luebking@web.de>
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com>
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 @@ public:
};
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 @@ protected:
*/
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 @@ private:
} // 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

View File

@ -438,6 +438,60 @@ QScriptValue kwinEffectRetarget(QScriptContext *context, QScriptEngine *engine)
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<quint64> 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<AnimationEffect::Direction>(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<AnimationEffect::TerminationFlags>(wrappedTerminationFlags.toInt32());
}
ScriptedEffect *effect = qobject_cast<ScriptedEffect *>(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<ScriptedEffect*>(context->callee().data().toQObject());
@ -592,6 +646,11 @@ bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript
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 @@ bool ScriptedEffect::retarget(quint64 animationId, KWin::FPx2 newTarget, int new
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<KWin::DataRole>(grabRole)).value<void*>();

View File

@ -128,6 +128,7 @@ public Q_SLOTS:
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);