/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2012 Martin Gräßlin SPDX-FileCopyrightText: 2018 David Edmundson SPDX-License-Identifier: GPL-2.0-or-later */ #include "scriptedeffect.h" #include "opengl/glshader.h" #include "opengl/glshadermanager.h" #include "scripting_logging.h" #include "workspace_wrapper.h" #include "core/output.h" #include "effect/effecthandler.h" #include "input.h" #include "screenedge.h" #include "workspace.h" // KDE #include #include #include #include // Qt #include #include #include #include #include #include Q_DECLARE_METATYPE(KSharedConfigPtr) namespace KWin { struct AnimationSettings { enum { Type = 1 << 0, Curve = 1 << 1, Delay = 1 << 2, Duration = 1 << 3, FullScreen = 1 << 4, KeepAlive = 1 << 5, FrozenTime = 1 << 6 }; AnimationEffect::Attribute type; QEasingCurve::Type curve; QJSValue from; QJSValue to; int delay; qint64 frozenTime; uint duration; uint set; uint metaData; bool fullScreenEffect; bool keepAlive; std::optional shader; }; AnimationSettings animationSettingsFromObject(const QJSValue &object) { AnimationSettings settings; settings.set = 0; settings.metaData = 0; settings.to = object.property(QStringLiteral("to")); settings.from = object.property(QStringLiteral("from")); const QJSValue duration = object.property(QStringLiteral("duration")); if (duration.isNumber()) { settings.duration = duration.toUInt(); settings.set |= AnimationSettings::Duration; } else { settings.duration = 0; } const QJSValue delay = object.property(QStringLiteral("delay")); if (delay.isNumber()) { settings.delay = delay.toInt(); settings.set |= AnimationSettings::Delay; } else { settings.delay = 0; } const QJSValue curve = object.property(QStringLiteral("curve")); if (curve.isNumber()) { settings.curve = static_cast(curve.toInt()); settings.set |= AnimationSettings::Curve; } else { settings.curve = QEasingCurve::Linear; } const 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); } const QJSValue isFullScreen = object.property(QStringLiteral("fullScreen")); if (isFullScreen.isBool()) { settings.fullScreenEffect = isFullScreen.toBool(); settings.set |= AnimationSettings::FullScreen; } else { settings.fullScreenEffect = false; } const QJSValue keepAlive = object.property(QStringLiteral("keepAlive")); if (keepAlive.isBool()) { settings.keepAlive = keepAlive.toBool(); settings.set |= AnimationSettings::KeepAlive; } else { settings.keepAlive = true; } const QJSValue frozenTime = object.property(QStringLiteral("frozenTime")); if (frozenTime.isNumber()) { settings.frozenTime = frozenTime.toInt(); settings.set |= AnimationSettings::FrozenTime; } else { settings.frozenTime = -1; } if (const auto shader = object.property(QStringLiteral("fragmentShader")); shader.isNumber()) { settings.shader = shader.toUInt(); } return settings; } static KWin::FPx2 fpx2FromScriptValue(const QJSValue &value) { if (value.isNull()) { return FPx2(); } if (value.isNumber()) { return FPx2(value.toNumber()); } if (value.isObject()) { const QJSValue value1 = value.property(QStringLiteral("value1")); const QJSValue value2 = value.property(QStringLiteral("value2")); if (!value1.isNumber() || !value2.isNumber()) { qCDebug(KWIN_SCRIPTING) << "Cannot cast scripted FPx2 to C++"; return FPx2(); } return FPx2(value1.toNumber(), value2.toNumber()); } return FPx2(); } ScriptedEffect *ScriptedEffect::create(const KPluginMetaData &effect) { const QString name = effect.pluginId(); const QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + name + QLatin1String("/contents/code/main.js")); if (scriptFile.isEmpty()) { qCDebug(KWIN_SCRIPTING) << "Could not locate effect script" << name; return nullptr; } return ScriptedEffect::create(name, scriptFile, effect.value(QStringLiteral("X-KDE-Ordering"), 0), effect.value(QStringLiteral("X-KWin-Exclusive-Category"))); } ScriptedEffect *ScriptedEffect::create(const QString &effectName, const QString &pathToScript, int chainPosition, const QString &exclusiveCategory) { ScriptedEffect *effect = new ScriptedEffect(); effect->m_exclusiveCategory = exclusiveCategory; if (!effect->init(effectName, pathToScript)) { delete effect; return nullptr; } effect->m_chainPosition = chainPosition; return effect; } bool ScriptedEffect::supported() { return effects->animationsSupported(); } ScriptedEffect::ScriptedEffect() : AnimationEffect() , m_engine(new QJSEngine(this)) , m_scriptFile(QString()) , m_config(nullptr) , m_chainPosition(0) { Q_ASSERT(effects); connect(effects, &EffectsHandler::activeFullScreenEffectChanged, this, [this]() { Effect *fullScreenEffect = effects->activeFullScreenEffect(); if (fullScreenEffect == m_activeFullScreenEffect) { return; } if (m_activeFullScreenEffect == this || fullScreenEffect == this) { Q_EMIT isActiveFullScreenEffectChanged(); } m_activeFullScreenEffect = fullScreenEffect; }); } ScriptedEffect::~ScriptedEffect() = default; bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript) { qRegisterMetaType(); qRegisterMetaType>(); QFile scriptFile(pathToScript); if (!scriptFile.open(QIODevice::ReadOnly)) { qCDebug(KWIN_SCRIPTING) << "Could not open script file: " << pathToScript; return false; } m_effectName = effectName; m_scriptFile = pathToScript; // does the effect contain an KConfigXT file? const QString kconfigXTFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/config/main.xml")); if (!kconfigXTFile.isNull()) { KConfigGroup cg = QCoreApplication::instance()->property("config").value()->group(QStringLiteral("Effect-%1").arg(m_effectName)); QFile xmlFile(kconfigXTFile); m_config = new KConfigLoader(cg, &xmlFile, this); m_config->load(); } m_engine->installExtensions(QJSEngine::ConsoleExtension); QJSValue globalObject = m_engine->globalObject(); QJSValue effectsObject = m_engine->newQObject(effects); QQmlEngine::setObjectOwnership(effects, QQmlEngine::CppOwnership); globalObject.setProperty(QStringLiteral("effects"), effectsObject); QJSValue selfObject = m_engine->newQObject(this); QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); globalObject.setProperty(QStringLiteral("effect"), selfObject); globalObject.setProperty(QStringLiteral("Effect"), m_engine->newQMetaObject(&ScriptedEffect::staticMetaObject)); globalObject.setProperty(QStringLiteral("KWin"), m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject)); globalObject.setProperty(QStringLiteral("Globals"), m_engine->newQMetaObject(&KWin::staticMetaObject)); globalObject.setProperty(QStringLiteral("QEasingCurve"), m_engine->newQMetaObject(&QEasingCurve::staticMetaObject)); static const QStringList globalProperties{ QStringLiteral("animationTime"), QStringLiteral("displayWidth"), QStringLiteral("displayHeight"), QStringLiteral("registerShortcut"), QStringLiteral("registerScreenEdge"), QStringLiteral("registerRealtimeScreenEdge"), QStringLiteral("registerTouchScreenEdge"), QStringLiteral("unregisterScreenEdge"), QStringLiteral("unregisterTouchScreenEdge"), QStringLiteral("animate"), QStringLiteral("set"), QStringLiteral("retarget"), QStringLiteral("freezeInTime"), QStringLiteral("redirect"), QStringLiteral("complete"), QStringLiteral("cancel"), QStringLiteral("addShader"), QStringLiteral("setUniform"), }; for (const QString &propertyName : globalProperties) { globalObject.setProperty(propertyName, selfObject.property(propertyName)); } const QJSValue result = m_engine->evaluate(QString::fromUtf8(scriptFile.readAll())); if (result.isError()) { qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(scriptFile.fileName()), result.property(QStringLiteral("lineNumber")).toInt(), qPrintable(result.property(QStringLiteral("message")).toString())); return false; } return true; } void ScriptedEffect::animationEnded(KWin::EffectWindow *w, Attribute a, uint meta) { AnimationEffect::animationEnded(w, a, meta); Q_EMIT animationEnded(w, 0); } QString ScriptedEffect::pluginId() const { return m_effectName; } bool ScriptedEffect::isActiveFullScreenEffect() const { return effects->activeFullScreenEffect() == this; } QList ScriptedEffect::touchEdgesForAction(const QString &action) const { QList ret; if (m_exclusiveCategory == QLatin1StringView("show-desktop") && action == QLatin1StringView("show-desktop")) { for (const auto b : {ElectricTop, ElectricRight, ElectricBottom, ElectricLeft}) { if (workspace()->screenEdges()->actionForTouchBorder(b) == ElectricActionShowDesktop) { ret.append(b); } } return ret; } else { if (!m_config) { return ret; } return m_config->property(QStringLiteral("TouchBorderActivate") + action).value>(); } } QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType animationType) { QJSValue windowProperty = object.property(QStringLiteral("window")); if (!windowProperty.isObject()) { m_engine->throwError(QStringLiteral("Window property missing in animation options")); return QJSValue(); } EffectWindow *window = qobject_cast(windowProperty.toQObject()); if (!window) { m_engine->throwError(QStringLiteral("Window property references invalid window")); return QJSValue(); } QList settings{animationSettingsFromObject(object)}; // global QJSValue animations = object.property(QStringLiteral("animations")); // array if (!animations.isUndefined()) { if (!animations.isArray()) { m_engine->throwError(QStringLiteral("Animations provided but not an array")); return QJSValue(); } 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)) { m_engine->throwError(QStringLiteral("Type property missing in animation options")); return QJSValue(); } if (!(set & AnimationSettings::Duration)) { m_engine->throwError(QStringLiteral("Duration property missing in animation options")); return QJSValue(); } // 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; } if (!(s.set & AnimationSettings::FullScreen)) { s.fullScreenEffect = settings.at(0).fullScreenEffect; } if (!(s.set & AnimationSettings::KeepAlive)) { s.keepAlive = settings.at(0).keepAlive; } if (!s.shader.has_value()) { s.shader = settings.at(0).shader; } 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); } } if (s.type == ShaderUniform && s.shader) { auto uniformProperty = value.property(QStringLiteral("uniform")).toString(); auto shader = findShader(s.shader.value()); if (!shader) { m_engine->throwError(QStringLiteral("Shader for given shaderId not found")); return {}; } if (!effects->makeOpenGLContextCurrent()) { m_engine->throwError(QStringLiteral("Failed to make OpenGL context current")); return {}; } ShaderBinder binder{shader}; s.metaData = shader->uniformLocation(uniformProperty.toUtf8().constData()); } settings << s; } } } if (settings.count() == 1) { const uint set = settings.at(0).set; if (!(set & AnimationSettings::Type)) { m_engine->throwError(QStringLiteral("Type property missing in animation options")); return QJSValue(); } if (!(set & AnimationSettings::Duration)) { m_engine->throwError(QStringLiteral("Duration property missing in animation options")); return QJSValue(); } } 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()) { m_engine->throwError(QStringLiteral("No animations provided")); return QJSValue(); } QJSValue array = m_engine->newArray(settings.length()); for (int i = 0; i < settings.count(); i++) { const AnimationSettings &setting = settings[i]; int animationId; if (animationType == AnimationType::Set) { animationId = set(window, setting.type, setting.duration, setting.to, setting.from, setting.metaData, setting.curve, setting.delay, setting.fullScreenEffect, setting.keepAlive, setting.shader ? setting.shader.value() : 0u); if (setting.frozenTime >= 0) { freezeInTime(animationId, setting.frozenTime); } } else { animationId = animate(window, setting.type, setting.duration, setting.to, setting.from, setting.metaData, setting.curve, setting.delay, setting.fullScreenEffect, setting.keepAlive, setting.shader ? setting.shader.value() : 0u); if (setting.frozenTime >= 0) { freezeInTime(animationId, setting.frozenTime); } } array.setProperty(i, animationId); } return array; } quint64 ScriptedEffect::animate(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute, int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve, int delay, bool fullScreen, bool keepAlive, uint shaderId) { QEasingCurve qec; if (curve < QEasingCurve::Custom) { qec.setType(static_cast(curve)); } else if (curve == GaussianCurve) { qec.setCustomType(qecGaussian); } return AnimationEffect::animate(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec, delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId)); } QJSValue ScriptedEffect::animate(const QJSValue &object) { return animate_helper(object, AnimationType::Animate); } quint64 ScriptedEffect::set(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute, int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve, int delay, bool fullScreen, bool keepAlive, uint shaderId) { QEasingCurve qec; if (curve < QEasingCurve::Custom) { qec.setType(static_cast(curve)); } else if (curve == GaussianCurve) { qec.setCustomType(qecGaussian); } return AnimationEffect::set(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec, delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId)); } QJSValue ScriptedEffect::set(const QJSValue &object) { return animate_helper(object, AnimationType::Set); } bool ScriptedEffect::retarget(quint64 animationId, const QJSValue &newTarget, int newRemainingTime) { return AnimationEffect::retarget(animationId, fpx2FromScriptValue(newTarget), newRemainingTime); } bool ScriptedEffect::retarget(const QList &animationIds, const QJSValue &newTarget, int newRemainingTime) { return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { return retarget(animationId, newTarget, newRemainingTime); }); } bool ScriptedEffect::freezeInTime(quint64 animationId, qint64 frozenTime) { return AnimationEffect::freezeInTime(animationId, frozenTime); } bool ScriptedEffect::freezeInTime(const QList &animationIds, qint64 frozenTime) { return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { return AnimationEffect::freezeInTime(animationId, frozenTime); }); } bool ScriptedEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) { return AnimationEffect::redirect(animationId, direction, terminationFlags); } bool ScriptedEffect::redirect(const QList &animationIds, Direction direction, TerminationFlags terminationFlags) { return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { return redirect(animationId, direction, terminationFlags); }); } bool ScriptedEffect::complete(quint64 animationId) { return AnimationEffect::complete(animationId); } bool ScriptedEffect::complete(const QList &animationIds) { return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { return complete(animationId); }); } bool ScriptedEffect::cancel(quint64 animationId) { return AnimationEffect::cancel(animationId); } bool ScriptedEffect::cancel(const QList &animationIds) { bool ret = false; for (const quint64 &animationId : animationIds) { ret |= cancel(animationId); } return ret; } bool ScriptedEffect::isGrabbed(EffectWindow *w, ScriptedEffect::DataRole grabRole) { void *e = w->data(static_cast(grabRole)).value(); if (e) { return e != this; } else { return false; } } bool ScriptedEffect::grab(EffectWindow *w, DataRole grabRole, bool force) { void *grabber = w->data(grabRole).value(); if (grabber == this) { return true; } if (grabber != nullptr && grabber != this && !force) { return false; } w->setData(grabRole, QVariant::fromValue(static_cast(this))); return true; } bool ScriptedEffect::ungrab(EffectWindow *w, DataRole grabRole) { void *grabber = w->data(grabRole).value(); if (grabber == nullptr) { return true; } if (grabber != this) { return false; } w->setData(grabRole, QVariant()); return true; } void ScriptedEffect::reconfigure(ReconfigureFlags flags) { AnimationEffect::reconfigure(flags); if (m_config) { m_config->read(); } Q_EMIT configChanged(); } void ScriptedEffect::registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback) { if (!callback.isCallable()) { m_engine->throwError(QStringLiteral("Shortcut handler must be callable")); return; } QAction *action = new QAction(this); action->setObjectName(objectName); action->setText(text); const QKeySequence shortcut = QKeySequence(keySequence); KGlobalAccel::self()->setShortcut(action, QList() << shortcut); connect(action, &QAction::triggered, this, [this, action, callback]() { QJSValue actionObject = m_engine->newQObject(action); QQmlEngine::setObjectOwnership(action, QQmlEngine::CppOwnership); QJSValue(callback).call(QJSValueList{actionObject}); }); } bool ScriptedEffect::borderActivated(ElectricBorder edge) { auto it = screenEdgeCallbacks().constFind(edge); if (it != screenEdgeCallbacks().constEnd()) { for (const QJSValue &callback : it.value()) { QJSValue(callback).call(); } } return true; } QJSValue ScriptedEffect::readConfig(const QString &key, const QJSValue &defaultValue) { if (!m_config) { return defaultValue; } return m_engine->toScriptValue(m_config->property(key)); } int ScriptedEffect::displayWidth() const { return workspace()->geometry().width(); } int ScriptedEffect::displayHeight() const { return workspace()->geometry().height(); } int ScriptedEffect::animationTime(int defaultTime) const { return Effect::animationTime(std::chrono::milliseconds(defaultTime)); } bool ScriptedEffect::registerScreenEdge(int edge, const QJSValue &callback) { if (!callback.isCallable()) { m_engine->throwError(QStringLiteral("Screen edge handler must be callable")); return false; } auto it = screenEdgeCallbacks().find(edge); if (it == screenEdgeCallbacks().end()) { // not yet registered workspace()->screenEdges()->reserve(static_cast(edge), this, "borderActivated"); screenEdgeCallbacks().insert(edge, QJSValueList{callback}); } else { it->append(callback); } return true; } bool ScriptedEffect::registerRealtimeScreenEdge(int edge, const QJSValue &callback) { if (!callback.isCallable()) { m_engine->throwError(QStringLiteral("Screen edge handler must be callable")); return false; } auto it = realtimeScreenEdgeCallbacks().find(edge); if (it == realtimeScreenEdgeCallbacks().end()) { // not yet registered realtimeScreenEdgeCallbacks().insert(edge, QJSValueList{callback}); auto *triggerAction = new QAction(this); connect(triggerAction, &QAction::triggered, this, [this, edge]() { auto it = realtimeScreenEdgeCallbacks().constFind(edge); if (it != realtimeScreenEdgeCallbacks().constEnd()) { for (const QJSValue &callback : it.value()) { QJSValue(callback).call({edge}); } } }); effects->registerRealtimeTouchBorder(static_cast(edge), triggerAction, [this](ElectricBorder border, const QPointF &deltaProgress, Output *screen) { auto it = realtimeScreenEdgeCallbacks().constFind(border); if (it != realtimeScreenEdgeCallbacks().constEnd()) { for (const QJSValue &callback : it.value()) { QJSValue delta = m_engine->newObject(); delta.setProperty("width", deltaProgress.x()); delta.setProperty("height", deltaProgress.y()); QJSValue(callback).call({border, QJSValue(delta), m_engine->newQObject(screen)}); } } }); } 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; } workspace()->screenEdges()->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; } if (!callback.isCallable()) { m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable")); return false; } QAction *action = new QAction(this); connect(action, &QAction::triggered, this, [callback]() { QJSValue(callback).call(); }); workspace()->screenEdges()->reserveTouch(KWin::ElectricBorder(edge), action); m_touchScreenEdgeCallbacks.insert(edge, action); return true; } bool ScriptedEffect::unregisterTouchScreenEdge(int edge) { auto it = m_touchScreenEdgeCallbacks.find(edge); if (it == m_touchScreenEdgeCallbacks.end()) { return false; } delete it.value(); m_touchScreenEdgeCallbacks.erase(it); return true; } QJSEngine *ScriptedEffect::engine() const { return m_engine; } uint ScriptedEffect::addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile) { if (!effects->makeOpenGLContextCurrent()) { m_engine->throwError(QStringLiteral("Failed to make OpenGL context current")); return 0; } const QString shaderDir{QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/shaders/")}; const QString fragment = fragmentShaderFile.isEmpty() ? QString{} : QStandardPaths::locate(QStandardPaths::GenericDataLocation, shaderDir + fragmentShaderFile); auto shader = ShaderManager::instance()->generateShaderFromFile(static_cast(int(traits)), {}, fragment); if (!shader->isValid()) { m_engine->throwError(QStringLiteral("Shader failed to load")); // 0 is never a valid shader identifier, it's ensured the first shader gets id 1 return 0; } const uint shaderId{m_nextShaderId}; m_nextShaderId++; m_shaders[shaderId] = std::move(shader); return shaderId; } GLShader *ScriptedEffect::findShader(uint shaderId) const { if (auto it = m_shaders.find(shaderId); it != m_shaders.end()) { return it->second.get(); } return nullptr; } void ScriptedEffect::setUniform(uint shaderId, const QString &name, const QJSValue &value) { auto shader = findShader(shaderId); if (!shader) { m_engine->throwError(QStringLiteral("Shader for given shaderId not found")); return; } if (!effects->makeOpenGLContextCurrent()) { m_engine->throwError(QStringLiteral("Failed to make OpenGL context current")); return; } auto setColorUniform = [this, shader, name](const QColor &color) { if (!color.isValid()) { return; } if (!shader->setUniform(name.toUtf8().constData(), color)) { m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); } }; ShaderBinder binder{shader}; if (value.isString()) { setColorUniform(value.toString()); } else if (value.isNumber()) { if (!shader->setUniform(name.toUtf8().constData(), float(value.toNumber()))) { m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); } } else if (value.isArray()) { const auto length = value.property(QStringLiteral("length")).toInt(); if (length == 2) { if (!shader->setUniform(name.toUtf8().constData(), QVector2D{float(value.property(0).toNumber()), float(value.property(1).toNumber())})) { m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); } } else if (length == 3) { if (!shader->setUniform(name.toUtf8().constData(), QVector3D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber())})) { m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); } } else if (length == 4) { 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())})) { m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); } } else { m_engine->throwError(QStringLiteral("Invalid number of elements in array")); } } else if (value.isVariant()) { const auto variant = value.toVariant(); setColorUniform(variant.value()); } else { m_engine->throwError(QStringLiteral("Invalid value provided for uniform")); } } } // namespace #include "moc_scriptedeffect.cpp"