diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp index e682a475d..dab3f3035 100644 --- a/autotests/integration/effects/scripted_effects_test.cpp +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -1,791 +1,792 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2018 David Edmundson 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "scripting/scriptedeffect.h" #include "libkwineffects/anidata_p.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 "shell_client.h" #include "virtualdesktops.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include 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(); 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(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(effects)->isEffectLoaded(name)); } void ScriptedEffectsTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); 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(workspaceCreatedSpy.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(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); auto waitFor = [&effectOutputSpy, this](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::createXdgShellV6Surface(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("file"); QTest::addColumn("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::createXdgShellV6Surface(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 AnimationEffect::AniMap state = effect->state(); + 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::OutQuad); 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].terminationFlags, AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); } } QCOMPARE(effectOutputSpy[0].first(), "true"); // window state changes, scale should be retargetted c->setMinimized(true); { - const AnimationEffect::AniMap state = effect->state(); + 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); 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].terminationFlags, AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); } } c->setMinimized(false); { - const AnimationEffect::AniMap state = effect->state(); + 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(QString(), Qt::FindDirectChildrenOnly); actions[0]->trigger(); QCOMPARE(effectOutputSpy.count(), 1); } void ScriptedEffectsTest::testFullScreenEffect_data() { QTest::addColumn("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::createXdgShellV6Surface(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("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); } } 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); ShellClient *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(), 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); ShellClient *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(), 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); ShellClient *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(), 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); ShellClient *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(), 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(), 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(); + const auto 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(); + const auto 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(); + const auto state = effect->state(); QCOMPARE(state.count(), 0); } else { - const AnimationEffect::AniMap state = effect->state(); + const auto 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); } } void ScriptedEffectsTest::testComplete() { // this test verifies that complete works // load the test effect auto effect = new ScriptedEffectWithDebugSpy; QVERIFY(effect->load(QStringLiteral("completeTest"))); // create test client using namespace KWayland::Client; Surface *surface = Test::createSurface(Test::waylandCompositor()); QVERIFY(surface); XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface); QVERIFY(shellSurface); ShellClient *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); auto around = [] (std::chrono::milliseconds elapsed, std::chrono::milliseconds pivot, std::chrono::milliseconds margin) { return qAbs(elapsed.count() - pivot.count()) < margin.count(); }; // initially, the test animation should be at the start position { - const AnimationEffect::AniMap state = effect->state(); + const auto state = effect->state(); QCOMPARE(state.count(), 1); QCOMPARE(state.firstKey(), c->effectWindow()); const QList animations = state.first().first; QCOMPARE(animations.count(), 1); QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms)); QVERIFY(!animations[0].timeLine.done()); } // wait for 250ms QTest::qWait(250); { - const AnimationEffect::AniMap state = effect->state(); + const auto state = effect->state(); QCOMPARE(state.count(), 1); QCOMPARE(state.firstKey(), c->effectWindow()); const QList animations = state.first().first; QCOMPARE(animations.count(), 1); QVERIFY(around(animations[0].timeLine.elapsed(), 250ms, 50ms)); QVERIFY(!animations[0].timeLine.done()); } // minimize the test client, when the test effect sees that a window was // minimized, it will try to complete animation for it QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); QVERIFY(effectOutputSpy.isValid()); c->setMinimized(true); QCOMPARE(effectOutputSpy.count(), 1); QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); { - const AnimationEffect::AniMap state = effect->state(); + const auto state = effect->state(); QCOMPARE(state.count(), 1); QCOMPARE(state.firstKey(), c->effectWindow()); const QList animations = state.first().first; QCOMPARE(animations.count(), 1); QCOMPARE(animations[0].timeLine.elapsed(), 1000ms); QVERIFY(animations[0].timeLine.done()); } } WAYLANDTEST_MAIN(ScriptedEffectsTest) #include "scripted_effects_test.moc" diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h index 4e32f2fae..c8b412867 100644 --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -1,413 +1,418 @@ /******************************************************************** KWin - the KDE window manager 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef ANIMATION_EFFECT_H #define ANIMATION_EFFECT_H #include #include #include #include #include namespace KWin { class KWINEFFECTS_EXPORT FPx2 { public: FPx2() { f[0] = f[1] = 0.0; valid = false; } explicit FPx2(float v) { f[0] = f[1] = v; valid = true; } FPx2(float v1, float v2) { f[0] = v1; f[1] = v2; valid = true; } FPx2(const FPx2 &other) { f[0] = other.f[0]; f[1] = other.f[1]; valid = other.valid; } explicit FPx2(const QPoint &other) { f[0] = other.x(); f[1] = other.y(); valid = true; } explicit FPx2(const QPointF &other) { f[0] = other.x(); f[1] = other.y(); valid = true; } explicit FPx2(const QSize &other) { f[0] = other.width(); f[1] = other.height(); valid = true; } explicit FPx2(const QSizeF &other) { f[0] = other.width(); f[1] = other.height(); valid = true; } inline void invalidate() { valid = false; } inline bool isValid() const { return valid; } inline float operator[](int n) const { return f[n]; } inline QString toString() const { QString ret; if (valid) ret = QString::number(f[0]) + QLatin1Char(',') + QString::number(f[1]); else ret = QString(); return ret; } inline FPx2 &operator+=(const FPx2 &other) { f[0] += other[0]; f[1] += other[1]; return *this; } inline FPx2 &operator-=(const FPx2 &other) { f[0] -= other[0]; f[1] -= other[1]; return *this; } inline FPx2 &operator*=(float fl) { f[0] *= fl; f[1] *= fl; return *this; } inline FPx2 &operator/=(float fl) { f[0] /= fl; f[1] /= fl; return *this; } friend inline bool operator==(const FPx2 &f1, const FPx2 &f2) { return f1[0] == f2[0] && f1[1] == f2[1]; } friend inline bool operator!=(const FPx2 &f1, const FPx2 &f2) { return f1[0] != f2[0] || f1[1] != f2[1]; } friend inline const FPx2 operator+(const FPx2 &f1, const FPx2 &f2) { return FPx2( f1[0] + f2[0], f1[1] + f2[1] ); } friend inline const FPx2 operator-(const FPx2 &f1, const FPx2 &f2) { return FPx2( f1[0] - f2[0], f1[1] - f2[1] ); } friend inline const FPx2 operator*(const FPx2 &f, float fl) { return FPx2( f[0] * fl, f[1] * fl ); } friend inline const FPx2 operator*(float fl, const FPx2 &f) { return FPx2( f[0] * fl, f[1] *fl ); } friend inline const FPx2 operator-(const FPx2 &f) { return FPx2( -f[0], -f[1] ); } friend inline const FPx2 operator/(const FPx2 &f, float fl) { return FPx2( f[0] / fl, f[1] / fl ); } inline void set(float v) { f[0] = v; valid = true; } inline void set(float v1, float v2) { f[0] = v1; f[1] = v2; valid = true; } private: float f[2]; bool valid; }; class AniData; class AnimationEffectPrivate; /** * Base class for animation effects. * * AnimationEffect serves as a base class for animation effects. It makes easier * implementing animated transitions, without having to worry about low-level * specific stuff, e.g. referencing and unreferencing deleted windows, scheduling * repaints for the next frame, etc. * * Each animation animates one specific attribute, e.g. size, position, scale, etc. * You can provide your own implementation of the Generic attribute if none of the * standard attributes(e.g. size, position, etc) satisfy your requirements. * * @since 4.8 **/ class KWINEFFECTS_EXPORT AnimationEffect : public Effect { Q_OBJECT public: - typedef QMap< EffectWindow*, QPair, QRect> > AniMap; - enum Anchor { Left = 1<<0, Top = 1<<1, Right = 1<<2, Bottom = 1<<3, Horizontal = Left|Right, Vertical = Top|Bottom, Mouse = 1<<4 }; Q_ENUM(Anchor) enum Attribute { Opacity = 0, Brightness, Saturation, Scale, Rotation, Position, Size, Translation, Clip, Generic, CrossFadePrevious, NonFloatBase = Position }; Q_ENUM(Attribute) enum MetaType { SourceAnchor, TargetAnchor, RelativeSourceX, RelativeSourceY, RelativeTargetX, RelativeTargetY, Axis }; Q_ENUM(MetaType) /** * This enum type is used to specify the direction of the animation. * * @since 5.15 **/ 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. * * @since 5.15 **/ enum TerminationFlag { /** * Don't terminate the animation when it reaches source or target position. **/ DontTerminate = 0x00, /** * 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). **/ TerminateAtSource = 0x01, /** * Terminate the animation when it reaches the target position. If this flag * is not set, then the animation will be persistent. **/ TerminateAtTarget = 0x02 }; Q_FLAGS(TerminationFlag) Q_DECLARE_FLAGS(TerminationFlags, TerminationFlag) /** * Constructs AnimationEffect. * * Whenever you intend to connect to the EffectsHandler::windowClosed() signal, * do so when reimplementing the constructor. Do not add private slots named * _windowClosed or _windowDeleted! The AnimationEffect connects them right after * the construction. * * If you shadow the _windowDeleted slot (it doesn't matter that it's a private * slot), this will lead to segfaults. * * If you shadow _windowClosed or connect your slot to EffectsHandler::windowClosed() * after _windowClosed was connected, animations for closing windows will fail. **/ AnimationEffect(); ~AnimationEffect(); bool isActive() const; /** * Gets stored metadata. * * Metadata can be used to store some extra information, for example rotation axis, * etc. The first 24 bits are reserved for the AnimationEffect class, you can use * the last 8 bits for custom hints. In case when you transform a Generic attribute, * all 32 bits are yours and you can use them as you want and read them in your * genericAnimation() implementation. * * @param type The type of the metadata. * @param meta Where the metadata is stored. * @returns Stored metadata. * @since 4.8 **/ static int metaData(MetaType type, uint meta ); /** * Sets metadata. * * @param type The type of the metadata. * @param value The data to be stored. * @param meta Where the metadata will be stored. * @since 4.8 **/ static void setMetaData(MetaType type, uint value, uint &meta ); // Reimplemented from KWin::Effect. QString debug(const QString ¶meter) const; virtual void prePaintScreen( ScreenPrePaintData& data, int time ); virtual void prePaintWindow( EffectWindow* w, WindowPrePaintData& data, int time ); virtual void paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data ); virtual void postPaintScreen(); /** * Gaussian (bumper) animation curve for QEasingCurve. * * @since 4.8 **/ static qreal qecGaussian(qreal progress) { progress = 2*progress - 1; progress *= -5*progress; return qExp(progress); } /** * @since 4.8 **/ static inline qint64 clock() { return s_clock.elapsed(); } protected: /** * Starts an animated transition of any supported attribute. * * @param w The animated window. * @param a The animated attribute. * @param meta Basically a wildcard to carry various extra information, e.g. * the anchor, relativity or rotation axis. You will probably use it when * performing Generic animations. * @param ms How long the transition will last. * @param to The target value. FPx2 is an agnostic two component float type * (like QPointF or QSizeF, but without requiring to be either and supporting * an invalid state). * @param curve How the animation progresses, e.g. 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 fullScreen Sets this effect as the active full screen effect for the * duration of the animation. * @param keepAlive Whether closed windows should be kept alive during animation. * @returns An ID that you can use to cancel a running animation. * @since 4.8 **/ quint64 animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2(), bool fullScreen = false, bool keepAlive = true) { return p_animate(w, a, meta, ms, to, curve, delay, from, false, fullScreen, keepAlive); } /** * Starts a persistent animated transition of any supported attribute. * * This method is equal to animate() with one important difference: * the target value for the attribute is kept until you call cancel(). * * @param w The animated window. * @param a The animated attribute. * @param meta Basically a wildcard to carry various extra information, e.g. * the anchor, relativity or rotation axis. You will probably use it when * performing Generic animations. * @param ms How long the transition will last. * @param to The target value. FPx2 is an agnostic two component float type * (like QPointF or QSizeF, but without requiring to be either and supporting * an invalid state). * @param curve How the animation progresses, e.g. 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 fullScreen Sets this effect as the active full screen effect for the * duration of the animation. * @param keepAlive Whether closed windows should be kept alive during animation. * @returns An ID that you need to use to cancel this manipulation. * @since 4.11 **/ quint64 set( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2(), bool fullScreen = false, bool keepAlive = true) { return p_animate(w, a, meta, ms, to, curve, delay, from, true, fullScreen, keepAlive); } /** * Changes the target (but not type or curve) of a running animation. * * Please use cancel() to cancel an animation rather than altering it. * * @param animationId The id of the animation to be retargetted. * @param newTarget The new target. * @param newRemainingTime The new duration of the transition. By default (-1), * the remaining time remains unchanged. * @returns @c true if the animation was retargetted successfully, @c false otherwise. * @note You can NOT retarget an animation that just has just ended! * @since 5.6 **/ 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 terminationFlags 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. * @since 5.15 **/ bool redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags = TerminateAtSource); /** * Fast-forwards the animation to the target position. * * @param animationId The id of the animation. * @returns @c true if the animation was fast-forwarded successfully, otherwise * @c false. * @since 5.15 **/ bool complete(quint64 animationId); /** * Called whenever an animation ends. * * You can reimplement this method to keep a constant transformation for the window * (i.e. keep it at some opacity or position) or to start another animation. * * @param w The animated window. * @param a The animated attribute. * @param meta Originally supplied metadata to animate() or set(). * @since 4.8 **/ virtual void animationEnded(EffectWindow *w, Attribute a, uint meta) {Q_UNUSED(w); Q_UNUSED(a); Q_UNUSED(meta);} /** * Cancels a running animation. * * @param animationId The id of the animation. * @returns @c true if the animation was found (and canceled), @c false otherwise. * @note There is NO animated reset of the original value. You'll have to provide * that with a second animation. * @note This will eventually release a Deleted window as well. * @note If you intend to run another animation on the (Deleted) window, you have * to do that before cancelling the old animation (to keep the window around). * @since 4.11 **/ bool cancel(quint64 animationId); /** * Called whenever animation that transforms Generic attribute needs to be painted. * * You should reimplement this method if you transform Generic attribute. @p meta * can be used to support more than one additional animations. * * @param w The animated window. * @param data The paint data. * @param progress Current progress value. * @param meta The metadata. * @since 4.8 **/ virtual void genericAnimation( EffectWindow *w, WindowPaintData &data, float progress, uint meta ) {Q_UNUSED(w); Q_UNUSED(data); Q_UNUSED(progress); Q_UNUSED(meta);} - //Internal for unit tests + /** + * @internal + **/ + typedef QMap, QRect> > AniMap; + + /** + * @internal + **/ 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, bool fullScreenEffect, 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; float progress( const AniData& ) const; void disconnectGeometryChanges(); void updateLayerRepaints(); void validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const; private Q_SLOTS: void init(); void triggerRepaint(); void _windowClosed( KWin::EffectWindow* w ); void _windowDeleted( KWin::EffectWindow* w ); void _expandedGeometryChanged(KWin::EffectWindow *w, const QRect &old); private: static QElapsedTimer s_clock; AnimationEffectPrivate * const d_ptr; Q_DECLARE_PRIVATE(AnimationEffect) Q_DISABLE_COPY(AnimationEffect) }; } // 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