diff --git a/autotests/integration/effects/CMakeLists.txt b/autotests/integration/effects/CMakeLists.txt --- a/autotests/integration/effects/CMakeLists.txt +++ b/autotests/integration/effects/CMakeLists.txt @@ -5,3 +5,4 @@ endif() integrationTest(NAME testFade SRCS fade_test.cpp) integrationTest(WAYLAND_ONLY NAME testEffectWindowGeometry SRCS windowgeometry_test.cpp) +integrationTest(NAME testScriptedEffects SRCS scripted_effects_test.cpp) diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -0,0 +1,340 @@ +/******************************************************************** +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 "kwin_wayland_test.h" +#include "composite.h" +#include "cursor.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" +#include "virtualdesktops.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; +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 effectHandlerTest(); + void effectContextTest(); + void shortcutsTest(); + void animationTest_data(); + void animationTest(); + void screenEdgeTest(); + void screenEdgeTouchTest(); +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) +{ + ScriptedEffectWithDebugSpy *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; + } + + //don't ask. + 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(); + 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(); + EffectsHandlerImpl *e = static_cast(effects); + while (!e->loadedEffects().isEmpty()) { + const QString effect = e->loadedEffects().first(); + e->unloadEffect(effect); + QVERIFY(!e->isEffectLoaded(effect)); + } +} + +void ScriptedEffectsTest::effectHandlerTest() +{ + //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() == 1 || effectOutputSpy.wait()); + QCOMPARE(effectOutputSpy.last().first(), expected); + effectOutputSpy.clear(); + }; + QVERIFY(effect->load("effectsHandler")); + + //trigger windowAdded signal + + // create a window + using namespace KWayland::Client; + Surface *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); + + waitFor("windowAdded - Window 1"); + + //windowMinimsed + c->minimize(); + waitFor("windowMinimized - Window 1"); + + c->unminimize(); + waitFor("windowUnminimized - Window 1"); + + surface->deleteLater(); + waitFor("windowClosed - Window 1"); + + //desktop management + KWin::VirtualDesktopManager::self()->setCurrent(2); + waitFor("desktopChanged - 1 2"); +} + +void ScriptedEffectsTest::effectContextTest() +{ + //this tests misc non-objects exposed to the script engine: animationTime, displaySize + 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::shortcutsTest() +{ + //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); + auto 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::animationTest_data() +{ + QTest::addColumn("file"); + QTest::addColumn("animationCount"); + + QTest::newRow("single") << "animationTest" << 1; + QTest::newRow("multi") << "animationTestMulti" << 2; +} + +void ScriptedEffectsTest::animationTest() +{ + //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; + Surface *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); + + { + 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].duration, 100); + QCOMPARE(animationsForWindow[0].to, FPx2(1.4)); + QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); + QCOMPARE(animationsForWindow[0].keepAtTarget, false); + if (animationCount == 2) { + QCOMPARE(animationsForWindow[1].duration, 100); + 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); + { + auto state = effect->state(); + QCOMPARE(state.count(), 1); + const auto &animationsForWindow = state.first().first; + QCOMPARE(animationsForWindow.count(), animationCount); + QCOMPARE(animationsForWindow[0].duration, 200); + 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); + QCOMPARE(animationsForWindow[1].to, FPx2(1.5)); + QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); + QCOMPARE(animationsForWindow[1].keepAtTarget, false); + } + } + c->setMinimized(false); + { + auto state = effect->state(); + QCOMPARE(state.count(), 0); + } +} + +void ScriptedEffectsTest::screenEdgeTest() +{ + 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::screenEdgeTouchTest() +{ + 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); +} + +WAYLANDTEST_MAIN(ScriptedEffectsTest) +#include "scripted_effects_test.moc" diff --git a/autotests/integration/effects/scripts/animationTest.js b/autotests/integration/effects/scripts/animationTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/animationTest.js @@ -0,0 +1,12 @@ +effects.windowAdded.connect(function(w) { + w.anim1 = effect.animate(w, Effect.Scale, 100, 1.4, 0.2); + sendTestResponse(typeof(w.anim1) == "number"); +}); + +effects.windowUnminimized.connect(function(w) { + cancel(w.anim1); +}); + +effects.windowMinimized.connect(function(w) { + retarget(w.anim1, 1.5, 200); +}); diff --git a/autotests/integration/effects/scripts/animationTestMulti.js b/autotests/integration/effects/scripts/animationTestMulti.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/animationTestMulti.js @@ -0,0 +1,24 @@ +effects.windowAdded.connect(function(w) { + w.anim1 = animate({ + window: w, + duration: 100, + animations: [{ + type: Effect.Scale, + curve: Effect.GaussianCurve, + to: 1.4 + }, { + type: Effect.Opacity, + curve: Effect.GaussianCurve, + to: 0.0 + }] + }); + sendTestResponse(typeof(w.anim1) == "object" && Array.isArray(w.anim1)); +}); + +effects.windowUnminimized.connect(function(w) { + cancel(w.anim1); +}); + +effects.windowMinimized.connect(function(w) { + retarget(w.anim1, 1.5, 200); +}); diff --git a/autotests/integration/effects/scripts/effectContext.js b/autotests/integration/effects/scripts/effectContext.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/effectContext.js @@ -0,0 +1,6 @@ +sendTestResponse(displayWidth() + "x" + displayHeight()); +sendTestResponse(animationTime(100)); + +//test enums for Effect / QEasingCurve +sendTestResponse(Effect.Saturation) +sendTestResponse(QEasingCurve.Linear) diff --git a/autotests/integration/effects/scripts/effectsHandler.js b/autotests/integration/effects/scripts/effectsHandler.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/effectsHandler.js @@ -0,0 +1,15 @@ +effects.windowAdded.connect(function(window) { + sendTestResponse("windowAdded - " + window.caption); +}); +effects.windowClosed.connect(function(window) { + sendTestResponse("windowClosed - " + window.caption); +}); +effects.windowMinimized.connect(function(window) { + sendTestResponse("windowMinimized - " + window.caption); +}); +effects.windowUnminimized.connect(function(window) { + sendTestResponse("windowUnminimized - " + window.caption); +}); +effects['desktopChanged(int,int)'].connect(function(old, current) { + sendTestResponse("desktopChanged - " + old + " " + current); +}); diff --git a/autotests/integration/effects/scripts/screenEdgeTest.js b/autotests/integration/effects/scripts/screenEdgeTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/screenEdgeTest.js @@ -0,0 +1,3 @@ +registerScreenEdge(1, function() { + sendTestResponse("triggered"); +}); diff --git a/autotests/integration/effects/scripts/screenEdgeTouchTest.js b/autotests/integration/effects/scripts/screenEdgeTouchTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/screenEdgeTouchTest.js @@ -0,0 +1,3 @@ +registerTouchScreenEdge(1, function() { + sendTestResponse("triggered"); +}); diff --git a/autotests/integration/effects/scripts/shortcutsTest.js b/autotests/integration/effects/scripts/shortcutsTest.js new file mode 100644 --- /dev/null +++ b/autotests/integration/effects/scripts/shortcutsTest.js @@ -0,0 +1,3 @@ +registerShortcut("testShortcut", "Test Shortcut", "Meta+Shift+Y", function() { + sendTestResponse("shortcutTriggered"); +}); diff --git a/libkwineffects/anidata_p.h b/libkwineffects/anidata_p.h --- a/libkwineffects/anidata_p.h +++ b/libkwineffects/anidata_p.h @@ -27,7 +27,7 @@ namespace KWin { -class AniData { +class KWINEFFECTS_EXPORT AniData { public: AniData(); AniData(AnimationEffect::Attribute a, int meta, int ms, const FPx2 &to, diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h --- a/libkwineffects/kwinanimationeffect.h +++ b/libkwineffects/kwinanimationeffect.h @@ -97,6 +97,8 @@ Q_ENUMS(Attribute) Q_ENUMS(MetaType) 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 }; enum Attribute { @@ -205,6 +207,9 @@ 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 + 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 ); QRect clipRect(const QRect &windowRect, const AniData&) const; @@ -222,7 +227,6 @@ void _expandedGeometryChanged(KWin::EffectWindow *w, const QRect &old); private: static QElapsedTimer s_clock; - typedef QMap< EffectWindow*, QPair, QRect> > AniMap; AnimationEffectPrivate * const d_ptr; Q_DECLARE_PRIVATE(AnimationEffect) }; diff --git a/libkwineffects/kwinanimationeffect.cpp b/libkwineffects/kwinanimationeffect.cpp --- a/libkwineffects/kwinanimationeffect.cpp +++ b/libkwineffects/kwinanimationeffect.cpp @@ -948,5 +948,11 @@ return dbg; } +AnimationEffect::AniMap AnimationEffect::state() const +{ + Q_D(const AnimationEffect); + return d->m_animations; +} + #include "moc_kwinanimationeffect.cpp" diff --git a/scripting/scriptedeffect.h b/scripting/scriptedeffect.h --- a/scripting/scriptedeffect.h +++ b/scripting/scriptedeffect.h @@ -109,14 +109,15 @@ void animationEnded(KWin::EffectWindow *w, quint64 animationId); protected: + ScriptedEffect(); + QScriptEngine *engine() const; + bool init(const QString &effectName, const QString &pathToScript); void animationEnded(KWin::EffectWindow *w, Attribute a, uint meta); private Q_SLOTS: void signalHandlerException(const QScriptValue &value); void globalShortcutTriggered(); private: - ScriptedEffect(); - bool init(const QString &effectName, const QString &pathToScript); QScriptEngine *m_engine; QString m_effectName; QString m_scriptFile; diff --git a/scripting/scriptedeffect.cpp b/scripting/scriptedeffect.cpp --- a/scripting/scriptedeffect.cpp +++ b/scripting/scriptedeffect.cpp @@ -678,4 +678,9 @@ return true; } +QScriptEngine *ScriptedEffect::engine() const +{ + return m_engine; +} + } // namespace