diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp index 445d24050..f0e81370a 100644 --- a/autotests/integration/effects/scripted_effects_test.cpp +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -1,626 +1,623 @@ /******************************************************************** 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(); private: ScriptedEffect *loadEffect(const QString &name); }; class ScriptedEffectWithDebugSpy : public KWin::ScriptedEffect { Q_OBJECT public: ScriptedEffectWithDebugSpy(); bool load(const QString &name); 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()); KWin::VirtualDesktopManager::self()->setCount(2); } void ScriptedEffectsTest::init() { QVERIFY(Test::setupWaylandConnection()); } void ScriptedEffectsTest::cleanup() { Test::destroyWaylandConnection(); auto *e = static_cast(effects); while (!e->loadedEffects().isEmpty()) { const QString effect = e->loadedEffects().first(); e->unloadEffect(effect); QVERIFY(!e->isEffectLoaded(effect)); } 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); - // we are running the event loop during renderAndWaitForShown - // some time will pass with the event loop running between the window being added and getting to here - // anim.duration is an aboslute value, but retarget will update the duration based on time passed - int timePassed = 0; - { const AnimationEffect::AniMap 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].duration, 100); + QCOMPARE(animationsForWindow[0].timeLine.duration(), 100ms); QCOMPARE(animationsForWindow[0].to, FPx2(1.4)); QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); - QCOMPARE(animationsForWindow[0].curve.type(), QEasingCurve::OutQuad); + QCOMPARE(animationsForWindow[0].timeLine.easingCurve().type(), QEasingCurve::OutQuad); QCOMPARE(animationsForWindow[0].keepAtTarget, false); - timePassed = animationsForWindow[0].time; + if (animationCount == 2) { - QCOMPARE(animationsForWindow[1].duration, 100); + 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(effectOutputSpy[0].first(), "true"); // window state changes, scale should be retargetted c->setMinimized(true); { const AnimationEffect::AniMap state = effect->state(); QCOMPARE(state.count(), 1); const auto &animationsForWindow = state.first().first; QCOMPARE(animationsForWindow.count(), animationCount); - QCOMPARE(animationsForWindow[0].duration, 200 + timePassed); + QCOMPARE(animationsForWindow[0].timeLine.duration(), 200ms); QCOMPARE(animationsForWindow[0].to, FPx2(1.5)); QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); QCOMPARE(animationsForWindow[0].keepAtTarget, false); if (animationCount == 2) { - QCOMPARE(animationsForWindow[1].duration, 200 + timePassed); + QCOMPARE(animationsForWindow[1].timeLine.duration(), 200ms); QCOMPARE(animationsForWindow[1].to, FPx2(1.5)); QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); QCOMPARE(animationsForWindow[1].keepAtTarget, false); } } c->setMinimized(false); { const AnimationEffect::AniMap 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); } WAYLANDTEST_MAIN(ScriptedEffectsTest) #include "scripted_effects_test.moc" diff --git a/libkwineffects/anidata.cpp b/libkwineffects/anidata.cpp index bfdb00776..ceb4a8121 100644 --- a/libkwineffects/anidata.cpp +++ b/libkwineffects/anidata.cpp @@ -1,127 +1,122 @@ /******************************************************************** 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 . *********************************************************************/ #include "anidata_p.h" #include "logging_p.h" QDebug operator<<(QDebug dbg, const KWin::AniData &a) { dbg.nospace() << a.debugInfo(); return dbg.space(); } using namespace KWin; FullScreenEffectLock::FullScreenEffectLock(Effect *effect) { effects->setActiveFullScreenEffect(effect); } FullScreenEffectLock::~FullScreenEffectLock() { effects->setActiveFullScreenEffect(nullptr); } KeepAliveLock::KeepAliveLock(EffectWindow *w) : m_window(w) { m_window->refWindow(); } KeepAliveLock::~KeepAliveLock() { m_window->unrefWindow(); } PreviousWindowPixmapLock::PreviousWindowPixmapLock(EffectWindow *w) : m_window(w) { m_window->referencePreviousWindowPixmap(); } PreviousWindowPixmapLock::~PreviousWindowPixmapLock() { m_window->unreferencePreviousWindowPixmap(); // Add synthetic repaint to prevent glitches after cross-fading // translucent windows. effects->addRepaint(m_window->expandedGeometry()); } AniData::AniData() : attribute(AnimationEffect::Opacity) , customCurve(0) // Linear - , time(0) - , duration(0) , meta(0) , startTime(0) , waitAtSource(false) , keepAtTarget(false) , keepAlive(true) { } -AniData::AniData(AnimationEffect::Attribute a, int meta_, int ms, const FPx2 &to_, - QEasingCurve curve_, int delay, const FPx2 &from_, bool waitAtSource_, bool keepAtTarget_, +AniData::AniData(AnimationEffect::Attribute a, int meta_, const FPx2 &to_, + int delay, const FPx2 &from_, bool waitAtSource_, bool keepAtTarget_, FullScreenEffectLockPtr fullScreenEffectLock_, bool keepAlive, PreviousWindowPixmapLockPtr previousWindowPixmapLock_) : attribute(a) - , curve(curve_) , from(from_) , to(to_) - , time(0) - , duration(ms) , meta(meta_) , startTime(AnimationEffect::clock() + delay) , fullScreenEffectLock(fullScreenEffectLock_) , waitAtSource(waitAtSource_) , keepAtTarget(keepAtTarget_) , keepAlive(keepAlive) , previousWindowPixmapLock(previousWindowPixmapLock_) { } static QString attributeString(KWin::AnimationEffect::Attribute attribute) { switch (attribute) { case KWin::AnimationEffect::Opacity: return QStringLiteral("Opacity"); case KWin::AnimationEffect::Brightness: return QStringLiteral("Brightness"); case KWin::AnimationEffect::Saturation: return QStringLiteral("Saturation"); case KWin::AnimationEffect::Scale: return QStringLiteral("Scale"); case KWin::AnimationEffect::Translation: return QStringLiteral("Translation"); case KWin::AnimationEffect::Rotation: return QStringLiteral("Rotation"); case KWin::AnimationEffect::Position: return QStringLiteral("Position"); case KWin::AnimationEffect::Size: return QStringLiteral("Size"); case KWin::AnimationEffect::Clip: return QStringLiteral("Clip"); default: return QStringLiteral(" "); } } QString AniData::debugInfo() const { return QLatin1String("Animation: ") + attributeString(attribute) + QLatin1String("\n From: ") + from.toString() + QLatin1String("\n To: ") + to.toString() + QLatin1String("\n Started: ") + QString::number(AnimationEffect::clock() - startTime) + QLatin1String("ms ago\n") + - QLatin1String( " Duration: ") + QString::number(duration) + QLatin1String("ms\n") + - QLatin1String( " Passed: ") + QString::number(time) + QLatin1String("ms\n"); + QLatin1String( " Duration: ") + QString::number(timeLine.duration().count()) + QLatin1String("ms\n") + + QLatin1String( " Passed: ") + QString::number(timeLine.elapsed().count()) + QLatin1String("ms\n"); } diff --git a/libkwineffects/anidata_p.h b/libkwineffects/anidata_p.h index 3e14980cc..0e8dc3132 100644 --- a/libkwineffects/anidata_p.h +++ b/libkwineffects/anidata_p.h @@ -1,106 +1,105 @@ /******************************************************************** 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 ANIDATA_H #define ANIDATA_H #include "kwinanimationeffect.h" #include namespace KWin { /** * Wraps effects->setActiveFullScreenEffect for the duration of it's lifespan */ class FullScreenEffectLock { public: FullScreenEffectLock(Effect *effect); ~FullScreenEffectLock(); private: Q_DISABLE_COPY(FullScreenEffectLock) }; typedef QSharedPointer FullScreenEffectLockPtr; /** * Keeps windows alive during animation after they got closed **/ class KeepAliveLock { public: KeepAliveLock(EffectWindow *w); ~KeepAliveLock(); private: EffectWindow *m_window; Q_DISABLE_COPY(KeepAliveLock) }; typedef QSharedPointer KeepAliveLockPtr; /** * References the previous window pixmap to prevent discarding. **/ class PreviousWindowPixmapLock { public: PreviousWindowPixmapLock(EffectWindow *w); ~PreviousWindowPixmapLock(); private: EffectWindow *m_window; Q_DISABLE_COPY(PreviousWindowPixmapLock) }; typedef QSharedPointer PreviousWindowPixmapLockPtr; class KWINEFFECTS_EXPORT AniData { public: AniData(); - AniData(AnimationEffect::Attribute a, int meta, int ms, const FPx2 &to, - QEasingCurve curve, int delay, const FPx2 &from, bool waitAtSource, + AniData(AnimationEffect::Attribute a, int meta, const FPx2 &to, + int delay, const FPx2 &from, bool waitAtSource, bool keepAtTarget = false, FullScreenEffectLockPtr=FullScreenEffectLockPtr(), bool keepAlive = true, PreviousWindowPixmapLockPtr previousWindowPixmapLock = {}); - inline void addTime(int t) { time += t; } + inline bool isOneDimensional() const { return from[0] == from[1] && to[0] == to[1]; } quint64 id{0}; QString debugInfo() const; AnimationEffect::Attribute attribute; - QEasingCurve curve; int customCurve; FPx2 from, to; - int time, duration; + TimeLine timeLine; uint meta; qint64 startTime; QSharedPointer fullScreenEffectLock; bool waitAtSource, keepAtTarget; bool keepAlive; KeepAliveLockPtr keepAliveLock; PreviousWindowPixmapLockPtr previousWindowPixmapLock; }; } // namespace QDebug operator<<(QDebug dbg, const KWin::AniData &a); #endif // ANIDATA_H diff --git a/libkwineffects/kwinanimationeffect.cpp b/libkwineffects/kwinanimationeffect.cpp index e512172e2..c5c5e24d5 100644 --- a/libkwineffects/kwinanimationeffect.cpp +++ b/libkwineffects/kwinanimationeffect.cpp @@ -1,985 +1,991 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2011 Thomas Lübking 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 "kwinanimationeffect.h" #include "anidata_p.h" #include #include #include #include QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2) { dbg.nospace() << fpx2[0] << "," << fpx2[1] << QString(fpx2.isValid() ? QStringLiteral(" (valid)") : QStringLiteral(" (invalid)")); return dbg.space(); } namespace KWin { QElapsedTimer AnimationEffect::s_clock; class AnimationEffectPrivate { public: AnimationEffectPrivate() { m_animated = m_damageDirty = m_animationsTouched = m_isInitialized = false; m_justEndedAnimation = 0; } AnimationEffect::AniMap m_animations; static quint64 m_animCounter; quint64 m_justEndedAnimation; // protect against cancel QWeakPointer m_fullScreenEffectLock; bool m_animated, m_damageDirty, m_needSceneRepaint, m_animationsTouched, m_isInitialized; }; } using namespace KWin; quint64 AnimationEffectPrivate::m_animCounter = 0; AnimationEffect::AnimationEffect() : d_ptr(new AnimationEffectPrivate()) { Q_D(AnimationEffect); d->m_animated = false; if (!s_clock.isValid()) s_clock.start(); /* this is the same as the QTimer::singleShot(0, SLOT(init())) kludge * defering the init and esp. the connection to the windowClosed slot */ QMetaObject::invokeMethod( this, "init", Qt::QueuedConnection ); } AnimationEffect::~AnimationEffect() { delete d_ptr; } void AnimationEffect::init() { Q_D(AnimationEffect); if (d->m_isInitialized) return; // not more than once, please d->m_isInitialized = true; /* by connecting the signal from a slot AFTER the inheriting class constructor had the chance to * connect it we can provide auto-referencing of animated and closed windows, since at the time * our slot will be called, the slot of the subclass has been (SIGNAL/SLOT connections are FIFO) * and has pot. started an animation so we have the window in our hash :) */ connect ( effects, SIGNAL(windowClosed(KWin::EffectWindow*)), SLOT(_windowClosed(KWin::EffectWindow*)) ); connect ( effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), SLOT(_windowDeleted(KWin::EffectWindow*)) ); } bool AnimationEffect::isActive() const { Q_D(const AnimationEffect); return !d->m_animations.isEmpty(); } #define RELATIVE_XY(_FIELD_) const bool relative[2] = { static_cast(metaData(Relative##_FIELD_##X, meta)), \ static_cast(metaData(Relative##_FIELD_##Y, meta)) } void AnimationEffect::validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const { if (a < NonFloatBase) { if (a == Scale) { QRect area = effects->clientArea(ScreenArea , w); if (from && from->isValid()) { RELATIVE_XY(Source); from->set(relative[0] ? (*from)[0] * area.width() / w->width() : (*from)[0], relative[1] ? (*from)[1] * area.height() / w->height() : (*from)[1]); } if (to && to->isValid()) { RELATIVE_XY(Target); to->set(relative[0] ? (*to)[0] * area.width() / w->width() : (*to)[0], relative[1] ? (*to)[1] * area.height() / w->height() : (*to)[1] ); } } else if (a == Rotation) { if (from && !from->isValid()) { setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); from->set(0.0,0.0); } if (to && !to->isValid()) { setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); to->set(0.0,0.0); } } if (from && !from->isValid()) from->set(1.0,1.0); if (to && !to->isValid()) to->set(1.0,1.0); } else if (a == Position) { QRect area = effects->clientArea(ScreenArea , w); QPoint pt = w->geometry().bottomRight(); // cannot be < 0 ;-) if (from) { if (from->isValid()) { RELATIVE_XY(Source); from->set(relative[0] ? area.x() + (*from)[0] * area.width() : (*from)[0], relative[1] ? area.y() + (*from)[1] * area.height() : (*from)[1]); } else { from->set(pt.x(), pt.y()); setMetaData(SourceAnchor, AnimationEffect::Bottom|AnimationEffect::Right, meta); } } if (to) { if (to->isValid()) { RELATIVE_XY(Target); to->set(relative[0] ? area.x() + (*to)[0] * area.width() : (*to)[0], relative[1] ? area.y() + (*to)[1] * area.height() : (*to)[1]); } else { to->set(pt.x(), pt.y()); setMetaData( TargetAnchor, AnimationEffect::Bottom|AnimationEffect::Right, meta ); } } } else if (a == Size) { QRect area = effects->clientArea(ScreenArea , w); if (from) { if (from->isValid()) { RELATIVE_XY(Source); from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], relative[1] ? (*from)[1] * area.height() : (*from)[1]); } else { from->set(w->width(), w->height()); } } if (to) { if (to->isValid()) { RELATIVE_XY(Target); to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], relative[1] ? (*to)[1] * area.height() : (*to)[1]); } else { to->set(w->width(), w->height()); } } } else if (a == Translation) { QRect area = w->rect(); if (from) { if (from->isValid()) { RELATIVE_XY(Source); from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], relative[1] ? (*from)[1] * area.height() : (*from)[1]); } else { from->set(0.0, 0.0); } } if (to) { if (to->isValid()) { RELATIVE_XY(Target); to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], relative[1] ? (*to)[1] * area.height() : (*to)[1]); } else { to->set(0.0, 0.0); } } } else if (a == Clip) { if (from && !from->isValid()) { from->set(1.0,1.0); setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); } if (to && !to->isValid()) { to->set(1.0,1.0); setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); } } else if (a == CrossFadePrevious) { if (from && !from->isValid()) { from->set(0.0); } if (to && !to->isValid()) { to->set(1.0); } } } quint64 AnimationEffect::p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget, bool fullScreenEffect, bool keepAlive) { const bool waitAtSource = from.isValid(); validate(a, meta, &from, &to, w); Q_D(AnimationEffect); if (!d->m_isInitialized) init(); // needs to ensure the window gets removed if deleted in the same event cycle if (d->m_animations.isEmpty()) { connect (effects, SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); connect (effects, SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); connect (effects, SIGNAL(windowPaddingChanged(KWin::EffectWindow*,QRect)), SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); } AniMap::iterator it = d->m_animations.find(w); if (it == d->m_animations.end()) it = d->m_animations.insert(w, QPair, QRect>(QList(), QRect())); FullScreenEffectLockPtr fullscreen; if (fullScreenEffect) { if (d->m_fullScreenEffectLock.isNull()) { fullscreen = FullScreenEffectLockPtr::create(this); d->m_fullScreenEffectLock = fullscreen.toWeakRef(); } else { fullscreen = d->m_fullScreenEffectLock.toStrongRef(); } } PreviousWindowPixmapLockPtr previousPixmap; if (a == CrossFadePrevious) { previousPixmap = PreviousWindowPixmapLockPtr::create(w); } it->first.append(AniData( a, // Attribute meta, // Metadata - ms, // Duration to, // Target - curve, // Easing curve 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 )); - quint64 ret_id = ++d->m_animCounter; - it->first.last().id = ret_id; + const quint64 ret_id = ++d->m_animCounter; + AniData &animation = it->first.last(); + animation.id = ret_id; + + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration(std::chrono::milliseconds(ms)); + animation.timeLine.setEasingCurve(curve); + animation.timeLine.setSourceRedirectMode(TimeLine::RedirectMode::Strict); + animation.timeLine.setTargetRedirectMode(TimeLine::RedirectMode::Relaxed); + it->second = QRect(); d->m_animationsTouched = true; if (delay > 0) { QTimer::singleShot(delay, this, SLOT(triggerRepaint())); const QSize &s = effects->virtualScreenSize(); if (waitAtSource) w->addLayerRepaint(0, 0, s.width(), s.height()); } else { triggerRepaint(); } return ret_id; } bool AnimationEffect::retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime) { Q_D(AnimationEffect); if (animationId == d->m_justEndedAnimation) return false; // this is just ending, do not try to retarget it for (AniMap::iterator entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); entry != mapEnd; ++entry) { for (QList::iterator anim = entry->first.begin(), animEnd = entry->first.end(); anim != animEnd; ++anim) { if (anim->id == animationId) { anim->from.set(interpolated(*anim, 0), interpolated(*anim, 1)); validate(anim->attribute, anim->meta, nullptr, &newTarget, entry.key()); anim->to.set(newTarget[0], newTarget[1]); - anim->duration = anim->time + newRemainingTime; + + anim->timeLine.setDirection(TimeLine::Forward); + anim->timeLine.setDuration(std::chrono::milliseconds(newRemainingTime)); + anim->timeLine.reset(); + return true; } } } return false; // no animation found } bool AnimationEffect::cancel(quint64 animationId) { Q_D(AnimationEffect); if (animationId == d->m_justEndedAnimation) return true; // this is just ending, do not try to cancel it but fake success for (AniMap::iterator entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); entry != mapEnd; ++entry) { for (QList::iterator anim = entry->first.begin(), animEnd = entry->first.end(); anim != animEnd; ++anim) { if (anim->id == animationId) { entry->first.erase(anim); // remove the animation if (entry->first.isEmpty()) { // no other animations on the window, release it. d->m_animations.erase(entry); } if (d->m_animations.isEmpty()) disconnectGeometryChanges(); d->m_animationsTouched = true; // could be called from animationEnded return true; } } } return false; } void AnimationEffect::prePaintScreen( ScreenPrePaintData& data, int time ) { Q_D(AnimationEffect); if (d->m_animations.isEmpty()) { effects->prePaintScreen(data, time); return; } d->m_animationsTouched = false; AniMap::iterator entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); d->m_animated = false; // short int transformed = 0; while (entry != mapEnd) { bool invalidateLayerRect = false; QList::iterator anim = entry->first.begin(), animEnd = entry->first.end(); int animCounter = 0; while (anim != animEnd) { if (anim->startTime > clock()) { if (!anim->waitAtSource) { ++anim; ++animCounter; continue; } } else { - anim->addTime(time); + anim->timeLine.update(std::chrono::milliseconds(time)); } - if (anim->time < anim->duration || anim->keepAtTarget) { + if (!anim->timeLine.done() || anim->keepAtTarget) { // if (anim->attribute != Brightness && anim->attribute != Saturation && anim->attribute != Opacity) // transformed = true; d->m_animated = true; ++anim; ++animCounter; } else { EffectWindow *oldW = entry.key(); d->m_justEndedAnimation = anim->id; animationEnded(oldW, anim->attribute, anim->meta); d->m_justEndedAnimation = 0; // NOTICE animationEnded is an external call and might have called "::animate" // as a result our iterators could now point random junk on the heap // so we've to restore the former states, ie. find our window list and animation if (d->m_animationsTouched) { d->m_animationsTouched = false; entry = d->m_animations.begin(), mapEnd = d->m_animations.end(); while (entry.key() != oldW && entry != mapEnd) ++entry; Q_ASSERT(entry != mapEnd); // usercode should not delete animations from animationEnded (not even possible atm.) anim = entry->first.begin(), animEnd = entry->first.end(); Q_ASSERT(animCounter < entry->first.count()); for (int i = 0; i < animCounter; ++i) ++anim; } anim = entry->first.erase(anim); invalidateLayerRect = d->m_damageDirty = true; animEnd = entry->first.end(); } } if (entry->first.isEmpty()) { data.paint |= entry->second; // d->m_damageDirty = true; // TODO likely no longer required entry = d->m_animations.erase(entry); mapEnd = d->m_animations.end(); } else { if (invalidateLayerRect) *const_cast(&(entry->second)) = QRect(); // invalidate ++entry; } } // janitorial... if (d->m_animations.isEmpty()) { disconnectGeometryChanges(); } effects->prePaintScreen(data, time); } static int xCoord(const QRect &r, int flag) { if (flag & AnimationEffect::Left) return r.x(); else if (flag & AnimationEffect::Right) return r.right(); else return r.x() + r.width()/2; } static int yCoord(const QRect &r, int flag) { if (flag & AnimationEffect::Top) return r.y(); else if (flag & AnimationEffect::Bottom) return r.bottom(); else return r.y() + r.height()/2; } QRect AnimationEffect::clipRect(const QRect &geo, const AniData &anim) const { QRect clip = geo; FPx2 ratio = anim.from + progress(anim) * (anim.to - anim.from); if (anim.from[0] < 1.0 || anim.to[0] < 1.0) { clip.setWidth(clip.width() * ratio[0]); } if (anim.from[1] < 1.0 || anim.to[1] < 1.0) { clip.setHeight(clip.height() * ratio[1]); } const QRect center = geo.adjusted(clip.width()/2, clip.height()/2, -(clip.width()+1)/2, -(clip.height()+1)/2 ); const int x[2] = { xCoord(center, metaData(SourceAnchor, anim.meta)), xCoord(center, metaData(TargetAnchor, anim.meta)) }; const int y[2] = { yCoord(center, metaData(SourceAnchor, anim.meta)), yCoord(center, metaData(TargetAnchor, anim.meta)) }; const QPoint d(x[0] + ratio[0]*(x[1]-x[0]), y[0] + ratio[1]*(y[1]-y[0])); clip.moveTopLeft(QPoint(d.x() - clip.width()/2, d.y() - clip.height()/2)); return clip; } void AnimationEffect::clipWindow(const EffectWindow *w, const AniData &anim, WindowQuadList &quads) const { return; const QRect geo = w->expandedGeometry(); QRect clip = AnimationEffect::clipRect(geo, anim); WindowQuadList filtered; if (clip.left() != geo.left()) { quads = quads.splitAtX(clip.left()); foreach (const WindowQuad &quad, quads) { if (quad.right() >= clip.left()) filtered << quad; } quads = filtered; filtered.clear(); } if (clip.right() != geo.right()) { quads = quads.splitAtX(clip.left()); foreach (const WindowQuad &quad, quads) { if (quad.right() <= clip.right()) filtered << quad; } quads = filtered; filtered.clear(); } if (clip.top() != geo.top()) { quads = quads.splitAtY(clip.top()); foreach (const WindowQuad &quad, quads) { if (quad.top() >= clip.top()) filtered << quad; } quads = filtered; filtered.clear(); } if (clip.bottom() != geo.bottom()) { quads = quads.splitAtY(clip.bottom()); foreach (const WindowQuad &quad, quads) { if (quad.bottom() <= clip.bottom()) filtered << quad; } quads = filtered; } } void AnimationEffect::disconnectGeometryChanges() { disconnect (effects,SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), this, SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); disconnect (effects,SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), this, SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); disconnect (effects,SIGNAL(windowPaddingChanged(KWin::EffectWindow*,QRect)), this, SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); } void AnimationEffect::prePaintWindow( EffectWindow* w, WindowPrePaintData& data, int time ) { Q_D(AnimationEffect); if ( d->m_animated ) { AniMap::const_iterator entry = d->m_animations.constFind( w ); if ( entry != d->m_animations.constEnd() ) { bool isUsed = false; bool paintDeleted = false; for (QList::const_iterator anim = entry->first.constBegin(); anim != entry->first.constEnd(); ++anim) { if (anim->startTime > clock() && !anim->waitAtSource) continue; isUsed = true; if (anim->attribute == Opacity || anim->attribute == CrossFadePrevious) data.setTranslucent(); else if (!(anim->attribute == Brightness || anim->attribute == Saturation)) { data.setTransformed(); data.mask |= PAINT_WINDOW_TRANSFORMED; if (anim->attribute == Clip) clipWindow(w, *anim, data.quads); } paintDeleted |= anim->keepAlive; } if ( isUsed ) { if ( w->isMinimized() ) w->enablePainting( EffectWindow::PAINT_DISABLED_BY_MINIMIZE ); else if ( w->isDeleted() && paintDeleted ) w->enablePainting( EffectWindow::PAINT_DISABLED_BY_DELETE ); else if ( !w->isOnCurrentDesktop() ) w->enablePainting( EffectWindow::PAINT_DISABLED_BY_DESKTOP ); // if( !w->isPaintingEnabled() && !effects->activeFullScreenEffect() ) // effects->addLayerRepaint(w->expandedGeometry()); } } } effects->prePaintWindow( w, data, time ); } static inline float geometryCompensation(int flags, float v) { if (flags & (AnimationEffect::Left|AnimationEffect::Top)) return 0.0; // no compensation required if (flags & (AnimationEffect::Right|AnimationEffect::Bottom)) return 1.0 - v; // full compensation return 0.5 * (1.0 - v); // half compensation } void AnimationEffect::paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data ) { Q_D(AnimationEffect); if ( d->m_animated ) { AniMap::const_iterator entry = d->m_animations.constFind( w ); if ( entry != d->m_animations.constEnd() ) { for ( QList::const_iterator anim = entry->first.constBegin(); anim != entry->first.constEnd(); ++anim ) { if (anim->startTime > clock() && !anim->waitAtSource) continue; switch (anim->attribute) { case Opacity: data.multiplyOpacity(interpolated(*anim)); break; case Brightness: data.multiplyBrightness(interpolated(*anim)); break; case Saturation: data.multiplySaturation(interpolated(*anim)); break; case Scale: { const QSize sz = w->geometry().size(); float f1(1.0), f2(0.0); if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { // scale x f1 = interpolated(*anim, 0); f2 = geometryCompensation( anim->meta & AnimationEffect::Horizontal, f1 ); data.translate(f2 * sz.width()); data.setXScale(data.xScale() * f1); } if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { // scale y if (!anim->isOneDimensional()) { f1 = interpolated(*anim, 1); f2 = geometryCompensation( anim->meta & AnimationEffect::Vertical, f1 ); } else if ( ((anim->meta & AnimationEffect::Vertical)>>1) != (anim->meta & AnimationEffect::Horizontal) ) f2 = geometryCompensation( anim->meta & AnimationEffect::Vertical, f1 ); data.translate(0.0, f2 * sz.height()); data.setYScale(data.yScale() * f1); } break; } case Clip: region = clipRect(w->expandedGeometry(), *anim); break; case Translation: data += QPointF(interpolated(*anim, 0), interpolated(*anim, 1)); break; case Size: { FPx2 dest = anim->from + progress(*anim) * (anim->to - anim->from); const QSize sz = w->geometry().size(); float f; if (anim->from[0] >= 0.0 && anim->to[0] >= 0.0) { // resize x f = dest[0]/sz.width(); data.translate(geometryCompensation( anim->meta & AnimationEffect::Horizontal, f ) * sz.width()); data.setXScale(data.xScale() * f); } if (anim->from[1] >= 0.0 && anim->to[1] >= 0.0) { // resize y f = dest[1]/sz.height(); data.translate(0.0, geometryCompensation( anim->meta & AnimationEffect::Vertical, f ) * sz.height()); data.setYScale(data.yScale() * f); } break; } case Position: { const QRect geo = w->geometry(); const float prgrs = progress(*anim); if ( anim->from[0] >= 0.0 && anim->to[0] >= 0.0 ) { float dest = interpolated(*anim, 0); const int x[2] = { xCoord(geo, metaData(SourceAnchor, anim->meta)), xCoord(geo, metaData(TargetAnchor, anim->meta)) }; data.translate(dest - (x[0] + prgrs*(x[1] - x[0]))); } if ( anim->from[1] >= 0.0 && anim->to[1] >= 0.0 ) { float dest = interpolated(*anim, 1); const int y[2] = { yCoord(geo, metaData(SourceAnchor, anim->meta)), yCoord(geo, metaData(TargetAnchor, anim->meta)) }; data.translate(0.0, dest - (y[0] + prgrs*(y[1] - y[0]))); } break; } case Rotation: { data.setRotationAxis((Qt::Axis)metaData(Axis, anim->meta)); const float prgrs = progress(*anim); data.setRotationAngle(anim->from[0] + prgrs*(anim->to[0] - anim->from[0])); const QRect geo = w->rect(); const uint sAnchor = metaData(SourceAnchor, anim->meta), tAnchor = metaData(TargetAnchor, anim->meta); QPointF pt(xCoord(geo, sAnchor), yCoord(geo, sAnchor)); if (tAnchor != sAnchor) { QPointF pt2(xCoord(geo, tAnchor), yCoord(geo, tAnchor)); pt += static_cast(prgrs)*(pt2 - pt); } data.setRotationOrigin(QVector3D(pt)); break; } case Generic: genericAnimation(w, data, progress(*anim), anim->meta); break; case CrossFadePrevious: data.setCrossFadeProgress(progress(*anim)); break; default: break; } } } } effects->paintWindow( w, mask, region, data ); } void AnimationEffect::postPaintScreen() { Q_D(AnimationEffect); if ( d->m_animated ) { if (d->m_damageDirty) updateLayerRepaints(); if (d->m_needSceneRepaint) { effects->addRepaintFull(); } else { AniMap::const_iterator it = d->m_animations.constBegin(), end = d->m_animations.constEnd(); for (; it != end; ++it) { bool addRepaint = false; QList::const_iterator anim = it->first.constBegin(); for (; anim != it->first.constEnd(); ++anim) { if (anim->startTime > clock()) continue; - if (anim->time < anim->duration) { + if (!anim->timeLine.done()) { addRepaint = true; break; } } if (addRepaint) { it.key()->addLayerRepaint(it->second); } } } } effects->postPaintScreen(); } float AnimationEffect::interpolated( const AniData &a, int i ) const { if (a.startTime > clock()) return a.from[i]; - if (a.time < a.duration) - return a.from[i] + a.curve.valueForProgress( ((float)a.time)/a.duration )*(a.to[i] - a.from[i]); + if (!a.timeLine.done()) + return a.from[i] + a.timeLine.value() * (a.to[i] - a.from[i]); return a.to[i]; // we're done and "waiting" at the target value } float AnimationEffect::progress( const AniData &a ) const { - if (a.startTime > clock()) - return 0.0; - if (a.time < a.duration) - return a.curve.valueForProgress( ((float)a.time)/a.duration ); - return 1.0; // we're done and "waiting" at the target value + return a.startTime < clock() ? a.timeLine.value() : 0.0; } // TODO - get this out of the header - the functionpointer usage of QEasingCurve somehow sucks ;-) // qreal AnimationEffect::qecGaussian(qreal progress) // exp(-5*(2*x-1)^2) // { // progress = 2*progress - 1; // progress *= -5*progress; // return qExp(progress); // } int AnimationEffect::metaData( MetaType type, uint meta ) { switch (type) { case SourceAnchor: return ((meta>>5) & 0x1f); case TargetAnchor: return (meta& 0x1f); case RelativeSourceX: case RelativeSourceY: case RelativeTargetX: case RelativeTargetY: { const int shift = 10 + type - RelativeSourceX; return ((meta>>shift) & 1); } case Axis: return ((meta>>10) & 3); default: return 0; } } void AnimationEffect::setMetaData( MetaType type, uint value, uint &meta ) { switch (type) { case SourceAnchor: meta &= ~(0x1f<<5); meta |= ((value & 0x1f)<<5); break; case TargetAnchor: meta &= ~(0x1f); meta |= (value & 0x1f); break; case RelativeSourceX: case RelativeSourceY: case RelativeTargetX: case RelativeTargetY: { const int shift = 10 + type - RelativeSourceX; if (value) meta |= (1<m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); entry != mapEnd; ++entry) *const_cast(&(entry->second)) = QRect(); updateLayerRepaints(); if (d->m_needSceneRepaint) { effects->addRepaintFull(); } else { AniMap::const_iterator it = d->m_animations.constBegin(), end = d->m_animations.constEnd(); for (; it != end; ++it) { it.key()->addLayerRepaint(it->second); } } } static float fixOvershoot(float f, const AniData &d, short int dir, float s = 1.1) { - switch(d.curve.type()) { + switch(d.timeLine.easingCurve().type()) { case QEasingCurve::InOutElastic: case QEasingCurve::InOutBack: return f * s; case QEasingCurve::InElastic: case QEasingCurve::OutInElastic: case QEasingCurve::OutBack: return (dir&2) ? f * s : f; case QEasingCurve::OutElastic: case QEasingCurve::InBack: return (dir&1) ? f * s : f; default: return f; } } void AnimationEffect::updateLayerRepaints() { Q_D(AnimationEffect); d->m_needSceneRepaint = false; for (AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); entry != mapEnd; ++entry) { if (!entry->second.isNull()) continue; float f[2] = {1.0, 1.0}; float t[2] = {0.0, 0.0}; bool createRegion = false; QList rects; QRect *layerRect = const_cast(&(entry->second)); for (QList::const_iterator anim = entry->first.constBegin(), animEnd = entry->first.constEnd(); anim != animEnd; ++anim) { if (anim->startTime > clock()) continue; switch (anim->attribute) { case Opacity: case Brightness: case Saturation: case CrossFadePrevious: createRegion = true; break; case Rotation: createRegion = false; *layerRect = QRect(QPoint(0, 0), effects->virtualScreenSize()); goto region_creation; // sic! no need to do anything else case Generic: d->m_needSceneRepaint = true; // we don't know whether this will change visual stacking order return; // sic! no need to do anything else case Translation: case Position: { createRegion = true; QRect r(entry.key()->geometry()); int x[2] = {0,0}; int y[2] = {0,0}; if (anim->attribute == Translation) { x[0] = anim->from[0]; x[1] = anim->to[0]; y[0] = anim->from[1]; y[1] = anim->to[1]; } else { if ( anim->from[0] >= 0.0 && anim->to[0] >= 0.0 ) { x[0] = anim->from[0] - xCoord(r, metaData(SourceAnchor, anim->meta)); x[1] = anim->to[0] - xCoord(r, metaData(TargetAnchor, anim->meta)); } if ( anim->from[1] >= 0.0 && anim->to[1] >= 0.0 ) { y[0] = anim->from[1] - yCoord(r, metaData(SourceAnchor, anim->meta)); y[1] = anim->to[1] - yCoord(r, metaData(TargetAnchor, anim->meta)); } } r = entry.key()->expandedGeometry(); rects << r.translated(x[0], y[0]) << r.translated(x[1], y[1]); break; } case Clip: createRegion = true; break; case Size: case Scale: { createRegion = true; const QSize sz = entry.key()->geometry().size(); float fx = qMax(fixOvershoot(anim->from[0], *anim, 1), fixOvershoot(anim->to[0], *anim, 2)); // float fx = qMax(interpolated(*anim,0), anim->to[0]); if (fx >= 0.0) { if (anim->attribute == Size) fx /= sz.width(); f[0] *= fx; t[0] += geometryCompensation( anim->meta & AnimationEffect::Horizontal, fx ) * sz.width(); } // float fy = qMax(interpolated(*anim,1), anim->to[1]); float fy = qMax(fixOvershoot(anim->from[1], *anim, 1), fixOvershoot(anim->to[1], *anim, 2)); if (fy >= 0.0) { if (anim->attribute == Size) fy /= sz.height(); if (!anim->isOneDimensional()) { f[1] *= fy; t[1] += geometryCompensation( anim->meta & AnimationEffect::Vertical, fy ) * sz.height(); } else if ( ((anim->meta & AnimationEffect::Vertical)>>1) != (anim->meta & AnimationEffect::Horizontal) ) { f[1] *= fx; t[1] += geometryCompensation( anim->meta & AnimationEffect::Vertical, fx ) * sz.height(); } } break; } } } region_creation: if (createRegion) { const QRect geo = entry.key()->expandedGeometry(); if (rects.isEmpty()) rects << geo; QList::const_iterator r, rEnd = rects.constEnd(); for ( r = rects.constBegin(); r != rEnd; ++r) { // transform const_cast(&(*r))->setSize(QSize(qRound(r->width()*f[0]), qRound(r->height()*f[1]))); const_cast(&(*r))->translate(t[0], t[1]); // "const_cast" - don't do that at home, kids ;-) } QRect rect = rects.at(0); if (rects.count() > 1) { for ( r = rects.constBegin() + 1; r != rEnd; ++r) // unite rect |= *r; const int dx = 110*(rect.width() - geo.width())/100 + 1 - rect.width() + geo.width(); const int dy = 110*(rect.height() - geo.height())/100 + 1 - rect.height() + geo.height(); rect.adjust(-dx,-dy,dx,dy); // fix pot. overshoot } *layerRect = rect; } } d->m_damageDirty = false; } void AnimationEffect::_expandedGeometryChanged(KWin::EffectWindow *w, const QRect &old) { Q_UNUSED(old) Q_D(AnimationEffect); AniMap::const_iterator entry = d->m_animations.constFind(w); if (entry != d->m_animations.constEnd()) { *const_cast(&(entry->second)) = QRect(); updateLayerRepaints(); if (!entry->second.isNull()) // actually got updated, ie. is in use - ensure it get's a repaint w->addLayerRepaint(entry->second); } } void AnimationEffect::_windowClosed( EffectWindow* w ) { Q_D(AnimationEffect); auto it = d->m_animations.find(w); if (it == d->m_animations.end()) { return; } KeepAliveLockPtr keepAliveLock; QList &animations = (*it).first; for (auto animationIt = animations.begin(); animationIt != animations.end(); ++animationIt) { if (!(*animationIt).keepAlive) { continue; } if (keepAliveLock.isNull()) { keepAliveLock = KeepAliveLockPtr::create(w); } (*animationIt).keepAliveLock = keepAliveLock; } } void AnimationEffect::_windowDeleted( EffectWindow* w ) { Q_D(AnimationEffect); d->m_animations.remove( w ); } QString AnimationEffect::debug(const QString &/*parameter*/) const { Q_D(const AnimationEffect); QString dbg; if (d->m_animations.isEmpty()) dbg = QStringLiteral("No window is animated"); else { AniMap::const_iterator entry = d->m_animations.constBegin(), mapEnd = d->m_animations.constEnd(); for (; entry != mapEnd; ++entry) { QString caption = entry.key()->isDeleted() ? QStringLiteral("[Deleted]") : entry.key()->caption(); if (caption.isEmpty()) caption = QStringLiteral("[Untitled]"); dbg += QLatin1String("Animating window: ") + caption + QLatin1Char('\n'); QList::const_iterator anim = entry->first.constBegin(), animEnd = entry->first.constEnd(); for (; anim != animEnd; ++anim) dbg += anim->debugInfo(); } } return dbg; } AnimationEffect::AniMap AnimationEffect::state() const { Q_D(const AnimationEffect); return d->m_animations; } #include "moc_kwinanimationeffect.cpp"