kwin/autotests/integration/effects/scripted_effects_test.cpp

781 lines
29 KiB
C++
Raw Normal View History

2020-08-02 22:22:19 +00:00
/*
KWin - the KDE window manager
This file is part of the KDE project.
2020-08-02 22:22:19 +00:00
SPDX-FileCopyrightText: 2018 David Edmundson <davidedmundson@kde.org>
2020-08-02 22:22:19 +00:00
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "scripting/scriptedeffect.h"
#include "libkwineffects/anidata_p.h"
#include "abstract_client.h"
#include "composite.h"
#include "cursor.h"
#include "deleted.h"
#include "effect_builtins.h"
#include "effectloader.h"
#include "effects.h"
#include "kwin_wayland_test.h"
#include "platform.h"
#include "virtualdesktops.h"
#include "wayland_server.h"
#include "workspace.h"
#include <QScriptContext>
#include <QScriptEngine>
#include <QScriptValue>
#include <KConfigGroup>
#include <KGlobalAccel>
#include <KWayland/Client/compositor.h>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/slide.h>
#include <KWayland/Client/surface.h>
#include <KWayland/Client/xdgshell.h>
using namespace KWin;
using namespace std::chrono_literals;
static const QString s_socketName = QStringLiteral("wayland_test_effects_scripts-0");
class ScriptedEffectsTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void testEffectsHandler();
void testEffectsContext();
void testShortcuts();
void testAnimations_data();
void testAnimations();
void testScreenEdge();
void testScreenEdgeTouch();
void testFullScreenEffect_data();
void testFullScreenEffect();
void testKeepAlive_data();
void testKeepAlive();
void testGrab();
void testGrabAlreadyGrabbedWindow();
void testGrabAlreadyGrabbedWindowForced();
void testUngrab();
[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
2018-10-24 19:58:29 +00:00
void testRedirect_data();
void testRedirect();
void testComplete();
private:
ScriptedEffect *loadEffect(const QString &name);
};
class ScriptedEffectWithDebugSpy : public KWin::ScriptedEffect
{
Q_OBJECT
public:
ScriptedEffectWithDebugSpy();
bool load(const QString &name);
using AnimationEffect::AniMap;
using AnimationEffect::state;
signals:
void testOutput(const QString &data);
};
QScriptValue kwinEffectScriptTestOut(QScriptContext *context, QScriptEngine *engine)
{
auto *script = qobject_cast<ScriptedEffectWithDebugSpy*>(context->callee().data().toQObject());
QString result;
for (int i = 0; i < context->argumentCount(); ++i) {
if (i > 0) {
result.append(QLatin1Char(' '));
}
result.append(context->argument(i).toString());
}
emit script->testOutput(result);
return engine->undefinedValue();
}
ScriptedEffectWithDebugSpy::ScriptedEffectWithDebugSpy()
: ScriptedEffect()
{
QScriptValue testHookFunc = engine()->newFunction(kwinEffectScriptTestOut);
testHookFunc.setData(engine()->newQObject(this));
engine()->globalObject().setProperty(QStringLiteral("sendTestResponse"), testHookFunc);
}
bool ScriptedEffectWithDebugSpy::load(const QString &name)
{
const QString path = QFINDTESTDATA("./scripts/" + name + ".js");
if (!init(name, path)) {
return false;
}
// inject our newly created effect to be registered with the EffectsHandlerImpl::loaded_effects
// this is private API so some horrible code is used to find the internal effectloader
// and register ourselves
auto c = effects->children();
for (auto it = c.begin(); it != c.end(); ++it) {
if (qstrcmp((*it)->metaObject()->className(), "KWin::EffectLoader") != 0) {
continue;
}
QMetaObject::invokeMethod(*it, "effectLoaded", Q_ARG(KWin::Effect*, this), Q_ARG(QString, name));
break;
}
return (static_cast<EffectsHandlerImpl*>(effects)->isEffectLoaded(name));
}
void ScriptedEffectsTest::initTestCase()
{
qRegisterMetaType<KWin::AbstractClient*>();
qRegisterMetaType<KWin::Deleted*>();
qRegisterMetaType<KWin::Effect*>();
QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
QVERIFY(applicationStartedSpy.isValid());
kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
QVERIFY(waylandServer()->init(s_socketName));
ScriptedEffectLoader loader;
// disable all effects - we don't want to have it interact with the rendering
auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
KConfigGroup plugins(config, QStringLiteral("Plugins"));
const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects();
for (QString name : builtinNames) {
plugins.writeEntry(name + QStringLiteral("Enabled"), false);
}
config->sync();
kwinApp()->setConfig(config);
qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2"));
qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1");
kwinApp()->start();
QVERIFY(applicationStartedSpy.wait());
QVERIFY(Compositor::self());
auto scene = KWin::Compositor::self()->scene();
QVERIFY(scene);
QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing);
KWin::VirtualDesktopManager::self()->setCount(2);
}
void ScriptedEffectsTest::init()
{
QVERIFY(Test::setupWaylandConnection());
}
void ScriptedEffectsTest::cleanup()
{
Test::destroyWaylandConnection();
auto effectsImpl = static_cast<EffectsHandlerImpl *>(effects);
effectsImpl->unloadAllEffects();
QVERIFY(effectsImpl->loadedEffects().isEmpty());
KWin::VirtualDesktopManager::self()->setCurrent(1);
}
void ScriptedEffectsTest::testEffectsHandler()
{
// this triggers and tests some of the signals in EffectHandler, which is exposed to JS as context property "effects"
auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
2020-08-26 17:24:02 +00:00
auto waitFor = [&effectOutputSpy](const QString &expected) {
QVERIFY(effectOutputSpy.count() > 0 || effectOutputSpy.wait());
QCOMPARE(effectOutputSpy.first().first(), expected);
effectOutputSpy.removeFirst();
};
QVERIFY(effect->load("effectsHandler"));
// trigger windowAdded signal
// create a window
using namespace KWayland::Client;
auto *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
auto *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
shellSurface->setTitle("WindowA");
auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
waitFor("windowAdded - WindowA");
waitFor("stackingOrder - 1 WindowA");
// windowMinimsed
c->minimize();
waitFor("windowMinimized - WindowA");
c->unminimize();
waitFor("windowUnminimized - WindowA");
surface->deleteLater();
waitFor("windowClosed - WindowA");
// desktop management
KWin::VirtualDesktopManager::self()->setCurrent(2);
waitFor("desktopChanged - 1 2");
}
void ScriptedEffectsTest::testEffectsContext()
{
// this tests misc non-objects exposed to the script engine: animationTime, displaySize, use of external enums
auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effect->load("effectContext"));
QCOMPARE(effectOutputSpy[0].first(), "1280x1024");
QCOMPARE(effectOutputSpy[1].first(), "100");
QCOMPARE(effectOutputSpy[2].first(), "2");
QCOMPARE(effectOutputSpy[3].first(), "0");
}
void ScriptedEffectsTest::testShortcuts()
{
// this tests method registerShortcut
auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effect->load("shortcutsTest"));
QCOMPARE(effect->shortcutCallbacks().count(), 1);
QAction *action = effect->shortcutCallbacks().keys()[0];
QCOMPARE(action->objectName(), "testShortcut");
QCOMPARE(action->text(), "Test Shortcut");
QCOMPARE(KGlobalAccel::self()->shortcut(action).first(), QKeySequence("Meta+Shift+Y"));
action->trigger();
QCOMPARE(effectOutputSpy[0].first(), "shortcutTriggered");
}
void ScriptedEffectsTest::testAnimations_data()
{
QTest::addColumn<QString>("file");
QTest::addColumn<int>("animationCount");
QTest::newRow("single") << "animationTest" << 1;
QTest::newRow("multi") << "animationTestMulti" << 2;
}
void ScriptedEffectsTest::testAnimations()
{
// this tests animate/set/cancel
// methods take either an int or an array, as forced in the data above
// also splits animate vs effects.animate(..)
QFETCH(QString, file);
QFETCH(int, animationCount);
auto *effect = new ScriptedEffectWithDebugSpy;
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effect->load(file));
// animated after window added connect
using namespace KWayland::Client;
auto *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
auto *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
shellSurface->setTitle("Window 1");
auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
{
const auto state = effect->state();
QCOMPARE(state.count(), 1);
QCOMPARE(state.firstKey(), c->effectWindow());
const auto &animationsForWindow = state.first().first;
QCOMPARE(animationsForWindow.count(), animationCount);
QCOMPARE(animationsForWindow[0].timeLine.duration(), 100ms);
QCOMPARE(animationsForWindow[0].to, FPx2(1.4));
QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale);
QCOMPARE(animationsForWindow[0].timeLine.easingCurve().type(), QEasingCurve::OutCubic);
[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
2018-10-24 19:58:29 +00:00
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);
[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
2018-10-24 19:58:29 +00:00
QCOMPARE(animationsForWindow[1].terminationFlags,
AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget);
}
}
QCOMPARE(effectOutputSpy[0].first(), "true");
// window state changes, scale should be retargetted
c->setMinimized(true);
{
const auto state = effect->state();
QCOMPARE(state.count(), 1);
const auto &animationsForWindow = state.first().first;
QCOMPARE(animationsForWindow.count(), animationCount);
QCOMPARE(animationsForWindow[0].timeLine.duration(), 200ms);
QCOMPARE(animationsForWindow[0].to, FPx2(1.5));
QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale);
[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
2018-10-24 19:58:29 +00:00
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);
[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
2018-10-24 19:58:29 +00:00
QCOMPARE(animationsForWindow[1].terminationFlags,
AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget);
}
}
c->setMinimized(false);
{
const auto state = effect->state();
QCOMPARE(state.count(), 0);
}
}
void ScriptedEffectsTest::testScreenEdge()
{
// this test checks registerScreenEdge functions
auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effect->load("screenEdgeTest"));
effect->borderActivated(KWin::ElectricTopRight);
QCOMPARE(effectOutputSpy.count(), 1);
}
void ScriptedEffectsTest::testScreenEdgeTouch()
{
// this test checks registerTouchScreenEdge functions
auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effect->load("screenEdgeTouchTest"));
auto actions = effect->findChildren<QAction*>(QString(), Qt::FindDirectChildrenOnly);
actions[0]->trigger();
QCOMPARE(effectOutputSpy.count(), 1);
}
void ScriptedEffectsTest::testFullScreenEffect_data()
{
QTest::addColumn<QString>("file");
QTest::newRow("single") << "fullScreenEffectTest";
QTest::newRow("multi") << "fullScreenEffectTestMulti";
QTest::newRow("global") << "fullScreenEffectTestGlobal";
}
void ScriptedEffectsTest::testFullScreenEffect()
{
QFETCH(QString, file);
auto *effectMain = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
QSignalSpy effectOutputSpy(effectMain, &ScriptedEffectWithDebugSpy::testOutput);
QSignalSpy fullScreenEffectActiveSpy(effects, &EffectsHandler::hasActiveFullScreenEffectChanged);
QSignalSpy isActiveFullScreenEffectSpy(effectMain, &ScriptedEffect::isActiveFullScreenEffectChanged);
QVERIFY(effectMain->load(file));
//load any random effect from another test to confirm fullscreen effect state is correctly
//shown as being someone else
auto effectOther = new ScriptedEffectWithDebugSpy();
QVERIFY(effectOther->load("screenEdgeTouchTest"));
QSignalSpy isActiveFullScreenEffectSpyOther(effectOther, &ScriptedEffect::isActiveFullScreenEffectChanged);
using namespace KWayland::Client;
auto *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
auto *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
shellSurface->setTitle("Window 1");
auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
QCOMPARE(effects->hasActiveFullScreenEffect(), false);
QCOMPARE(effectMain->isActiveFullScreenEffect(), false);
//trigger animation
KWin::VirtualDesktopManager::self()->setCurrent(2);
QCOMPARE(effects->activeFullScreenEffect(), effectMain);
QCOMPARE(effects->hasActiveFullScreenEffect(), true);
QCOMPARE(fullScreenEffectActiveSpy.count(), 1);
QCOMPARE(effectMain->isActiveFullScreenEffect(), true);
QCOMPARE(isActiveFullScreenEffectSpy.count(), 1);
QCOMPARE(effectOther->isActiveFullScreenEffect(), false);
QCOMPARE(isActiveFullScreenEffectSpyOther.count(), 0);
//after 500ms trigger another full screen animation
QTest::qWait(500);
KWin::VirtualDesktopManager::self()->setCurrent(1);
QCOMPARE(effects->activeFullScreenEffect(), effectMain);
//after 1000ms (+a safety margin for time based tests) we should still be the active full screen effect
//despite first animation expiring
QTest::qWait(500+100);
QCOMPARE(effects->activeFullScreenEffect(), effectMain);
//after 1500ms (+a safetey margin) we should have no full screen effect
QTest::qWait(500+100);
QCOMPARE(effects->activeFullScreenEffect(), nullptr);
}
void ScriptedEffectsTest::testKeepAlive_data()
{
QTest::addColumn<QString>("file");
QTest::addColumn<bool>("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::createXdgShellStableSurface(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);
}
}
void ScriptedEffectsTest::testGrab()
{
// this test verifies that scripted effects can grab windows that are
// not already grabbed
// load the test effect
auto effect = new ScriptedEffectWithDebugSpy;
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effectOutputSpy.isValid());
QVERIFY(effect->load(QStringLiteral("grabTest")));
// create test client
using namespace KWayland::Client;
Surface *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
// the test effect should grab the test client successfully
QCOMPARE(effectOutputSpy.count(), 1);
QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value<void *>(), effect);
}
void ScriptedEffectsTest::testGrabAlreadyGrabbedWindow()
{
// this test verifies that scripted effects cannot grab already grabbed
// windows (unless force is set to true of course)
// load effect that will hold the window grab
auto owner = new ScriptedEffectWithDebugSpy;
QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(ownerOutputSpy.isValid());
QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowTest_owner")));
// load effect that will try to grab already grabbed window
auto grabber = new ScriptedEffectWithDebugSpy;
QSignalSpy grabberOutputSpy(grabber, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(grabberOutputSpy.isValid());
QVERIFY(grabber->load(QStringLiteral("grabAlreadyGrabbedWindowTest_grabber")));
// create test client
using namespace KWayland::Client;
Surface *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
// effect that initially held the grab should still hold the grab
QCOMPARE(ownerOutputSpy.count(), 1);
QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok"));
QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value<void *>(), owner);
// effect that tried to grab already grabbed window should fail miserably
QCOMPARE(grabberOutputSpy.count(), 1);
QCOMPARE(grabberOutputSpy.first().first(), QStringLiteral("fail"));
}
void ScriptedEffectsTest::testGrabAlreadyGrabbedWindowForced()
{
// this test verifies that scripted effects can steal window grabs when
// they forcefully try to grab windows
// load effect that initially will be holding the window grab
auto owner = new ScriptedEffectWithDebugSpy;
QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(ownerOutputSpy.isValid());
QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_owner")));
// load effect that will try to steal the window grab
auto thief = new ScriptedEffectWithDebugSpy;
QSignalSpy thiefOutputSpy(thief, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(thiefOutputSpy.isValid());
QVERIFY(thief->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_thief")));
// create test client
using namespace KWayland::Client;
Surface *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
// verify that the owner in fact held the grab
QCOMPARE(ownerOutputSpy.count(), 1);
QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok"));
// effect that grabbed the test client forcefully should now hold the grab
QCOMPARE(thiefOutputSpy.count(), 1);
QCOMPARE(thiefOutputSpy.first().first(), QStringLiteral("ok"));
QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value<void *>(), thief);
}
void ScriptedEffectsTest::testUngrab()
{
// this test verifies that scripted effects can ungrab windows that they
// are previously grabbed
// load the test effect
auto effect = new ScriptedEffectWithDebugSpy;
QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
QVERIFY(effectOutputSpy.isValid());
QVERIFY(effect->load(QStringLiteral("ungrabTest")));
// create test client
using namespace KWayland::Client;
Surface *surface = Test::createSurface(Test::waylandCompositor());
QVERIFY(surface);
XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface);
QVERIFY(shellSurface);
AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
QVERIFY(c);
QCOMPARE(workspace()->activeClient(), c);
// the test effect should grab the test client successfully
QCOMPARE(effectOutputSpy.count(), 1);
QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value<void *>(), effect);
// when the test effect sees that a window was minimized, it will try to ungrab it
effectOutputSpy.clear();
c->setMinimized(true);
QCOMPARE(effectOutputSpy.count(), 1);
QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
QCOMPARE(c->effectWindow()->data(WindowAddedGrabRole).value<void *>(), nullptr);
}
[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
2018-10-24 19:58:29 +00:00
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);
AbstractClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue);
[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
2018-10-24 19:58:29 +00:00
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 auto state = effect->state();
[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
2018-10-24 19:58:29 +00:00
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 auto state = effect->state();
[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
2018-10-24 19:58:29 +00:00
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 auto state = effect->state();
[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
2018-10-24 19:58:29 +00:00
QCOMPARE(state.count(), 0);
} else {
const auto state = effect->state();
[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
2018-10-24 19:58:29 +00:00
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);
}
}
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);
AbstractClient *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 auto state = effect->state();
QCOMPARE(state.count(), 1);
QCOMPARE(state.firstKey(), c->effectWindow());
const QList<AniData> 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 auto state = effect->state();
QCOMPARE(state.count(), 1);
QCOMPARE(state.firstKey(), c->effectWindow());
const QList<AniData> 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 auto 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.elapsed(), 1000ms);
QVERIFY(animations[0].timeLine.done());
}
}
WAYLANDTEST_MAIN(ScriptedEffectsTest)
#include "scripted_effects_test.moc"