diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -139,6 +139,7 @@ target_link_libraries(testBuiltInEffectLoader Qt5::Concurrent Qt5::Test + Qt5::Qml Qt5::X11Extras KF5::Package kwineffects @@ -201,6 +202,7 @@ target_link_libraries(testPluginEffectLoader Qt5::Concurrent Qt5::Test + Qt5::Qml Qt5::X11Extras KF5::Package kwineffects diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp --- a/autotests/integration/effects/scripted_effects_test.cpp +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -34,9 +34,8 @@ #include "wayland_server.h" #include "workspace.h" -#include -#include -#include +#include +#include #include #include @@ -74,39 +73,30 @@ { Q_OBJECT public: - ScriptedEffectWithDebugSpy(); bool load(const QString &name); using AnimationEffect::state; + Q_INVOKABLE void sendTestResponse(const QString &out); //proxies triggers out from the tests + QList actions(); //returns any QActions owned by the ScriptEngine signals: void testOutput(const QString &data); }; -QScriptValue kwinEffectScriptTestOut(QScriptContext *context, QScriptEngine *engine) +void ScriptedEffectWithDebugSpy::sendTestResponse(const QString &out) { - 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(); + emit testOutput(out); } -ScriptedEffectWithDebugSpy::ScriptedEffectWithDebugSpy() - : ScriptedEffect() +QList ScriptedEffectWithDebugSpy::actions() { - QScriptValue testHookFunc = engine()->newFunction(kwinEffectScriptTestOut); - testHookFunc.setData(engine()->newQObject(this)); - engine()->globalObject().setProperty(QStringLiteral("sendTestResponse"), testHookFunc); + return findChildren(QString(), Qt::FindDirectChildrenOnly); } bool ScriptedEffectWithDebugSpy::load(const QString &name) { + auto selfContext = engine()->newQObject(this); + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); const QString path = QFINDTESTDATA("./scripts/" + name + ".js"); + engine()->globalObject().setProperty("sendTestResponse", selfContext.property("sendTestResponse")); if (!init(name, path)) { return false; } @@ -236,8 +226,8 @@ 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(effect->actions().count(), 1); + auto action = effect->actions()[0]; QCOMPARE(action->objectName(), "testShortcut"); QCOMPARE(action->text(), "Test Shortcut"); QCOMPARE(KGlobalAccel::self()->shortcut(action).first(), QKeySequence("Meta+Shift+Y")); @@ -346,8 +336,7 @@ 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(); + effect->actions()[0]->trigger(); QCOMPARE(effectOutputSpy.count(), 1); } diff --git a/autotests/test_scripted_effectloader.cpp b/autotests/test_scripted_effectloader.cpp --- a/autotests/test_scripted_effectloader.cpp +++ b/autotests/test_scripted_effectloader.cpp @@ -47,6 +47,11 @@ { } +void ScreenEdges::unreserve(ElectricBorder, QObject *) +{ +} + + void ScreenEdges::reserveTouch(ElectricBorder, QAction *) { } @@ -59,7 +64,7 @@ namespace MetaScripting { -void registration(QScriptEngine *) +void registration(QJSEngine *) { } } diff --git a/autotests/test_window_paint_data.cpp b/autotests/test_window_paint_data.cpp --- a/autotests/test_window_paint_data.cpp +++ b/autotests/test_window_paint_data.cpp @@ -64,7 +64,7 @@ virtual EffectWindow *findModal(); virtual const EffectWindowGroup *group() const; virtual bool isPaintingEnabled(); - virtual EffectWindowList mainWindows() const; + virtual EffectWindowList mainEffectWindows() const; virtual QByteArray readProperty(long int atom, long int type, int format) const; virtual void refWindow(); virtual void unrefWindow(); @@ -126,7 +126,7 @@ return true; } -EffectWindowList MockEffectWindow::mainWindows() const +EffectWindowList MockEffectWindow::mainEffectWindows() const { return EffectWindowList(); } diff --git a/effects.h b/effects.h --- a/effects.h +++ b/effects.h @@ -371,7 +371,7 @@ void deleteProperty(long atom) const override; EffectWindow* findModal() override; - EffectWindowList mainWindows() const override; + EffectWindowList mainEffectWindows() const override; WindowQuadList buildQuads(bool force = false) const override; diff --git a/effects.cpp b/effects.cpp --- a/effects.cpp +++ b/effects.cpp @@ -1709,7 +1709,7 @@ return ret; } -EffectWindowList EffectWindowImpl::mainWindows() const +EffectWindowList EffectWindowImpl::mainEffectWindows() const { if (dynamic_cast(toplevel)) { return getMainWindows(toplevel); diff --git a/effects/slideback/slideback.cpp b/effects/slideback/slideback.cpp --- a/effects/slideback/slideback.cpp +++ b/effects/slideback/slideback.cpp @@ -332,7 +332,7 @@ { QRect modalGroupGeometry = w->geometry(); if (w->isModal()) { - foreach (EffectWindow * modalWindow, w->mainWindows()) { + foreach (EffectWindow * modalWindow, w->mainEffectWindows()) { modalGroupGeometry = modalGroupGeometry.united(getModalGroupGeometry(modalWindow)); } } diff --git a/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h --- a/libkwineffects/kwineffects.h +++ b/libkwineffects/kwineffects.h @@ -812,7 +812,7 @@ * if used manually. */ Q_PROPERTY(qreal animationTimeFactor READ animationTimeFactor) - Q_PROPERTY(QList< KWin::EffectWindow* > stackingOrder READ stackingOrder) + Q_PROPERTY(QList< QObject* > stackingOrder READ stackingOrderProxy) /** * Whether window decorations use the alpha channel. **/ @@ -1322,6 +1322,9 @@ **/ virtual KSharedConfigPtr inputConfig() const = 0; + + QList stackingOrderProxy() const; + Q_SIGNALS: /** * Signal emitted when the current desktop changed. @@ -1336,6 +1339,8 @@ * @deprecated */ void desktopChanged(int oldDesktop, int newDesktop); + ///@internal + void desktopChangedCompat(int oldDesktop, int newDesktop); /** * Signal emitted when a window moved to another desktop * NOTICE that this does NOT imply that the desktop has changed @@ -2169,7 +2174,9 @@ bool isModal() const; Q_SCRIPTABLE virtual KWin::EffectWindow* findModal() = 0; - Q_SCRIPTABLE virtual QList mainWindows() const = 0; + Q_SCRIPTABLE QList mainWindows() const; + virtual QList mainEffectWindows() const = 0; + /** * Returns whether the window should be excluded from window switching effects. diff --git a/libkwineffects/kwineffects.cpp b/libkwineffects/kwineffects.cpp --- a/libkwineffects/kwineffects.cpp +++ b/libkwineffects/kwineffects.cpp @@ -743,6 +743,7 @@ if (compositing_type == NoCompositing) return; KWin::effects = this; + connect(this, QOverload::of(&EffectsHandler::desktopChanged), this, &EffectsHandler::desktopChangedCompat); } EffectsHandler::~EffectsHandler() @@ -762,6 +763,18 @@ return compositing_type & OpenGLCompositing; } +QList EffectsHandler::stackingOrderProxy() const +{ + QList list; + const auto &sourceList(stackingOrder()); + list.reserve(sourceList.size()); + for(auto w: sourceList) { + list.append(w); + } + return list; +} + + EffectsHandler* effects = nullptr; @@ -992,6 +1005,17 @@ return d->managed; } +QList EffectWindow::mainWindows() const +{ + QList list; + const auto &sourceList(mainEffectWindows()); + list.reserve(sourceList.size()); + for(auto w: sourceList) { + list.append(w); + } + return list; +} + //**************************************** // EffectWindowGroup diff --git a/scripting/scriptedeffect.h b/scripting/scriptedeffect.h --- a/scripting/scriptedeffect.h +++ b/scripting/scriptedeffect.h @@ -3,6 +3,7 @@ This file is part of the KDE project. Copyright (C) 2012 Martin Gräßlin + 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 @@ -23,13 +24,15 @@ #include +#include + class KConfigLoader; class KPluginMetaData; -class QScriptEngine; -class QScriptValue; +class QJSEngine; namespace KWin { + class KWIN_EXPORT ScriptedEffect : public KWin::AnimationEffect { Q_OBJECT @@ -56,19 +59,22 @@ enum EasingCurve { GaussianCurve = 128 }; + static ScriptedEffect *create(const QString &effectName, const QString &pathToScript, int chainPosition); + static ScriptedEffect *create(const KPluginMetaData &effect); + ~ScriptedEffect() override; + static bool supported(); + const QString &scriptFile() const { return m_scriptFile; } - virtual void reconfigure(ReconfigureFlags flags); + + void reconfigure(ReconfigureFlags flags) override; + int requestedEffectChainPosition() const override { return m_chainPosition; } QString activeConfig() const; void setActiveConfig(const QString &name); - static ScriptedEffect *create(const QString &effectName, const QString &pathToScript, int chainPosition); - static ScriptedEffect *create(const KPluginMetaData &effect); - static bool supported(); - virtual ~ScriptedEffect(); /** * Whether another effect has grabbed the @p w with the given @p grabRole. * @param w The window to check @@ -83,47 +89,60 @@ * @returns The config value if present **/ Q_SCRIPTABLE QVariant readConfig(const QString &key, const QVariant defaultValue = QVariant()); - void registerShortcut(QAction *a, QScriptValue callback); - const QHash &shortcutCallbacks() const { - return m_shortcutCallbacks; - } - QHash > &screenEdgeCallbacks() { + Q_SCRIPTABLE bool registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback); + + Q_SCRIPTABLE int displayWidth() const; + Q_SCRIPTABLE int displayHeight() const; + Q_SCRIPTABLE int animationTime(int defaultTime) const; + + Q_SCRIPTABLE bool registerScreenEdge(int edge, const QJSValue &callback); + Q_SCRIPTABLE bool unregisterScreenEdge(int edge); + Q_SCRIPTABLE bool registerTouchScreenEdge(int edge, const QJSValue &callback); + Q_SCRIPTABLE bool unregisterTouchScreenEdge(int edge); + + QHash > &screenEdgeCallbacks() { return m_screenEdgeCallbacks; } +public Q_SLOTS: + int animate(KWin::EffectWindow *w, Attribute a, int ms, const QJSValue &to, const QJSValue &from = QJSValue(), uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0); + QJSValue animate(const QJSValue &object); - bool registerTouchScreenCallback(int edge, QScriptValue callback); - bool unregisterTouchScreenCallback(int edge); + int set(KWin::EffectWindow *w, Attribute a, int ms, const QJSValue &to, const QJSValue &from = QJSValue(), uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0); + QJSValue set(const QJSValue &object); -public Q_SLOTS: - //curve should be of type QEasingCurve::type or ScriptedEffect::EasingCurve - quint64 animate(KWin::EffectWindow *w, Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from = KWin::FPx2(), uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0); - quint64 set(KWin::EffectWindow *w, Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from = KWin::FPx2(), uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0); - bool retarget(quint64 animationId, KWin::FPx2 newTarget, int newRemainingTime = -1); - bool cancel(quint64 animationId) { return AnimationEffect::cancel(animationId); } - virtual bool borderActivated(ElectricBorder border); + bool retarget(int animationId, const QJSValue &newTarget, int newRemainingTime = -1); + bool retarget(const QList &animationIds, const QJSValue &newTarget, int newRemainingTime = -1); + + bool cancel(int animationId); + bool cancel(const QList &animationIds); + + bool borderActivated(ElectricBorder border) override; Q_SIGNALS: /** * Signal emitted whenever the effect's config changed. **/ void configChanged(); - void animationEnded(KWin::EffectWindow *w, quint64 animationId); + void animationEnded(KWin::EffectWindow *w, int animationId); protected: ScriptedEffect(); - QScriptEngine *engine() const; + QJSEngine *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: - QScriptEngine *m_engine; + enum class AnimationPersistence { + Animate, + AnimateAndRetain + }; + //wrapper round animateGlobal/setGlobal that parses the animations blob. + QJSValue startAnimation(const QJSValue &object, AnimationPersistence peristence); + QJSValue createError(const QString &errorMessage); + QJSEngine *m_engine; QString m_effectName; QString m_scriptFile; - QHash m_shortcutCallbacks; - QHash > m_screenEdgeCallbacks; + QHash > m_screenEdgeCallbacks; KConfigLoader *m_config; int m_chainPosition; QHash m_touchScreenEdgeCallbacks; diff --git a/scripting/scriptedeffect.cpp b/scripting/scriptedeffect.cpp --- a/scripting/scriptedeffect.cpp +++ b/scripting/scriptedeffect.cpp @@ -3,6 +3,7 @@ This file is part of the KDE project. Copyright (C) 2012 Martin Gräßlin + 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 @@ -31,408 +32,87 @@ #include // Qt #include -#include -#include +#include #include -typedef KWin::EffectWindow* KEffectWindowRef; - Q_DECLARE_METATYPE(KSharedConfigPtr) namespace KWin { -QScriptValue kwinEffectScriptPrint(QScriptContext *context, QScriptEngine *engine) -{ - ScriptedEffect *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()); - } - qCDebug(KWIN_SCRIPTING) << script->scriptFile() << ":" << result; - - return engine->undefinedValue(); -} - -QScriptValue kwinEffectScriptAnimationTime(QScriptContext *context, QScriptEngine *engine) -{ - if (context->argumentCount() != 1) { - return engine->undefinedValue(); - } - if (!context->argument(0).isNumber()) { - return engine->undefinedValue(); - } - return Effect::animationTime(context->argument(0).toInteger()); -} - -QScriptValue kwinEffectDisplayWidth(QScriptContext *context, QScriptEngine *engine) -{ - Q_UNUSED(context) - Q_UNUSED(engine) - return screens()->displaySize().width(); -} - -QScriptValue kwinEffectDisplayHeight(QScriptContext *context, QScriptEngine *engine) -{ - Q_UNUSED(context) - Q_UNUSED(engine) - return screens()->displaySize().height(); -} - -QScriptValue kwinScriptGlobalShortcut(QScriptContext *context, QScriptEngine *engine) -{ - return globalShortcut(context, engine); -} - -QScriptValue kwinScriptScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return registerScreenEdge(context, engine); -} - -QScriptValue kwinRegisterTouchScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return registerTouchScreenEdge(context, engine); -} - -QScriptValue kwinUnregisterTouchScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return unregisterTouchScreenEdge(context, engine); -} - struct AnimationSettings { enum { Type = 1<<0, Curve = 1<<1, Delay = 1<<2, Duration = 1<<3 }; AnimationEffect::Attribute type; QEasingCurve::Type curve; - FPx2 from; - FPx2 to; + QJSValue from; //should be a FPx2 + QJSValue to; int delay; uint duration; uint set; uint metaData; }; -AnimationSettings animationSettingsFromObject(QScriptValue &object) +AnimationSettings animationSettingsFromObject(const QJSValue &object) { AnimationSettings settings; settings.set = 0; settings.metaData = 0; - settings.to = qscriptvalue_cast(object.property(QStringLiteral("to"))); - settings.from = qscriptvalue_cast(object.property(QStringLiteral("from"))); + settings.to = object.property(QStringLiteral("to")); + settings.from = object.property(QStringLiteral("from")); - QScriptValue duration = object.property(QStringLiteral("duration")); - if (duration.isValid() && duration.isNumber()) { - settings.duration = duration.toUInt32(); + QJSValue duration = object.property(QStringLiteral("duration")); + if (duration.isNumber()) { + settings.duration = duration.toUInt(); settings.set |= AnimationSettings::Duration; } else { settings.duration = 0; } - QScriptValue delay = object.property(QStringLiteral("delay")); - if (delay.isValid() && delay.isNumber()) { - settings.delay = delay.toInt32(); + QJSValue delay = object.property(QStringLiteral("delay")); + if (delay.isNumber()) { + settings.delay = delay.toInt(); settings.set |= AnimationSettings::Delay; } else { settings.delay = 0; } - QScriptValue curve = object.property(QStringLiteral("curve")); - if (curve.isValid() && curve.isNumber()) { - settings.curve = static_cast(curve.toInt32()); + QJSValue curve = object.property(QStringLiteral("curve")); + if (curve.isNumber()) { + settings.curve = static_cast(curve.toInt()); settings.set |= AnimationSettings::Curve; } else { settings.curve = QEasingCurve::Linear; } - QScriptValue type = object.property(QStringLiteral("type")); - if (type.isValid() && type.isNumber()) { - settings.type = static_cast(type.toInt32()); + QJSValue type = object.property(QStringLiteral("type")); + if (type.isNumber()) { + settings.type = static_cast(type.toInt()); settings.set |= AnimationSettings::Type; } else { settings.type = static_cast(-1); } - - return settings; -} - -QList animationSettings(QScriptContext *context, ScriptedEffect *effect, EffectWindow **window) -{ - QList settings; - if (!effect) { - context->throwError(QScriptContext::ReferenceError, QStringLiteral("Internal Scripted KWin Effect error")); - return settings; - } - if (context->argumentCount() != 1) { - context->throwError(QScriptContext::SyntaxError, QStringLiteral("Exactly one argument expected")); - return settings; - } - if (!context->argument(0).isObject()) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Argument needs to be an object")); - return settings; - } - QScriptValue object = context->argument(0); - QScriptValue windowProperty = object.property(QStringLiteral("window")); - if (!windowProperty.isValid() || !windowProperty.isObject()) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Window property missing in animation options")); - return settings; - } - *window = qobject_cast(windowProperty.toQObject()); - - settings << animationSettingsFromObject(object); // global - - QScriptValue animations = object.property(QStringLiteral("animations")); // array - if (animations.isValid()) { - if (!animations.isArray()) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Animations provided but not an array")); - settings.clear(); - return settings; - } - const int length = static_cast(animations.property(QStringLiteral("length")).toInteger()); - for (int i=0; ithrowError(QScriptContext::TypeError, QStringLiteral("Type property missing in animation options")); - continue; - } - if (!(set & AnimationSettings::Duration)) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Duration property missing in animation options")); - continue; - } - // Complete local animations from global settings - if (!(s.set & AnimationSettings::Duration)) { - s.duration = settings.at(0).duration; - } - if (!(s.set & AnimationSettings::Curve)) { - s.curve = settings.at(0).curve; - } - if (!(s.set & AnimationSettings::Delay)) { - s.delay = settings.at(0).delay; - } - - s.metaData = 0; - typedef QMap MetaTypeMap; - static MetaTypeMap metaTypes({ - {AnimationEffect::SourceAnchor, QStringLiteral("sourceAnchor")}, - {AnimationEffect::TargetAnchor, QStringLiteral("targetAnchor")}, - {AnimationEffect::RelativeSourceX, QStringLiteral("relativeSourceX")}, - {AnimationEffect::RelativeSourceY, QStringLiteral("relativeSourceY")}, - {AnimationEffect::RelativeTargetX, QStringLiteral("relativeTargetX")}, - {AnimationEffect::RelativeTargetY, QStringLiteral("relativeTargetY")}, - {AnimationEffect::Axis, QStringLiteral("axis")} - }); - - for (MetaTypeMap::const_iterator it = metaTypes.constBegin(), - end = metaTypes.constEnd(); it != end; ++it) { - QScriptValue metaVal = value.property(*it); - if (metaVal.isValid() && metaVal.isNumber()) { - AnimationEffect::setMetaData(it.key(), metaVal.toInt32(), s.metaData); - } - } - - settings << s; - } - } - } - - if (settings.count() == 1) { - const uint set = settings.at(0).set; - if (!(set & AnimationSettings::Type)) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Type property missing in animation options")); - settings.clear(); - } - if (!(set & AnimationSettings::Duration)) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Duration property missing in animation options")); - settings.clear(); - } - } else if (!(settings.at(0).set & AnimationSettings::Type)) { // invalid global - settings.removeAt(0); // -> get rid of it, only used to complete the others - } - return settings; } -QScriptValue kwinEffectAnimate(QScriptContext *context, QScriptEngine *engine) -{ - ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); - EffectWindow *window; - QList settings = animationSettings(context, effect, &window); - if (settings.empty()) { - context->throwError(QScriptContext::TypeError, QStringLiteral("No animations provided")); - return engine->undefinedValue(); - } - if (!window) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Window property does not contain an EffectWindow")); - return engine->undefinedValue(); - } - - QScriptValue array = engine->newArray(settings.length()); - int i = 0; - foreach (const AnimationSettings &setting, settings) { - array.setProperty(i, (uint)effect->animate(window, - setting.type, - setting.duration, - setting.to, - setting.from, - setting.metaData, - setting.curve, - setting.delay)); - ++i; - } - return array; -} - -QScriptValue kwinEffectSet(QScriptContext *context, QScriptEngine *engine) -{ - ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); - - EffectWindow *window; - QList settings = animationSettings(context, effect, &window); - if (settings.empty()) { - context->throwError(QScriptContext::TypeError, QStringLiteral("No animations provided")); - return engine->undefinedValue(); - } - if (!window) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Window property does not contain an EffectWindow")); - return engine->undefinedValue(); - } - - QList animIds; - foreach (const AnimationSettings &setting, settings) { - animIds << QVariant(effect->set(window, - setting.type, - setting.duration, - setting.to, - setting.from, - setting.metaData, - setting.curve, - setting.delay)); - } - - return engine->newVariant(animIds); -} - -QList animations(const QVariant &v, bool *ok) -{ - QList animIds; - *ok = false; - if (v.isValid()) { - quint64 animId = v.toULongLong(ok); - if (*ok) - animIds << animId; - } - if (!*ok) { // may still be a variantlist of variants being quint64 - QList list = v.toList(); - if (!list.isEmpty()) { - foreach (const QVariant &vv, list) { - quint64 animId = vv.toULongLong(ok); - if (*ok) - animIds << animId; - } - *ok = !animIds.isEmpty(); - } - } - return animIds; -} - -QScriptValue fpx2ToScriptValue(QScriptEngine *eng, const KWin::FPx2 &fpx2) -{ - QScriptValue val = eng->newObject(); - val.setProperty(QStringLiteral("value1"), fpx2[0]); - val.setProperty(QStringLiteral("value2"), fpx2[1]); - return val; -} - -void fpx2FromScriptValue(const QScriptValue &value, KWin::FPx2 &fpx2) +KWin::FPx2 fpx2FromScriptValue(const QJSValue &value) { if (value.isNull()) { - fpx2 = FPx2(); - return; + return FPx2(); } if (value.isNumber()) { - fpx2 = FPx2(value.toNumber()); - return; + return FPx2(value.toNumber()); } if (value.isObject()) { - QScriptValue value1 = value.property(QStringLiteral("value1")); - QScriptValue value2 = value.property(QStringLiteral("value2")); - if (!value1.isValid() || !value2.isValid() || !value1.isNumber() || !value2.isNumber()) { + QJSValue value1 = value.property(QStringLiteral("value1")); + QJSValue value2 = value.property(QStringLiteral("value2")); + if (!value1.isNumber() || !value2.isNumber()) { qCDebug(KWIN_SCRIPTING) << "Cannot cast scripted FPx2 to C++"; - fpx2 = FPx2(); - return; + return FPx2(); } - fpx2 = FPx2(value1.toNumber(), value2.toNumber()); + return FPx2(value1.toNumber(), value2.toNumber()); } -} - -QScriptValue kwinEffectRetarget(QScriptContext *context, QScriptEngine *engine) -{ - ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); - if (context->argumentCount() < 2 || context->argumentCount() > 3) { - context->throwError(QScriptContext::SyntaxError, QStringLiteral("2 or 3 arguments expected")); - return engine->undefinedValue(); - } - QVariant v = context->argument(0).toVariant(); - bool ok = false; - QList animIds = animations(v, &ok); - if (!ok) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Argument needs to be one or several quint64")); - return engine->undefinedValue(); - } - FPx2 target; - fpx2FromScriptValue(context->argument(1), target); - - ok = false; - const int remainingTime = context->argumentCount() == 3 ? context->argument(2).toVariant().toInt() : -1; - foreach (const quint64 &animId, animIds) { - ok = effect->retarget(animId, target, remainingTime); - if (!ok) { - break; - } - } - - return QScriptValue(ok); -} - -QScriptValue kwinEffectCancel(QScriptContext *context, QScriptEngine *engine) -{ - ScriptedEffect *effect = qobject_cast(context->callee().data().toQObject()); - if (context->argumentCount() != 1) { - context->throwError(QScriptContext::SyntaxError, QStringLiteral("Exactly one argument expected")); - return engine->undefinedValue(); - } - QVariant v = context->argument(0).toVariant(); - bool ok = false; - QList animIds = animations(v, &ok); - if (!ok) { - context->throwError(QScriptContext::TypeError, QStringLiteral("Argument needs to be one or several quint64")); - return engine->undefinedValue(); - } - foreach (const quint64 &animId, animIds) { - ok |= engine->newVariant(effect->cancel(animId)).toBool(); - } - - return engine->newVariant(ok); -} - -QScriptValue effectWindowToScriptValue(QScriptEngine *eng, const KEffectWindowRef &window) -{ - return eng->newQObject(window, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeChildObjects | QScriptEngine::ExcludeDeleteLater | QScriptEngine::PreferExistingWrapperObject); -} - -void effectWindowFromScriptValue(const QScriptValue &value, EffectWindow* &window) -{ - window = qobject_cast(value.toQObject()); + return FPx2(); } ScriptedEffect *ScriptedEffect::create(const KPluginMetaData &effect) @@ -470,25 +150,26 @@ ScriptedEffect::ScriptedEffect() : AnimationEffect() - , m_engine(new QScriptEngine(this)) + , m_engine(new QJSEngine(this)) , m_scriptFile(QString()) , m_config(nullptr) , m_chainPosition(0) { - connect(m_engine, SIGNAL(signalHandlerException(QScriptValue)), SLOT(signalHandlerException(QScriptValue))); } ScriptedEffect::~ScriptedEffect() { } bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript) { + qRegisterMetaType(); QFile scriptFile(pathToScript); if (!scriptFile.open(QIODevice::ReadOnly)) { qCDebug(KWIN_SCRIPTING) << "Could not open script file: " << pathToScript; return false; } + m_engine->installExtensions(QJSEngine::ConsoleExtension); m_effectName = effectName; m_scriptFile = pathToScript; @@ -501,109 +182,253 @@ m_config->load(); } - QScriptValue effectsObject = m_engine->newQObject(effects, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater); - m_engine->globalObject().setProperty(QStringLiteral("effects"), effectsObject, QScriptValue::Undeletable); + QJSValue effectsObject = m_engine->newQObject(effects); + QQmlEngine::setObjectOwnership(effects, QQmlEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("effects"), effectsObject); + + //desktopChanged is overloaded, which is problematic + //old code exposed the signal also with parameters. QJSEngine does not so we have to fake it + effectsObject.setProperty("desktopChanged(int,int)", effectsObject.property("desktopChangedCompat")); + + QJSValue selfWrapper = m_engine->newQObject(this); + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("Effect"), m_engine->newQMetaObject(&ScriptedEffect::staticMetaObject)); #ifndef KWIN_UNIT_TEST m_engine->globalObject().setProperty(QStringLiteral("KWin"), m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject)); #endif + m_engine->globalObject().setProperty(QStringLiteral("QEasingCurve"), m_engine->newQMetaObject(&QEasingCurve::staticMetaObject)); - m_engine->globalObject().setProperty(QStringLiteral("effect"), m_engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater), QScriptValue::Undeletable); - MetaScripting::registration(m_engine); - qScriptRegisterMetaType(m_engine, effectWindowToScriptValue, effectWindowFromScriptValue); - qScriptRegisterMetaType(m_engine, fpx2ToScriptValue, fpx2FromScriptValue); - qScriptRegisterSequenceMetaType >(m_engine); - // add our print - QScriptValue printFunc = m_engine->newFunction(kwinEffectScriptPrint); - printFunc.setData(m_engine->newQObject(this)); - m_engine->globalObject().setProperty(QStringLiteral("print"), printFunc); - // add our animationTime - QScriptValue animationTimeFunc = m_engine->newFunction(kwinEffectScriptAnimationTime); - animationTimeFunc.setData(m_engine->newQObject(this)); - m_engine->globalObject().setProperty(QStringLiteral("animationTime"), animationTimeFunc); - // add displayWidth and displayHeight - QScriptValue displayWidthFunc = m_engine->newFunction(kwinEffectDisplayWidth); - m_engine->globalObject().setProperty(QStringLiteral("displayWidth"), displayWidthFunc); - QScriptValue displayHeightFunc = m_engine->newFunction(kwinEffectDisplayHeight); - m_engine->globalObject().setProperty(QStringLiteral("displayHeight"), displayHeightFunc); - // add global Shortcut - registerGlobalShortcutFunction(this, m_engine, kwinScriptGlobalShortcut); - registerScreenEdgeFunction(this, m_engine, kwinScriptScreenEdge); - registerTouchScreenEdgeFunction(this, m_engine, kwinRegisterTouchScreenEdge); - unregisterTouchScreenEdgeFunction(this, m_engine, kwinUnregisterTouchScreenEdge); - // add the animate method - QScriptValue animateFunc = m_engine->newFunction(kwinEffectAnimate); - animateFunc.setData(m_engine->newQObject(this)); - m_engine->globalObject().setProperty(QStringLiteral("animate"), animateFunc); - - // and the set variant - QScriptValue setFunc = m_engine->newFunction(kwinEffectSet); - setFunc.setData(m_engine->newQObject(this)); - m_engine->globalObject().setProperty(QStringLiteral("set"), setFunc); - - // retarget - QScriptValue retargetFunc = m_engine->newFunction(kwinEffectRetarget); - retargetFunc.setData(m_engine->newQObject(this)); - m_engine->globalObject().setProperty(QStringLiteral("retarget"), retargetFunc); - - // cancel... - QScriptValue cancelFunc = m_engine->newFunction(kwinEffectCancel); - cancelFunc.setData(m_engine->newQObject(this)); - m_engine->globalObject().setProperty(QStringLiteral("cancel"), cancelFunc); - - QScriptValue ret = m_engine->evaluate(QString::fromUtf8(scriptFile.readAll())); + + m_engine->globalObject().setProperty("effect", selfWrapper); + + //expose functions at the root level for compatibility + m_engine->globalObject().setProperty("displayWidth", selfWrapper.property("displayWidth")); + m_engine->globalObject().setProperty("displayHeight", selfWrapper.property("displayHeight")); + m_engine->globalObject().setProperty("animationTime", selfWrapper.property("animationTime")); + + m_engine->globalObject().setProperty("registerShortcut", selfWrapper.property("registerShortcut")); + m_engine->globalObject().setProperty("registerScreenEdge", selfWrapper.property("registerScreenEdge")); + m_engine->globalObject().setProperty("unregisterScreenEdge", selfWrapper.property("unregisterScreenEdge")); + m_engine->globalObject().setProperty("registerTouchScreenEdge", selfWrapper.property("registerTouchScreenEdge")); + m_engine->globalObject().setProperty("unregisterTouchScreenEdge", selfWrapper.property("unregisterTouchScreenEdge")); + + m_engine->globalObject().setProperty("animate", selfWrapper.property("animate")); + m_engine->globalObject().setProperty("set", selfWrapper.property("set")); + m_engine->globalObject().setProperty("retarget", selfWrapper.property("retarget")); + m_engine->globalObject().setProperty("cancel", selfWrapper.property("cancel")); + + QJSValue ret = m_engine->evaluate(QString::fromUtf8(scriptFile.readAll())); if (ret.isError()) { - signalHandlerException(ret); + qCWarning(KWIN_SCRIPTING) << "KWin Effect script encountered an error at [Line " << ret.property("lineNumber").toString() << "]"; + qCWarning(KWIN_SCRIPTING) << "Message: " << ret.property("message").toString(); + qCWarning(KWIN_SCRIPTING) << ": " << ret.toString(); return false; } scriptFile.close(); return true; } -void ScriptedEffect::animationEnded(KWin::EffectWindow *w, Attribute a, uint meta) +int ScriptedEffect::displayHeight() const { - AnimationEffect::animationEnded(w, a, meta); - emit animationEnded(w, 0); + return screens()->displaySize().height(); } -void ScriptedEffect::signalHandlerException(const QScriptValue &value) +int ScriptedEffect::animationTime(int defaultTime) const { - if (value.isError()) { - qCDebug(KWIN_SCRIPTING) << "KWin Effect script encountered an error at [Line " << m_engine->uncaughtExceptionLineNumber() << "]"; - qCDebug(KWIN_SCRIPTING) << "Message: " << value.toString(); + return Effect::animationTime(defaultTime); +} - QScriptValueIterator iter(value); - while (iter.hasNext()) { - iter.next(); - qCDebug(KWIN_SCRIPTING) << " " << iter.name() << ": " << iter.value().toString(); - } - } +int ScriptedEffect::displayWidth() const +{ + return screens()->displaySize().width(); } -quint64 ScriptedEffect::animate(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from, uint metaData, int curve, int delay) +void ScriptedEffect::animationEnded(KWin::EffectWindow *w, Attribute a, uint meta) +{ + AnimationEffect::animationEnded(w, a, meta); + emit animationEnded(w, 0); +} + +int ScriptedEffect::animate(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve, int delay) { QEasingCurve qec; if (curve < QEasingCurve::Custom) qec.setType(static_cast(curve)); else if (curve == GaussianCurve) qec.setCustomType(qecGaussian); - return AnimationEffect::animate(w, a, metaData, ms, to, qec, delay, from); + return AnimationEffect::animate(w, a, metaData, ms, fpx2FromScriptValue(to), qec, delay, fpx2FromScriptValue(from)); } -quint64 ScriptedEffect::set(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, KWin::FPx2 to, KWin::FPx2 from, uint metaData, int curve, int delay) +int ScriptedEffect::set(KWin::EffectWindow* w, KWin::AnimationEffect::Attribute a, int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve, int delay) { QEasingCurve qec; if (curve < QEasingCurve::Custom) qec.setType(static_cast(curve)); else if (curve == GaussianCurve) qec.setCustomType(qecGaussian); - return AnimationEffect::set(w, a, metaData, ms, to, qec, delay, from); + return AnimationEffect::set(w, a, metaData, ms, fpx2FromScriptValue(to), qec, delay, fpx2FromScriptValue(from)); } -bool ScriptedEffect::retarget(quint64 animationId, KWin::FPx2 newTarget, int newRemainingTime) +bool ScriptedEffect::retarget(int animationId, const QJSValue &newTarget, int newRemainingTime) { - return AnimationEffect::retarget(animationId, newTarget, newRemainingTime); + return AnimationEffect::retarget(animationId, fpx2FromScriptValue(newTarget), newRemainingTime); +} + +bool ScriptedEffect::retarget(const QList &animationIds, const QJSValue &newTarget, int newRemainingTime) +{ + bool ok = false; + for (int animationId : animationIds) { + ok = retarget(animationId, newTarget, newRemainingTime); + if (!ok) { + break; + } + } + return ok; +} + +QJSValue ScriptedEffect::animate(const QJSValue &args) +{ + return startAnimation(args, AnimationPersistence::Animate); +} + +QJSValue ScriptedEffect::set(const QJSValue &object) +{ + return startAnimation(object, AnimationPersistence::AnimateAndRetain); +} + +QJSValue ScriptedEffect::createError(const QString &errorMessage) { + return m_engine->evaluate(QStringLiteral("new Error('%1');").arg(errorMessage)); +} + +QJSValue ScriptedEffect::startAnimation(const QJSValue &object, AnimationPersistence persistence) +{ + QVector settings; + QJSValue windowProperty = object.property(QStringLiteral("window")); + if (!windowProperty.isObject()) { + return createError(QStringLiteral("Window property missing in animation options")); + } + auto window = qobject_cast(windowProperty.toQObject()); + if (!window) { + return createError(QStringLiteral("Window property references invalid window")); + } + + settings << animationSettingsFromObject(object); // global + + QJSValue animations = object.property(QStringLiteral("animations")); // array + if (!animations.isNull()) { + if (!animations.isArray()) { + return createError(QStringLiteral("Animations provided but not an array")); + } + const int length = static_cast(animations.property(QStringLiteral("length")).toInt()); + for (int i = 0; i < length; ++i) { + QJSValue value = animations.property(QString::number(i)); + if (value.isObject()) { + AnimationSettings s = animationSettingsFromObject(value); + const uint set = s.set | settings.at(0).set; + // Catch show stoppers (incompletable animation) + if (!(set & AnimationSettings::Type)) { + return createError(QStringLiteral("Type property missing in animation options")); + } + if (!(set & AnimationSettings::Duration)) { + return createError(QStringLiteral("Duration property missing in animation options")); + } + // Complete local animations from global settings + if (!(s.set & AnimationSettings::Duration)) { + s.duration = settings.at(0).duration; + } + if (!(s.set & AnimationSettings::Curve)) { + s.curve = settings.at(0).curve; + } + if (!(s.set & AnimationSettings::Delay)) { + s.delay = settings.at(0).delay; + } + + s.metaData = 0; + typedef QMap MetaTypeMap; + static MetaTypeMap metaTypes({ + {AnimationEffect::SourceAnchor, QStringLiteral("sourceAnchor")}, + {AnimationEffect::TargetAnchor, QStringLiteral("targetAnchor")}, + {AnimationEffect::RelativeSourceX, QStringLiteral("relativeSourceX")}, + {AnimationEffect::RelativeSourceY, QStringLiteral("relativeSourceY")}, + {AnimationEffect::RelativeTargetX, QStringLiteral("relativeTargetX")}, + {AnimationEffect::RelativeTargetY, QStringLiteral("relativeTargetY")}, + {AnimationEffect::Axis, QStringLiteral("axis")} + }); + + for (auto it = metaTypes.constBegin(), + end = metaTypes.constEnd(); it != end; ++it) { + QJSValue metaVal = value.property(*it); + if (metaVal.isNumber()) { + AnimationEffect::setMetaData(it.key(), metaVal.toInt(), s.metaData); + } + } + + settings << s; + } + } + } + + if (settings.count() == 1) { + const uint set = settings.at(0).set; + if (!(set & AnimationSettings::Type)) { + return createError(QStringLiteral("Type property missing in animation options")); + } + if (!(set & AnimationSettings::Duration)) { + return createError(QStringLiteral("Duration property missing in animation options")); + } + } else if (!(settings.at(0).set & AnimationSettings::Type)) { // invalid global + settings.removeAt(0); // -> get rid of it, only used to complete the others + } + + if (settings.isEmpty()) { + return createError(QStringLiteral("No animations provided")); + } + + QJSValue array = m_engine->newArray(settings.length()); + for (int i = 0; i < settings.count(); i++) { + const AnimationSettings &setting = settings[i]; + int animationId; + if (persistence == AnimationPersistence::AnimateAndRetain) { + animationId = set(window, + setting.type, + setting.duration, + setting.to, + setting.from, + setting.metaData, + setting.curve, + setting.delay); + } else { + animationId = animate(window, + setting.type, + setting.duration, + setting.to, + setting.from, + setting.metaData, + setting.curve, + setting.delay); + } + array.setProperty(i, animationId); + } + return array; +} + +bool ScriptedEffect::cancel(int animationId) +{ + return AnimationEffect::cancel(animationId); +} + +bool ScriptedEffect::cancel(const QList &animationIds) +{ + bool ok = false; + for (int animationId : animationIds) { + ok = cancel(animationId); + if (!ok) { + break; + } + } + return ok; } bool ScriptedEffect::isGrabbed(EffectWindow* w, ScriptedEffect::DataRole grabRole) @@ -625,20 +450,34 @@ emit configChanged(); } -void ScriptedEffect::registerShortcut(QAction *a, QScriptValue callback) +bool ScriptedEffect::registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback) { - m_shortcutCallbacks.insert(a, callback); - connect(a, SIGNAL(triggered(bool)), SLOT(globalShortcutTriggered())); -} + QAction *a = new QAction(this); + a->setObjectName(objectName); + a->setText(text); + const QKeySequence shortcut = QKeySequence(keySequence); + KGlobalAccel::self()->setShortcut(a, QList() << shortcut); + input()->registerShortcut(shortcut, a); -void ScriptedEffect::globalShortcutTriggered() -{ - callGlobalShortcutCallback(this, sender()); + connect(a, &QAction::triggered, this, [this, a, callback]() { + QJSValue c(callback); + + QJSValue actionObject = m_engine->newQObject(a); + QQmlEngine::setObjectOwnership(a, QQmlEngine::CppOwnership); + c.call(QJSValueList({actionObject})); + }); + return true; } bool ScriptedEffect::borderActivated(ElectricBorder edge) { - screenEdgeActivated(this, edge); + auto it = screenEdgeCallbacks().constFind(edge); + if (it != screenEdgeCallbacks().constEnd()) { + for (const QJSValue &value : it.value()) { + QJSValue callback(value); + callback.call(); + } + } return true; } @@ -650,24 +489,49 @@ return m_config->property(key); } -bool ScriptedEffect::registerTouchScreenCallback(int edge, QScriptValue callback) +bool ScriptedEffect::registerScreenEdge(int edge, const QJSValue &callback) +{ + auto it = screenEdgeCallbacks().find(edge); + if (it == screenEdgeCallbacks().end()) { + // not yet registered + ScreenEdges::self()->reserve(static_cast(edge), this, "borderActivated"); + screenEdgeCallbacks().insert(edge, QList({callback})); + } else { + it->append(callback); + } + return true; +} + +bool ScriptedEffect::unregisterScreenEdge(int edge) +{ + auto it = screenEdgeCallbacks().find(edge); + if (it == screenEdgeCallbacks().end()) { + //not previously registered + return false; + } + ScreenEdges::self()->unreserve(static_cast(edge), this); + screenEdgeCallbacks().erase(it); + return true; +} + +bool ScriptedEffect::registerTouchScreenEdge(int edge, const QJSValue &callback) { if (m_touchScreenEdgeCallbacks.constFind(edge) != m_touchScreenEdgeCallbacks.constEnd()) { return false; } QAction *action = new QAction(this); connect(action, &QAction::triggered, this, - [callback] { - QScriptValue invoke(callback); - invoke.call(); - } + [callback] { + QJSValue invoke(callback); + invoke.call(); + } ); ScreenEdges::self()->reserveTouch(KWin::ElectricBorder(edge), action); m_touchScreenEdgeCallbacks.insert(edge, action); return true; } -bool ScriptedEffect::unregisterTouchScreenCallback(int edge) +bool ScriptedEffect::unregisterTouchScreenEdge(int edge) { auto it = m_touchScreenEdgeCallbacks.find(edge); if (it == m_touchScreenEdgeCallbacks.end()) { @@ -678,7 +542,7 @@ return true; } -QScriptEngine *ScriptedEffect::engine() const +QJSEngine *ScriptedEffect::engine() const { return m_engine; }