diff --git a/examples/quick-effect/.gitignore b/examples/quick-effect/.gitignore
new file mode 100644
index 0000000000..378eac25d3
--- /dev/null
+++ b/examples/quick-effect/.gitignore
@@ -0,0 +1 @@
+build
diff --git a/examples/quick-effect/CMakeLists.txt b/examples/quick-effect/CMakeLists.txt
new file mode 100644
index 0000000000..27b5ea3f14
--- /dev/null
+++ b/examples/quick-effect/CMakeLists.txt
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: None
+# SPDX-License-Identifier: CC0-1.0
+
+cmake_minimum_required(VERSION 3.16)
+project(quick-effect)
+
+find_package(ECM 5.240 REQUIRED NO_MODULE)
+set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
+
+find_package(KF6 5.240 REQUIRED COMPONENTS
+ Package
+)
+
+kpackage_install_package(package quick-effect effects kwin)
diff --git a/examples/quick-effect/package/contents/config/main.xml b/examples/quick-effect/package/contents/config/main.xml
new file mode 100644
index 0000000000..47bbc2c975
--- /dev/null
+++ b/examples/quick-effect/package/contents/config/main.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ #ff00ff
+
+
+
diff --git a/examples/quick-effect/package/contents/config/main.xml.license b/examples/quick-effect/package/contents/config/main.xml.license
new file mode 100644
index 0000000000..cf53d2bce1
--- /dev/null
+++ b/examples/quick-effect/package/contents/config/main.xml.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: None
+SPDX-License-Identifier: CC0-1.0
diff --git a/examples/quick-effect/package/contents/ui/config.ui b/examples/quick-effect/package/contents/ui/config.ui
new file mode 100644
index 0000000000..57ed22e80c
--- /dev/null
+++ b/examples/quick-effect/package/contents/ui/config.ui
@@ -0,0 +1,39 @@
+
+
+ QuickEffectConfig
+
+
+
+ 0
+ 0
+ 455
+ 177
+
+
+
+ -
+
+
+ Background color:
+
+
+
+ -
+
+
+ false
+
+
+
+
+
+
+
+ KColorButton
+ QPushButton
+
+
+
+
+
+
diff --git a/examples/quick-effect/package/contents/ui/config.ui.license b/examples/quick-effect/package/contents/ui/config.ui.license
new file mode 100644
index 0000000000..cf53d2bce1
--- /dev/null
+++ b/examples/quick-effect/package/contents/ui/config.ui.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: None
+SPDX-License-Identifier: CC0-1.0
diff --git a/examples/quick-effect/package/contents/ui/main.qml b/examples/quick-effect/package/contents/ui/main.qml
new file mode 100644
index 0000000000..024554e921
--- /dev/null
+++ b/examples/quick-effect/package/contents/ui/main.qml
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: None
+// SPDX-License-Identifier: CC0-1.0
+
+import QtQuick
+import org.kde.kwin
+
+SceneEffect {
+ id: effect
+
+ delegate: Rectangle {
+ color: effect.configuration.BackgroundColor
+
+ Text {
+ anchors.centerIn: parent
+ text: SceneView.screen.name
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: effect.visible = false
+ }
+ }
+
+ ScreenEdgeHandler {
+ enabled: true
+ edge: ScreenEdgeHandler.TopEdge
+ onActivated: effect.visible = !effect.visible
+ }
+
+ ShortcutHandler {
+ name: "Toggle Quick Effect"
+ text: "Toggle Quick Effect"
+ sequence: "Meta+Ctrl+Q"
+ onActivated: effect.visible = !effect.visible
+ }
+
+ PinchGestureHandler {
+ direction: PinchGestureHandler.Direction.Contracting
+ fingerCount: 3
+ onActivated: effect.visible = !effect.visible
+ }
+}
diff --git a/examples/quick-effect/package/metadata.json b/examples/quick-effect/package/metadata.json
new file mode 100644
index 0000000000..5c6e4e8009
--- /dev/null
+++ b/examples/quick-effect/package/metadata.json
@@ -0,0 +1,20 @@
+{
+ "KPackageStructure": "KWin/Effect",
+ "KPlugin": {
+ "Authors": [
+ {
+ "Email": "user@example.com",
+ "Name": "Real Name"
+ }
+ ],
+ "Category": "Appearance",
+ "Description": "Quick Effect",
+ "EnabledByDefault": true,
+ "Id": "quick-effect",
+ "License": "GPL",
+ "Name": "Quick Effect"
+ },
+ "X-KDE-Ordering": 60,
+ "X-KDE-PluginKeyword": "quick-effect",
+ "X-Plasma-API": "declarativescript"
+}
diff --git a/examples/quick-effect/package/metadata.json.license b/examples/quick-effect/package/metadata.json.license
new file mode 100644
index 0000000000..cf53d2bce1
--- /dev/null
+++ b/examples/quick-effect/package/metadata.json.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: None
+SPDX-License-Identifier: CC0-1.0
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index eff5968219..3ec068e912 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -156,6 +156,7 @@ target_sources(kwin PRIVATE
scripting/gesturehandler.cpp
scripting/screenedgehandler.cpp
scripting/scriptedeffect.cpp
+ scripting/scriptedquicksceneeffect.cpp
scripting/scripting.cpp
scripting/scripting_logging.cpp
scripting/scriptingutils.cpp
@@ -212,6 +213,7 @@ target_link_libraries(kwin
PRIVATE
KF6::ConfigCore
+ KF6::ConfigQml
KF6::ConfigWidgets
KF6::CoreAddons
KF6::Crash
diff --git a/src/effectloader.cpp b/src/effectloader.cpp
index 5ee2dbd893..c14c105bb3 100644
--- a/src/effectloader.cpp
+++ b/src/effectloader.cpp
@@ -14,6 +14,8 @@
#include "libkwineffects/kwineffects.h"
#include "plugin.h"
#include "scripting/scriptedeffect.h"
+#include "scripting/scriptedquicksceneeffect.h"
+#include "scripting/scripting.h"
#include "utils/common.h"
// KDE
#include
@@ -23,6 +25,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -116,11 +120,28 @@ bool ScriptedEffectLoader::loadEffect(const KPluginMetaData &effect, LoadEffectF
qCDebug(KWIN_CORE) << "Loading flags disable effect: " << name;
return false;
}
+
if (m_loadedEffects.contains(name)) {
qCDebug(KWIN_CORE) << name << "already loaded";
return false;
}
+ const QString api = effect.value(QStringLiteral("X-Plasma-API"));
+ if (api == QLatin1String("javascript")) {
+ return loadJavascriptEffect(effect);
+ } else if (api == QLatin1String("declarativescript")) {
+ return loadDeclarativeEffect(effect);
+ } else {
+ qCWarning(KWIN_CORE, "Failed to load %s effect: invalid X-Plasma-API field: %s. "
+ "Available options are javascript, and declarativescript", qPrintable(name), qPrintable(api));
+ }
+
+ return false;
+}
+
+bool ScriptedEffectLoader::loadJavascriptEffect(const KPluginMetaData &effect)
+{
+ const QString name = effect.pluginId();
if (!ScriptedEffect::supported()) {
qCDebug(KWIN_CORE) << "Effect is not supported: " << name;
return false;
@@ -141,6 +162,44 @@ bool ScriptedEffectLoader::loadEffect(const KPluginMetaData &effect, LoadEffectF
return true;
}
+bool ScriptedEffectLoader::loadDeclarativeEffect(const KPluginMetaData &metadata)
+{
+ const QString name = metadata.pluginId();
+ const QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
+ QLatin1String("kwin/effects/") + name + QLatin1String("/contents/ui/main.qml"));
+ if (scriptFile.isNull()) {
+ qCWarning(KWIN_CORE) << "Could not locate the effect script";
+ return false;
+ }
+
+ QQmlEngine *engine = Scripting::self()->qmlEngine();
+ QQmlComponent component(engine);
+ component.loadUrl(QUrl::fromLocalFile(scriptFile));
+ if (component.isError()) {
+ qCWarning(KWIN_CORE).nospace() << "Failed to load " << scriptFile << ": " << component.errors();
+ return false;
+ }
+
+ QObject *object = component.beginCreate(engine->rootContext());
+ auto effect = qobject_cast(object);
+ if (!effect) {
+ qCDebug(KWIN_CORE) << "Could not initialize scripted effect: " << name;
+ delete object;
+ return false;
+ }
+ effect->setMetaData(metadata);
+ component.completeCreate();
+
+ connect(effect, &Effect::destroyed, this, [this, name]() {
+ m_loadedEffects.removeAll(name);
+ });
+
+ qCDebug(KWIN_CORE) << "Successfully loaded scripted effect: " << name;
+ Q_EMIT effectLoaded(effect, name);
+ m_loadedEffects << name;
+ return true;
+}
+
void ScriptedEffectLoader::queryAndLoadAll()
{
if (m_queryConnection) {
diff --git a/src/effectloader.h b/src/effectloader.h
index 1fdb802041..ddf690c894 100644
--- a/src/effectloader.h
+++ b/src/effectloader.h
@@ -290,6 +290,9 @@ public:
private:
QList findAllEffects() const;
KPluginMetaData findEffect(const QString &name) const;
+ bool loadJavascriptEffect(const KPluginMetaData &effect);
+ bool loadDeclarativeEffect(const KPluginMetaData &effect);
+
QStringList m_loadedEffects;
EffectLoadQueue *m_queue;
QMetaObject::Connection m_queryConnection;
diff --git a/src/libkwineffects/kwinquickeffect.cpp b/src/libkwineffects/kwinquickeffect.cpp
index 389e370e94..9769d6beef 100644
--- a/src/libkwineffects/kwinquickeffect.cpp
+++ b/src/libkwineffects/kwinquickeffect.cpp
@@ -8,6 +8,7 @@
#include "logging_p.h"
+#include
#include
#include
#include
@@ -62,8 +63,9 @@ public:
}
bool isItemOnScreen(QQuickItem *item, EffectScreen *screen) const;
- std::unique_ptr qmlComponent;
+ std::unique_ptr delegate;
QUrl source;
+ std::map> contexts;
std::map> incubators;
std::map> views;
QPointer mouseImplicitGrab;
@@ -244,7 +246,25 @@ void QuickSceneEffect::setSource(const QUrl &url)
}
if (d->source != url) {
d->source = url;
- d->qmlComponent.reset();
+ d->delegate.reset();
+ }
+}
+
+QQmlComponent *QuickSceneEffect::delegate() const
+{
+ return d->delegate.get();
+}
+
+void QuickSceneEffect::setDelegate(QQmlComponent *delegate)
+{
+ if (isRunning()) {
+ qWarning() << "Cannot change QuickSceneEffect.source while running";
+ return;
+ }
+ if (d->delegate.get() != delegate) {
+ d->source = QUrl();
+ d->delegate.reset(delegate);
+ Q_EMIT delegateChanged();
}
}
@@ -393,6 +413,7 @@ void QuickSceneEffect::handleScreenRemoved(EffectScreen *screen)
{
d->views.erase(screen);
d->incubators.erase(screen);
+ d->contexts.erase(screen);
}
void QuickSceneEffect::addScreen(EffectScreen *screen)
@@ -415,14 +436,18 @@ void QuickSceneEffect::addScreen(EffectScreen *screen)
view->scheduleRepaint();
d->views[screen] = std::move(view);
} else if (incubator->isError()) {
- qCWarning(LIBKWINEFFECTS) << "Could not create a view for QML file" << d->qmlComponent->url();
+ qCWarning(LIBKWINEFFECTS) << "Could not create a view for QML file" << d->delegate->url();
qCWarning(LIBKWINEFFECTS) << incubator->errors();
}
});
-
incubator->setInitialProperties(properties);
+
+ QQmlContext *creationContext = d->delegate->creationContext();
+ QQmlContext *context = new QQmlContext(creationContext ? creationContext : qmlContext(this));
+
+ d->contexts[screen].reset(context);
d->incubators[screen].reset(incubator);
- d->qmlComponent->create(*incubator);
+ d->delegate->create(*incubator, context);
}
void QuickSceneEffect::startInternal()
@@ -431,19 +456,20 @@ void QuickSceneEffect::startInternal()
return;
}
- if (Q_UNLIKELY(d->source.isEmpty())) {
- qWarning() << "QuickSceneEffect.source is empty. Did you forget to call setSource()?";
- return;
- }
+ if (!d->delegate) {
+ if (Q_UNLIKELY(d->source.isEmpty())) {
+ qWarning() << "QuickSceneEffect.source is empty. Did you forget to call setSource()?";
+ return;
+ }
- if (!d->qmlComponent) {
- d->qmlComponent = std::make_unique(effects->qmlEngine());
- d->qmlComponent->loadUrl(d->source);
- if (d->qmlComponent->isError()) {
- qWarning().nospace() << "Failed to load " << d->source << ": " << d->qmlComponent->errors();
- d->qmlComponent.reset();
+ d->delegate = std::make_unique(effects->qmlEngine());
+ d->delegate->loadUrl(d->source);
+ if (d->delegate->isError()) {
+ qWarning().nospace() << "Failed to load " << d->source << ": " << d->delegate->errors();
+ d->delegate.reset();
return;
}
+ Q_EMIT delegateChanged();
}
effects->setActiveFullScreenEffect(this);
@@ -474,6 +500,7 @@ void QuickSceneEffect::stopInternal()
d->incubators.clear();
d->views.clear();
+ d->contexts.clear();
d->running = false;
qApp->removeEventFilter(this);
effects->ungrabKeyboard();
diff --git a/src/libkwineffects/kwinquickeffect.h b/src/libkwineffects/kwinquickeffect.h
index 1bdfac6761..7c70dd43db 100644
--- a/src/libkwineffects/kwinquickeffect.h
+++ b/src/libkwineffects/kwinquickeffect.h
@@ -9,7 +9,7 @@
#include "libkwineffects/kwineffects.h"
#include "libkwineffects/kwinoffscreenquickview.h"
-#include
+#include
namespace KWin
{
@@ -74,6 +74,7 @@ class KWINEFFECTS_EXPORT QuickSceneEffect : public Effect
{
Q_OBJECT
Q_PROPERTY(QuickSceneView *activeView READ activeView NOTIFY activeViewChanged)
+ Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
public:
explicit QuickSceneEffect(QObject *parent = nullptr);
@@ -112,6 +113,12 @@ public:
*/
Q_INVOKABLE void activateView(QuickSceneView *view);
+ /**
+ * The delegate provides a template defining the contents of each instantiated screen view.
+ */
+ QQmlComponent *delegate() const;
+ void setDelegate(QQmlComponent *delegate);
+
/**
* Returns the source URL.
*/
@@ -150,6 +157,7 @@ Q_SIGNALS:
void itemDraggedOutOfScreen(QQuickItem *item, QList screens);
void itemDroppedOutOfScreen(const QPointF &globalPos, QQuickItem *item, EffectScreen *screen);
void activeViewChanged(KWin::QuickSceneView *view);
+ void delegateChanged();
protected:
/**
diff --git a/src/scripting/scriptedquicksceneeffect.cpp b/src/scripting/scriptedquicksceneeffect.cpp
new file mode 100644
index 0000000000..c83a063e12
--- /dev/null
+++ b/src/scripting/scriptedquicksceneeffect.cpp
@@ -0,0 +1,126 @@
+/*
+ SPDX-FileCopyrightText: 2023 Vlad Zahorodnii
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "scripting/scriptedquicksceneeffect.h"
+#include "main.h"
+
+#include
+#include
+
+#include
+
+namespace KWin
+{
+
+ScriptedQuickSceneEffect::ScriptedQuickSceneEffect()
+{
+ m_visibleTimer.setSingleShot(true);
+ connect(&m_visibleTimer, &QTimer::timeout, this, [this]() {
+ setRunning(false);
+ });
+}
+
+ScriptedQuickSceneEffect::~ScriptedQuickSceneEffect()
+{
+}
+
+int ScriptedQuickSceneEffect::requestedEffectChainPosition() const
+{
+ return m_requestedEffectChainPosition;
+}
+
+void ScriptedQuickSceneEffect::setMetaData(const KPluginMetaData &metaData)
+{
+ m_requestedEffectChainPosition = metaData.value(QStringLiteral("X-KDE-Ordering"), 50);
+
+ KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Effect-%1").arg(metaData.pluginId()));
+ const QString configFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + metaData.pluginId() + QLatin1String("/contents/config/main.xml"));
+ if (configFilePath.isNull()) {
+ m_configLoader = new KConfigLoader(cg, nullptr, this);
+ } else {
+ QFile xmlFile(configFilePath);
+ m_configLoader = new KConfigLoader(cg, &xmlFile, this);
+ m_configLoader->load();
+ }
+
+ m_configuration = new KConfigPropertyMap(m_configLoader, this);
+ connect(m_configLoader, &KConfigLoader::configChanged, this, &ScriptedQuickSceneEffect::configurationChanged);
+}
+
+bool ScriptedQuickSceneEffect::isVisible() const
+{
+ return m_isVisible;
+}
+
+void ScriptedQuickSceneEffect::setVisible(bool visible)
+{
+ if (m_isVisible == visible) {
+ return;
+ }
+ m_isVisible = visible;
+
+ if (m_isVisible) {
+ m_visibleTimer.stop();
+ setRunning(true);
+ } else {
+ // Delay setRunning(false) to avoid destroying views while still executing JS code.
+ m_visibleTimer.start();
+ }
+
+ Q_EMIT visibleChanged();
+}
+
+KConfigPropertyMap *ScriptedQuickSceneEffect::configuration() const
+{
+ return m_configuration;
+}
+
+QQmlListProperty ScriptedQuickSceneEffect::data()
+{
+ return QQmlListProperty(this, nullptr,
+ data_append,
+ data_count,
+ data_at,
+ data_clear);
+}
+
+void ScriptedQuickSceneEffect::data_append(QQmlListProperty *objects, QObject *object)
+{
+ if (!object) {
+ return;
+ }
+
+ ScriptedQuickSceneEffect *effect = static_cast(objects->object);
+ if (!effect->m_children.contains(object)) {
+ object->setParent(effect);
+ effect->m_children.append(object);
+ }
+}
+
+qsizetype ScriptedQuickSceneEffect::data_count(QQmlListProperty *objects)
+{
+ ScriptedQuickSceneEffect *effect = static_cast(objects->object);
+ return effect->m_children.count();
+}
+
+QObject *ScriptedQuickSceneEffect::data_at(QQmlListProperty *objects, qsizetype index)
+{
+ ScriptedQuickSceneEffect *effect = static_cast(objects->object);
+ return effect->m_children.value(index);
+}
+
+void ScriptedQuickSceneEffect::data_clear(QQmlListProperty *objects)
+{
+ ScriptedQuickSceneEffect *effect = static_cast(objects->object);
+ while (!effect->m_children.isEmpty()) {
+ QObject *child = effect->m_children.takeLast();
+ child->setParent(nullptr);
+ }
+}
+
+} // namespace KWin
+
+#include "moc_scriptedquicksceneeffect.cpp"
diff --git a/src/scripting/scriptedquicksceneeffect.h b/src/scripting/scriptedquicksceneeffect.h
new file mode 100644
index 0000000000..413f5d9fdd
--- /dev/null
+++ b/src/scripting/scriptedquicksceneeffect.h
@@ -0,0 +1,93 @@
+/*
+ SPDX-FileCopyrightText: 2023 Vlad Zahorodnii
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include "libkwineffects/kwinquickeffect.h"
+
+#include
+
+#include
+
+class KConfigLoader;
+class KConfigPropertyMap;
+
+namespace KWin
+{
+
+/**
+ * The SceneEffect type provides a way to implement effects that replace the default scene with
+ * a custom one.
+ *
+ * Example usage:
+ * @code
+ * SceneEffect {
+ * id: root
+ *
+ * delegate: Rectangle {
+ * color: "blue"
+ * }
+ *
+ * ShortcutHandler {
+ * name: "Toggle Effect"
+ * text: i18n("Toggle Effect")
+ * sequence: "Meta+E"
+ * onActivated: root.visible = !root.visible;
+ * }
+ * }
+ * @endcode
+ */
+class ScriptedQuickSceneEffect : public QuickSceneEffect
+{
+ Q_OBJECT
+ Q_PROPERTY(QQmlListProperty data READ data)
+ Q_CLASSINFO("DefaultProperty", "data")
+
+ /**
+ * The key-value store with the effect settings.
+ */
+ Q_PROPERTY(KConfigPropertyMap *configuration READ configuration NOTIFY configurationChanged)
+
+ /**
+ * Whether the effect is shown. Setting this property to @c true activates the effect; setting
+ * this property to @c false will deactivate the effect and the screen views will be unloaded at
+ * the next available time.
+ */
+ Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged)
+
+public:
+ explicit ScriptedQuickSceneEffect();
+ ~ScriptedQuickSceneEffect() override;
+
+ void setMetaData(const KPluginMetaData &metaData);
+
+ int requestedEffectChainPosition() const override;
+
+ bool isVisible() const;
+ void setVisible(bool visible);
+
+ QQmlListProperty data();
+ KConfigPropertyMap *configuration() const;
+
+ static void data_append(QQmlListProperty *objects, QObject *object);
+ static qsizetype data_count(QQmlListProperty *objects);
+ static QObject *data_at(QQmlListProperty *objects, qsizetype index);
+ static void data_clear(QQmlListProperty *objects);
+
+Q_SIGNALS:
+ void visibleChanged();
+ void configurationChanged();
+
+private:
+ KConfigLoader *m_configLoader = nullptr;
+ KConfigPropertyMap *m_configuration = nullptr;
+ QObjectList m_children;
+ QTimer m_visibleTimer;
+ bool m_isVisible = false;
+ int m_requestedEffectChainPosition = 0;
+};
+
+} // namespace KWin
diff --git a/src/scripting/scripting.cpp b/src/scripting/scripting.cpp
index 2cef5b8207..3f047f9bf4 100644
--- a/src/scripting/scripting.cpp
+++ b/src/scripting/scripting.cpp
@@ -16,6 +16,7 @@
#include "gesturehandler.h"
#include "libkwineffects/kwinquickeffect.h"
#include "screenedgehandler.h"
+#include "scriptedquicksceneeffect.h"
#include "scripting_logging.h"
#include "scriptingutils.h"
#include "shortcuthandler.h"
@@ -34,6 +35,7 @@
#include "workspace.h"
// KDE
#include
+#include
#include
#include
#include
@@ -652,12 +654,14 @@ void KWin::Scripting::init()
qmlRegisterType("org.kde.kwin", 3, 0, "WindowFilterModel");
qmlRegisterType("org.kde.kwin", 3, 0, "VirtualDesktopModel");
qmlRegisterUncreatableType("org.kde.kwin", 3, 0, "SceneView", QStringLiteral("Can't instantiate an object of type SceneView"));
+ qmlRegisterType("org.kde.kwin", 3, 0, "SceneEffect");
qmlRegisterSingletonType("org.kde.kwin", 3, 0, "Workspace", [](QQmlEngine *qmlEngine, QJSEngine *jsEngine) {
return new DeclarativeScriptWorkspaceWrapper();
});
qmlRegisterSingletonInstance("org.kde.kwin", 3, 0, "Options", options);
+ qmlRegisterAnonymousType("org.kde.kwin", 3);
qmlRegisterAnonymousType("org.kde.kwin", 3);
qmlRegisterAnonymousType("org.kde.kwin", 3);
qmlRegisterAnonymousType("org.kde.kwin", 3);