File indexing completed on 2024-05-19 16:34:51

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2012 Martin Gräßlin <mgraesslin@kde.org>
0006     SPDX-FileCopyrightText: 2018 David Edmundson <davidedmundson@kde.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "scriptedeffect.h"
0012 #include "scripting_logging.h"
0013 #include "scriptingutils.h"
0014 #include "workspace_wrapper.h"
0015 
0016 #include "input.h"
0017 #include "screenedge.h"
0018 #include "workspace.h"
0019 // KDE
0020 #include <KConfigGroup>
0021 #include <KGlobalAccel>
0022 #include <KPluginMetaData>
0023 #include <kconfigloader.h>
0024 #include <kwinglutils.h>
0025 // Qt
0026 #include <QAction>
0027 #include <QFile>
0028 #include <QQmlEngine>
0029 #include <QStandardPaths>
0030 #include <QVector>
0031 
0032 #include <optional>
0033 
0034 Q_DECLARE_METATYPE(KSharedConfigPtr)
0035 
0036 namespace KWin
0037 {
0038 
0039 struct AnimationSettings
0040 {
0041     enum {
0042         Type = 1 << 0,
0043         Curve = 1 << 1,
0044         Delay = 1 << 2,
0045         Duration = 1 << 3,
0046         FullScreen = 1 << 4,
0047         KeepAlive = 1 << 5,
0048         FrozenTime = 1 << 6
0049     };
0050     AnimationEffect::Attribute type;
0051     QEasingCurve::Type curve;
0052     QJSValue from;
0053     QJSValue to;
0054     int delay;
0055     qint64 frozenTime;
0056     uint duration;
0057     uint set;
0058     uint metaData;
0059     bool fullScreenEffect;
0060     bool keepAlive;
0061     std::optional<uint> shader;
0062 };
0063 
0064 AnimationSettings animationSettingsFromObject(const QJSValue &object)
0065 {
0066     AnimationSettings settings;
0067     settings.set = 0;
0068     settings.metaData = 0;
0069 
0070     settings.to = object.property(QStringLiteral("to"));
0071     settings.from = object.property(QStringLiteral("from"));
0072 
0073     const QJSValue duration = object.property(QStringLiteral("duration"));
0074     if (duration.isNumber()) {
0075         settings.duration = duration.toUInt();
0076         settings.set |= AnimationSettings::Duration;
0077     } else {
0078         settings.duration = 0;
0079     }
0080 
0081     const QJSValue delay = object.property(QStringLiteral("delay"));
0082     if (delay.isNumber()) {
0083         settings.delay = delay.toInt();
0084         settings.set |= AnimationSettings::Delay;
0085     } else {
0086         settings.delay = 0;
0087     }
0088 
0089     const QJSValue curve = object.property(QStringLiteral("curve"));
0090     if (curve.isNumber()) {
0091         settings.curve = static_cast<QEasingCurve::Type>(curve.toInt());
0092         settings.set |= AnimationSettings::Curve;
0093     } else {
0094         settings.curve = QEasingCurve::Linear;
0095     }
0096 
0097     const QJSValue type = object.property(QStringLiteral("type"));
0098     if (type.isNumber()) {
0099         settings.type = static_cast<AnimationEffect::Attribute>(type.toInt());
0100         settings.set |= AnimationSettings::Type;
0101     } else {
0102         settings.type = static_cast<AnimationEffect::Attribute>(-1);
0103     }
0104 
0105     const QJSValue isFullScreen = object.property(QStringLiteral("fullScreen"));
0106     if (isFullScreen.isBool()) {
0107         settings.fullScreenEffect = isFullScreen.toBool();
0108         settings.set |= AnimationSettings::FullScreen;
0109     } else {
0110         settings.fullScreenEffect = false;
0111     }
0112 
0113     const QJSValue keepAlive = object.property(QStringLiteral("keepAlive"));
0114     if (keepAlive.isBool()) {
0115         settings.keepAlive = keepAlive.toBool();
0116         settings.set |= AnimationSettings::KeepAlive;
0117     } else {
0118         settings.keepAlive = true;
0119     }
0120 
0121     const QJSValue frozenTime = object.property(QStringLiteral("frozenTime"));
0122     if (frozenTime.isNumber()) {
0123         settings.frozenTime = frozenTime.toInt();
0124         settings.set |= AnimationSettings::FrozenTime;
0125     } else {
0126         settings.frozenTime = -1;
0127     }
0128 
0129     if (const auto shader = object.property(QStringLiteral("fragmentShader")); shader.isNumber()) {
0130         settings.shader = shader.toUInt();
0131     }
0132 
0133     return settings;
0134 }
0135 
0136 static KWin::FPx2 fpx2FromScriptValue(const QJSValue &value)
0137 {
0138     if (value.isNull()) {
0139         return FPx2();
0140     }
0141     if (value.isNumber()) {
0142         return FPx2(value.toNumber());
0143     }
0144     if (value.isObject()) {
0145         const QJSValue value1 = value.property(QStringLiteral("value1"));
0146         const QJSValue value2 = value.property(QStringLiteral("value2"));
0147         if (!value1.isNumber() || !value2.isNumber()) {
0148             qCDebug(KWIN_SCRIPTING) << "Cannot cast scripted FPx2 to C++";
0149             return FPx2();
0150         }
0151         return FPx2(value1.toNumber(), value2.toNumber());
0152     }
0153     return FPx2();
0154 }
0155 
0156 ScriptedEffect *ScriptedEffect::create(const KPluginMetaData &effect)
0157 {
0158     const QString name = effect.pluginId();
0159     const QString scriptName = effect.value(QStringLiteral("X-Plasma-MainScript"));
0160     if (scriptName.isEmpty()) {
0161         qCDebug(KWIN_SCRIPTING) << "X-Plasma-MainScript not set";
0162         return nullptr;
0163     }
0164     const QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0165                                                       QLatin1String("kwin/effects/") + name + QLatin1String("/contents/") + scriptName);
0166     if (scriptFile.isNull()) {
0167         qCDebug(KWIN_SCRIPTING) << "Could not locate the effect script";
0168         return nullptr;
0169     }
0170 
0171     return ScriptedEffect::create(name, scriptFile, effect.value(QStringLiteral("X-KDE-Ordering"), 0), effect.value(QStringLiteral("X-KWin-Exclusive-Category")));
0172 }
0173 
0174 ScriptedEffect *ScriptedEffect::create(const QString &effectName, const QString &pathToScript, int chainPosition, const QString &exclusiveCategory)
0175 {
0176     ScriptedEffect *effect = new ScriptedEffect();
0177     effect->m_exclusiveCategory = exclusiveCategory;
0178     if (!effect->init(effectName, pathToScript)) {
0179         delete effect;
0180         return nullptr;
0181     }
0182     effect->m_chainPosition = chainPosition;
0183 
0184     return effect;
0185 }
0186 
0187 bool ScriptedEffect::supported()
0188 {
0189     return effects->animationsSupported();
0190 }
0191 
0192 ScriptedEffect::ScriptedEffect()
0193     : AnimationEffect()
0194     , m_engine(new QJSEngine(this))
0195     , m_scriptFile(QString())
0196     , m_config(nullptr)
0197     , m_chainPosition(0)
0198 {
0199     Q_ASSERT(effects);
0200     connect(effects, &EffectsHandler::activeFullScreenEffectChanged, this, [this]() {
0201         Effect *fullScreenEffect = effects->activeFullScreenEffect();
0202         if (fullScreenEffect == m_activeFullScreenEffect) {
0203             return;
0204         }
0205         if (m_activeFullScreenEffect == this || fullScreenEffect == this) {
0206             Q_EMIT isActiveFullScreenEffectChanged();
0207         }
0208         m_activeFullScreenEffect = fullScreenEffect;
0209     });
0210 }
0211 
0212 ScriptedEffect::~ScriptedEffect() = default;
0213 
0214 bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript)
0215 {
0216     qRegisterMetaType<QJSValueList>();
0217     qRegisterMetaType<EffectWindowList>();
0218 
0219     QFile scriptFile(pathToScript);
0220     if (!scriptFile.open(QIODevice::ReadOnly)) {
0221         qCDebug(KWIN_SCRIPTING) << "Could not open script file: " << pathToScript;
0222         return false;
0223     }
0224     m_effectName = effectName;
0225     m_scriptFile = pathToScript;
0226 
0227     // does the effect contain an KConfigXT file?
0228     const QString kconfigXTFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/config/main.xml"));
0229     if (!kconfigXTFile.isNull()) {
0230         KConfigGroup cg = QCoreApplication::instance()->property("config").value<KSharedConfigPtr>()->group(QStringLiteral("Effect-%1").arg(m_effectName));
0231         QFile xmlFile(kconfigXTFile);
0232         m_config = new KConfigLoader(cg, &xmlFile, this);
0233         m_config->load();
0234     }
0235 
0236     m_engine->installExtensions(QJSEngine::ConsoleExtension);
0237 
0238     QJSValue globalObject = m_engine->globalObject();
0239 
0240     QJSValue effectsObject = m_engine->newQObject(effects);
0241     QQmlEngine::setObjectOwnership(effects, QQmlEngine::CppOwnership);
0242     globalObject.setProperty(QStringLiteral("effects"), effectsObject);
0243 
0244     QJSValue selfObject = m_engine->newQObject(this);
0245     QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
0246     globalObject.setProperty(QStringLiteral("effect"), selfObject);
0247 
0248     // desktopChanged is overloaded, which is problematic. Old code exposed the signal also
0249     // with parameters. QJSEngine does not so we have to fake it.
0250     effectsObject.setProperty(QStringLiteral("desktopChanged(int,int)"),
0251                               effectsObject.property(QStringLiteral("desktopChangedLegacy")));
0252     effectsObject.setProperty(QStringLiteral("desktopChanged(int,int,KWin::EffectWindow*)"),
0253                               effectsObject.property(QStringLiteral("desktopChanged")));
0254 
0255     globalObject.setProperty(QStringLiteral("Effect"),
0256                              m_engine->newQMetaObject(&ScriptedEffect::staticMetaObject));
0257     globalObject.setProperty(QStringLiteral("KWin"),
0258                              m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject));
0259     globalObject.setProperty(QStringLiteral("Globals"),
0260                              m_engine->newQMetaObject(&KWin::staticMetaObject));
0261     globalObject.setProperty(QStringLiteral("QEasingCurve"),
0262                              m_engine->newQMetaObject(&QEasingCurve::staticMetaObject));
0263 
0264     static const QStringList globalProperties{
0265         QStringLiteral("animationTime"),
0266         QStringLiteral("displayWidth"),
0267         QStringLiteral("displayHeight"),
0268 
0269         QStringLiteral("registerShortcut"),
0270         QStringLiteral("registerScreenEdge"),
0271         QStringLiteral("registerRealtimeScreenEdge"),
0272         QStringLiteral("registerTouchScreenEdge"),
0273         QStringLiteral("unregisterScreenEdge"),
0274         QStringLiteral("unregisterTouchScreenEdge"),
0275 
0276         QStringLiteral("animate"),
0277         QStringLiteral("set"),
0278         QStringLiteral("retarget"),
0279         QStringLiteral("freezeInTime"),
0280         QStringLiteral("redirect"),
0281         QStringLiteral("complete"),
0282         QStringLiteral("cancel"),
0283         QStringLiteral("addShader"),
0284         QStringLiteral("setUniform"),
0285     };
0286 
0287     for (const QString &propertyName : globalProperties) {
0288         globalObject.setProperty(propertyName, selfObject.property(propertyName));
0289     }
0290 
0291     const QJSValue result = m_engine->evaluate(QString::fromUtf8(scriptFile.readAll()));
0292 
0293     if (result.isError()) {
0294         qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(scriptFile.fileName()),
0295                   result.property(QStringLiteral("lineNumber")).toInt(),
0296                   qPrintable(result.property(QStringLiteral("message")).toString()));
0297         return false;
0298     }
0299 
0300     return true;
0301 }
0302 
0303 void ScriptedEffect::animationEnded(KWin::EffectWindow *w, Attribute a, uint meta)
0304 {
0305     AnimationEffect::animationEnded(w, a, meta);
0306     Q_EMIT animationEnded(w, 0);
0307 }
0308 
0309 QString ScriptedEffect::pluginId() const
0310 {
0311     return m_effectName;
0312 }
0313 
0314 bool ScriptedEffect::isActiveFullScreenEffect() const
0315 {
0316     return effects->activeFullScreenEffect() == this;
0317 }
0318 
0319 QList<int> ScriptedEffect::touchEdgesForAction(const QString &action) const
0320 {
0321     QList<int> ret;
0322     if (m_exclusiveCategory == QStringLiteral("show-desktop") && action == QStringLiteral("show-desktop")) {
0323         for (const auto b : {ElectricTop, ElectricRight, ElectricBottom, ElectricLeft}) {
0324             if (workspace()->screenEdges()->actionForTouchBorder(b) == ElectricActionShowDesktop) {
0325                 ret.append(b);
0326             }
0327         }
0328         return ret;
0329     } else {
0330         if (!m_config) {
0331             return ret;
0332         }
0333         return m_config->property(QStringLiteral("TouchBorderActivate") + action).value<QList<int>>();
0334     }
0335 }
0336 
0337 QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType animationType)
0338 {
0339     QJSValue windowProperty = object.property(QStringLiteral("window"));
0340     if (!windowProperty.isObject()) {
0341         m_engine->throwError(QStringLiteral("Window property missing in animation options"));
0342         return QJSValue();
0343     }
0344 
0345     EffectWindow *window = qobject_cast<EffectWindow *>(windowProperty.toQObject());
0346     if (!window) {
0347         m_engine->throwError(QStringLiteral("Window property references invalid window"));
0348         return QJSValue();
0349     }
0350 
0351     QVector<AnimationSettings> settings{animationSettingsFromObject(object)}; // global
0352 
0353     QJSValue animations = object.property(QStringLiteral("animations")); // array
0354     if (!animations.isUndefined()) {
0355         if (!animations.isArray()) {
0356             m_engine->throwError(QStringLiteral("Animations provided but not an array"));
0357             return QJSValue();
0358         }
0359 
0360         const int length = static_cast<int>(animations.property(QStringLiteral("length")).toInt());
0361         for (int i = 0; i < length; ++i) {
0362             QJSValue value = animations.property(QString::number(i));
0363             if (value.isObject()) {
0364                 AnimationSettings s = animationSettingsFromObject(value);
0365                 const uint set = s.set | settings.at(0).set;
0366                 // Catch show stoppers (incompletable animation)
0367                 if (!(set & AnimationSettings::Type)) {
0368                     m_engine->throwError(QStringLiteral("Type property missing in animation options"));
0369                     return QJSValue();
0370                 }
0371                 if (!(set & AnimationSettings::Duration)) {
0372                     m_engine->throwError(QStringLiteral("Duration property missing in animation options"));
0373                     return QJSValue();
0374                 }
0375                 // Complete local animations from global settings
0376                 if (!(s.set & AnimationSettings::Duration)) {
0377                     s.duration = settings.at(0).duration;
0378                 }
0379                 if (!(s.set & AnimationSettings::Curve)) {
0380                     s.curve = settings.at(0).curve;
0381                 }
0382                 if (!(s.set & AnimationSettings::Delay)) {
0383                     s.delay = settings.at(0).delay;
0384                 }
0385                 if (!(s.set & AnimationSettings::FullScreen)) {
0386                     s.fullScreenEffect = settings.at(0).fullScreenEffect;
0387                 }
0388                 if (!(s.set & AnimationSettings::KeepAlive)) {
0389                     s.keepAlive = settings.at(0).keepAlive;
0390                 }
0391                 if (!s.shader.has_value()) {
0392                     s.shader = settings.at(0).shader;
0393                 }
0394 
0395                 s.metaData = 0;
0396                 typedef QMap<AnimationEffect::MetaType, QString> MetaTypeMap;
0397                 static MetaTypeMap metaTypes({{AnimationEffect::SourceAnchor, QStringLiteral("sourceAnchor")},
0398                                               {AnimationEffect::TargetAnchor, QStringLiteral("targetAnchor")},
0399                                               {AnimationEffect::RelativeSourceX, QStringLiteral("relativeSourceX")},
0400                                               {AnimationEffect::RelativeSourceY, QStringLiteral("relativeSourceY")},
0401                                               {AnimationEffect::RelativeTargetX, QStringLiteral("relativeTargetX")},
0402                                               {AnimationEffect::RelativeTargetY, QStringLiteral("relativeTargetY")},
0403                                               {AnimationEffect::Axis, QStringLiteral("axis")}});
0404 
0405                 for (auto it = metaTypes.constBegin(),
0406                           end = metaTypes.constEnd();
0407                      it != end; ++it) {
0408                     QJSValue metaVal = value.property(*it);
0409                     if (metaVal.isNumber()) {
0410                         AnimationEffect::setMetaData(it.key(), metaVal.toInt(), s.metaData);
0411                     }
0412                 }
0413                 if (s.type == ShaderUniform && s.shader) {
0414                     auto uniformProperty = value.property(QStringLiteral("uniform")).toString();
0415                     auto shader = findShader(s.shader.value());
0416                     if (!shader) {
0417                         m_engine->throwError(QStringLiteral("Shader for given shaderId not found"));
0418                         return {};
0419                     }
0420                     if (!effects->makeOpenGLContextCurrent()) {
0421                         m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
0422                         return {};
0423                     }
0424                     ShaderBinder binder{shader};
0425                     s.metaData = shader->uniformLocation(uniformProperty.toUtf8().constData());
0426                 }
0427 
0428                 settings << s;
0429             }
0430         }
0431     }
0432 
0433     if (settings.count() == 1) {
0434         const uint set = settings.at(0).set;
0435         if (!(set & AnimationSettings::Type)) {
0436             m_engine->throwError(QStringLiteral("Type property missing in animation options"));
0437             return QJSValue();
0438         }
0439         if (!(set & AnimationSettings::Duration)) {
0440             m_engine->throwError(QStringLiteral("Duration property missing in animation options"));
0441             return QJSValue();
0442         }
0443     } else if (!(settings.at(0).set & AnimationSettings::Type)) { // invalid global
0444         settings.removeAt(0); // -> get rid of it, only used to complete the others
0445     }
0446 
0447     if (settings.isEmpty()) {
0448         m_engine->throwError(QStringLiteral("No animations provided"));
0449         return QJSValue();
0450     }
0451 
0452     QJSValue array = m_engine->newArray(settings.length());
0453     for (int i = 0; i < settings.count(); i++) {
0454         const AnimationSettings &setting = settings[i];
0455         int animationId;
0456         if (animationType == AnimationType::Set) {
0457             animationId = set(window,
0458                               setting.type,
0459                               setting.duration,
0460                               setting.to,
0461                               setting.from,
0462                               setting.metaData,
0463                               setting.curve,
0464                               setting.delay,
0465                               setting.fullScreenEffect,
0466                               setting.keepAlive,
0467                               setting.shader ? setting.shader.value() : 0u);
0468             if (setting.frozenTime >= 0) {
0469                 freezeInTime(animationId, setting.frozenTime);
0470             }
0471         } else {
0472             animationId = animate(window,
0473                                   setting.type,
0474                                   setting.duration,
0475                                   setting.to,
0476                                   setting.from,
0477                                   setting.metaData,
0478                                   setting.curve,
0479                                   setting.delay,
0480                                   setting.fullScreenEffect,
0481                                   setting.keepAlive,
0482                                   setting.shader ? setting.shader.value() : 0u);
0483             if (setting.frozenTime >= 0) {
0484                 freezeInTime(animationId, setting.frozenTime);
0485             }
0486         }
0487         array.setProperty(i, animationId);
0488     }
0489 
0490     return array;
0491 }
0492 
0493 quint64 ScriptedEffect::animate(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute,
0494                                 int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve,
0495                                 int delay, bool fullScreen, bool keepAlive, uint shaderId)
0496 {
0497     QEasingCurve qec;
0498     if (curve < QEasingCurve::Custom) {
0499         qec.setType(static_cast<QEasingCurve::Type>(curve));
0500     } else if (curve == GaussianCurve) {
0501         qec.setCustomType(qecGaussian);
0502     }
0503     return AnimationEffect::animate(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec,
0504                                     delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId));
0505 }
0506 
0507 QJSValue ScriptedEffect::animate(const QJSValue &object)
0508 {
0509     return animate_helper(object, AnimationType::Animate);
0510 }
0511 
0512 quint64 ScriptedEffect::set(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute,
0513                             int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve,
0514                             int delay, bool fullScreen, bool keepAlive, uint shaderId)
0515 {
0516     QEasingCurve qec;
0517     if (curve < QEasingCurve::Custom) {
0518         qec.setType(static_cast<QEasingCurve::Type>(curve));
0519     } else if (curve == GaussianCurve) {
0520         qec.setCustomType(qecGaussian);
0521     }
0522     return AnimationEffect::set(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec,
0523                                 delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId));
0524 }
0525 
0526 QJSValue ScriptedEffect::set(const QJSValue &object)
0527 {
0528     return animate_helper(object, AnimationType::Set);
0529 }
0530 
0531 bool ScriptedEffect::retarget(quint64 animationId, const QJSValue &newTarget, int newRemainingTime)
0532 {
0533     return AnimationEffect::retarget(animationId, fpx2FromScriptValue(newTarget), newRemainingTime);
0534 }
0535 
0536 bool ScriptedEffect::retarget(const QList<quint64> &animationIds, const QJSValue &newTarget, int newRemainingTime)
0537 {
0538     return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
0539         return retarget(animationId, newTarget, newRemainingTime);
0540     });
0541 }
0542 
0543 bool ScriptedEffect::freezeInTime(quint64 animationId, qint64 frozenTime)
0544 {
0545     return AnimationEffect::freezeInTime(animationId, frozenTime);
0546 }
0547 
0548 bool ScriptedEffect::freezeInTime(const QList<quint64> &animationIds, qint64 frozenTime)
0549 {
0550     return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
0551         return AnimationEffect::freezeInTime(animationId, frozenTime);
0552     });
0553 }
0554 
0555 bool ScriptedEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags)
0556 {
0557     return AnimationEffect::redirect(animationId, direction, terminationFlags);
0558 }
0559 
0560 bool ScriptedEffect::redirect(const QList<quint64> &animationIds, Direction direction, TerminationFlags terminationFlags)
0561 {
0562     return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
0563         return redirect(animationId, direction, terminationFlags);
0564     });
0565 }
0566 
0567 bool ScriptedEffect::complete(quint64 animationId)
0568 {
0569     return AnimationEffect::complete(animationId);
0570 }
0571 
0572 bool ScriptedEffect::complete(const QList<quint64> &animationIds)
0573 {
0574     return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
0575         return complete(animationId);
0576     });
0577 }
0578 
0579 bool ScriptedEffect::cancel(quint64 animationId)
0580 {
0581     return AnimationEffect::cancel(animationId);
0582 }
0583 
0584 bool ScriptedEffect::cancel(const QList<quint64> &animationIds)
0585 {
0586     bool ret = false;
0587     for (const quint64 &animationId : animationIds) {
0588         ret |= cancel(animationId);
0589     }
0590     return ret;
0591 }
0592 
0593 bool ScriptedEffect::isGrabbed(EffectWindow *w, ScriptedEffect::DataRole grabRole)
0594 {
0595     void *e = w->data(static_cast<KWin::DataRole>(grabRole)).value<void *>();
0596     if (e) {
0597         return e != this;
0598     } else {
0599         return false;
0600     }
0601 }
0602 
0603 bool ScriptedEffect::grab(EffectWindow *w, DataRole grabRole, bool force)
0604 {
0605     void *grabber = w->data(grabRole).value<void *>();
0606 
0607     if (grabber == this) {
0608         return true;
0609     }
0610 
0611     if (grabber != nullptr && grabber != this && !force) {
0612         return false;
0613     }
0614 
0615     w->setData(grabRole, QVariant::fromValue(static_cast<void *>(this)));
0616 
0617     return true;
0618 }
0619 
0620 bool ScriptedEffect::ungrab(EffectWindow *w, DataRole grabRole)
0621 {
0622     void *grabber = w->data(grabRole).value<void *>();
0623 
0624     if (grabber == nullptr) {
0625         return true;
0626     }
0627 
0628     if (grabber != this) {
0629         return false;
0630     }
0631 
0632     w->setData(grabRole, QVariant());
0633 
0634     return true;
0635 }
0636 
0637 void ScriptedEffect::reconfigure(ReconfigureFlags flags)
0638 {
0639     AnimationEffect::reconfigure(flags);
0640     if (m_config) {
0641         m_config->read();
0642     }
0643     Q_EMIT configChanged();
0644 }
0645 
0646 void ScriptedEffect::registerShortcut(const QString &objectName, const QString &text,
0647                                       const QString &keySequence, const QJSValue &callback)
0648 {
0649     if (!callback.isCallable()) {
0650         m_engine->throwError(QStringLiteral("Shortcut handler must be callable"));
0651         return;
0652     }
0653     QAction *action = new QAction(this);
0654     action->setObjectName(objectName);
0655     action->setText(text);
0656     const QKeySequence shortcut = QKeySequence(keySequence);
0657     KGlobalAccel::self()->setShortcut(action, QList<QKeySequence>() << shortcut);
0658     connect(action, &QAction::triggered, this, [this, action, callback]() {
0659         QJSValue actionObject = m_engine->newQObject(action);
0660         QQmlEngine::setObjectOwnership(action, QQmlEngine::CppOwnership);
0661         QJSValue(callback).call(QJSValueList{actionObject});
0662     });
0663 }
0664 
0665 bool ScriptedEffect::borderActivated(ElectricBorder edge)
0666 {
0667     auto it = screenEdgeCallbacks().constFind(edge);
0668     if (it != screenEdgeCallbacks().constEnd()) {
0669         for (const QJSValue &callback : it.value()) {
0670             QJSValue(callback).call();
0671         }
0672     }
0673     return true;
0674 }
0675 
0676 QJSValue ScriptedEffect::readConfig(const QString &key, const QJSValue &defaultValue)
0677 {
0678     if (!m_config) {
0679         return defaultValue;
0680     }
0681     return m_engine->toScriptValue(m_config->property(key));
0682 }
0683 
0684 int ScriptedEffect::displayWidth() const
0685 {
0686     return workspace()->geometry().width();
0687 }
0688 
0689 int ScriptedEffect::displayHeight() const
0690 {
0691     return workspace()->geometry().height();
0692 }
0693 
0694 int ScriptedEffect::animationTime(int defaultTime) const
0695 {
0696     return Effect::animationTime(defaultTime);
0697 }
0698 
0699 bool ScriptedEffect::registerScreenEdge(int edge, const QJSValue &callback)
0700 {
0701     if (!callback.isCallable()) {
0702         m_engine->throwError(QStringLiteral("Screen edge handler must be callable"));
0703         return false;
0704     }
0705     auto it = screenEdgeCallbacks().find(edge);
0706     if (it == screenEdgeCallbacks().end()) {
0707         // not yet registered
0708         workspace()->screenEdges()->reserve(static_cast<KWin::ElectricBorder>(edge), this, "borderActivated");
0709         screenEdgeCallbacks().insert(edge, QJSValueList{callback});
0710     } else {
0711         it->append(callback);
0712     }
0713     return true;
0714 }
0715 
0716 bool ScriptedEffect::registerRealtimeScreenEdge(int edge, const QJSValue &callback)
0717 {
0718     if (!callback.isCallable()) {
0719         m_engine->throwError(QStringLiteral("Screen edge handler must be callable"));
0720         return false;
0721     }
0722     auto it = realtimeScreenEdgeCallbacks().find(edge);
0723     if (it == realtimeScreenEdgeCallbacks().end()) {
0724         // not yet registered
0725         realtimeScreenEdgeCallbacks().insert(edge, QJSValueList{callback});
0726         auto *triggerAction = new QAction(this);
0727         connect(triggerAction, &QAction::triggered, this, [this, edge]() {
0728             auto it = realtimeScreenEdgeCallbacks().constFind(edge);
0729             if (it != realtimeScreenEdgeCallbacks().constEnd()) {
0730                 for (const QJSValue &callback : it.value()) {
0731                     QJSValue(callback).call({edge});
0732                 }
0733             }
0734         });
0735         effects->registerRealtimeTouchBorder(static_cast<KWin::ElectricBorder>(edge), triggerAction, [this](ElectricBorder border, const QPointF &deltaProgress, EffectScreen *screen) {
0736             auto it = realtimeScreenEdgeCallbacks().constFind(border);
0737             if (it != realtimeScreenEdgeCallbacks().constEnd()) {
0738                 for (const QJSValue &callback : it.value()) {
0739                     QJSValue delta = m_engine->newObject();
0740                     delta.setProperty("width", deltaProgress.x());
0741                     delta.setProperty("height", deltaProgress.y());
0742 
0743                     QJSValue(callback).call({border, QJSValue(delta), m_engine->newQObject(screen)});
0744                 }
0745             }
0746         });
0747     } else {
0748         it->append(callback);
0749     }
0750     return true;
0751 }
0752 
0753 bool ScriptedEffect::unregisterScreenEdge(int edge)
0754 {
0755     auto it = screenEdgeCallbacks().find(edge);
0756     if (it == screenEdgeCallbacks().end()) {
0757         // not previously registered
0758         return false;
0759     }
0760     workspace()->screenEdges()->unreserve(static_cast<KWin::ElectricBorder>(edge), this);
0761     screenEdgeCallbacks().erase(it);
0762     return true;
0763 }
0764 
0765 bool ScriptedEffect::registerTouchScreenEdge(int edge, const QJSValue &callback)
0766 {
0767     if (m_touchScreenEdgeCallbacks.constFind(edge) != m_touchScreenEdgeCallbacks.constEnd()) {
0768         return false;
0769     }
0770     if (!callback.isCallable()) {
0771         m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable"));
0772         return false;
0773     }
0774     QAction *action = new QAction(this);
0775     connect(action, &QAction::triggered, this, [callback]() {
0776         QJSValue(callback).call();
0777     });
0778     workspace()->screenEdges()->reserveTouch(KWin::ElectricBorder(edge), action);
0779     m_touchScreenEdgeCallbacks.insert(edge, action);
0780     return true;
0781 }
0782 
0783 bool ScriptedEffect::unregisterTouchScreenEdge(int edge)
0784 {
0785     auto it = m_touchScreenEdgeCallbacks.find(edge);
0786     if (it == m_touchScreenEdgeCallbacks.end()) {
0787         return false;
0788     }
0789     delete it.value();
0790     m_touchScreenEdgeCallbacks.erase(it);
0791     return true;
0792 }
0793 
0794 QJSEngine *ScriptedEffect::engine() const
0795 {
0796     return m_engine;
0797 }
0798 
0799 uint ScriptedEffect::addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile)
0800 {
0801     if (!effects->makeOpenGLContextCurrent()) {
0802         m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
0803         return 0;
0804     }
0805     const QString shaderDir{QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/shaders/")};
0806     const QString fragment = fragmentShaderFile.isEmpty() ? QString{} : QStandardPaths::locate(QStandardPaths::GenericDataLocation, shaderDir + fragmentShaderFile);
0807 
0808     auto shader = ShaderManager::instance()->generateShaderFromFile(static_cast<KWin::ShaderTraits>(int(traits)), {}, fragment);
0809     if (!shader->isValid()) {
0810         m_engine->throwError(QStringLiteral("Shader failed to load"));
0811         // 0 is never a valid shader identifier, it's ensured the first shader gets id 1
0812         return 0;
0813     }
0814 
0815     const uint shaderId{m_nextShaderId};
0816     m_nextShaderId++;
0817     m_shaders[shaderId] = std::move(shader);
0818     return shaderId;
0819 }
0820 
0821 GLShader *ScriptedEffect::findShader(uint shaderId) const
0822 {
0823     if (auto it = m_shaders.find(shaderId); it != m_shaders.end()) {
0824         return it->second.get();
0825     }
0826     return nullptr;
0827 }
0828 
0829 void ScriptedEffect::setUniform(uint shaderId, const QString &name, const QJSValue &value)
0830 {
0831     auto shader = findShader(shaderId);
0832     if (!shader) {
0833         m_engine->throwError(QStringLiteral("Shader for given shaderId not found"));
0834         return;
0835     }
0836     if (!effects->makeOpenGLContextCurrent()) {
0837         m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
0838         return;
0839     }
0840     auto setColorUniform = [this, shader, name] (const QColor &color)
0841     {
0842         if (!color.isValid()) {
0843             return;
0844         }
0845         if (!shader->setUniform(name.toUtf8().constData(), color)) {
0846             m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
0847         }
0848     };
0849     ShaderBinder binder{shader};
0850     if (value.isString()) {
0851         setColorUniform(value.toString());
0852     } else if (value.isNumber()) {
0853         if (!shader->setUniform(name.toUtf8().constData(), float(value.toNumber()))) {
0854             m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
0855         }
0856     } else if (value.isArray()) {
0857         const auto length = value.property(QStringLiteral("length")).toInt();
0858         if (length == 2) {
0859             if (!shader->setUniform(name.toUtf8().constData(), QVector2D{float(value.property(0).toNumber()), float(value.property(1).toNumber())})) {
0860                 m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
0861             }
0862         } else if (length == 3) {
0863             if (!shader->setUniform(name.toUtf8().constData(), QVector3D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber())})) {
0864                 m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
0865             }
0866         } else if (length == 4) {
0867             if (!shader->setUniform(name.toUtf8().constData(), QVector4D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber()), float(value.property(3).toNumber())})) {
0868                 m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
0869             }
0870         } else {
0871             m_engine->throwError(QStringLiteral("Invalid number of elements in array"));
0872         }
0873     } else if (value.isVariant()) {
0874         const auto variant = value.toVariant();
0875         setColorUniform(variant.value<QColor>());
0876     } else {
0877         m_engine->throwError(QStringLiteral("Invalid value provided for uniform"));
0878     }
0879 }
0880 
0881 } // namespace