scripting: Add qml effect bindings

This allows creating third party qtquick scene effects without linking
to libkwineffects and thus rebuilding the effect every kwin release.
This commit is contained in:
Vlad Zahorodnii 2023-02-10 21:07:18 +02:00
parent 793a0e72bf
commit 264ebe6377
17 changed files with 473 additions and 17 deletions

1
examples/quick-effect/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build

View file

@ -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)

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name=""/>
<group name="">
<entry name="BackgroundColor" type="Color">
<default>#ff00ff</default>
</entry>
</group>
</kcfg>

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: None
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QuickEffectConfig</class>
<widget class="QWidget" name="QuickEffectConfig">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>455</width>
<height>177</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Background color:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="KColorButton" name="kcfg_BackgroundColor">
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>KColorButton</class>
<extends>QPushButton</extends>
<header>kcolorbutton.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: None
SPDX-License-Identifier: CC0-1.0

View file

@ -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
}
}

View file

@ -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"
}

View file

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: None
SPDX-License-Identifier: CC0-1.0

View file

@ -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

View file

@ -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 <KConfigGroup>
@ -23,6 +25,8 @@
#include <QDebug>
#include <QFutureWatcher>
#include <QPluginLoader>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QStaticPlugin>
#include <QStringList>
#include <QtConcurrentRun>
@ -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<ScriptedQuickSceneEffect *>(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) {

View file

@ -290,6 +290,9 @@ public:
private:
QList<KPluginMetaData> findAllEffects() const;
KPluginMetaData findEffect(const QString &name) const;
bool loadJavascriptEffect(const KPluginMetaData &effect);
bool loadDeclarativeEffect(const KPluginMetaData &effect);
QStringList m_loadedEffects;
EffectLoadQueue<ScriptedEffectLoader, KPluginMetaData> *m_queue;
QMetaObject::Connection m_queryConnection;

View file

@ -8,6 +8,7 @@
#include "logging_p.h"
#include <QQmlContext>
#include <QQmlEngine>
#include <QQmlIncubator>
#include <QQuickItem>
@ -62,8 +63,9 @@ public:
}
bool isItemOnScreen(QQuickItem *item, EffectScreen *screen) const;
std::unique_ptr<QQmlComponent> qmlComponent;
std::unique_ptr<QQmlComponent> delegate;
QUrl source;
std::map<EffectScreen *, std::unique_ptr<QQmlContext>> contexts;
std::map<EffectScreen *, std::unique_ptr<QQmlIncubator>> incubators;
std::map<EffectScreen *, std::unique_ptr<QuickSceneView>> views;
QPointer<QuickSceneView> 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->qmlComponent) {
d->qmlComponent = std::make_unique<QQmlComponent>(effects->qmlEngine());
d->qmlComponent->loadUrl(d->source);
if (d->qmlComponent->isError()) {
qWarning().nospace() << "Failed to load " << d->source << ": " << d->qmlComponent->errors();
d->qmlComponent.reset();
if (!d->delegate) {
if (Q_UNLIKELY(d->source.isEmpty())) {
qWarning() << "QuickSceneEffect.source is empty. Did you forget to call setSource()?";
return;
}
d->delegate = std::make_unique<QQmlComponent>(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();

View file

@ -9,7 +9,7 @@
#include "libkwineffects/kwineffects.h"
#include "libkwineffects/kwinoffscreenquickview.h"
#include <QQmlEngine>
#include <QQmlComponent>
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<EffectScreen *> screens);
void itemDroppedOutOfScreen(const QPointF &globalPos, QQuickItem *item, EffectScreen *screen);
void activeViewChanged(KWin::QuickSceneView *view);
void delegateChanged();
protected:
/**

View file

@ -0,0 +1,126 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "scripting/scriptedquicksceneeffect.h"
#include "main.h"
#include <KConfigGroup>
#include <KConfigLoader>
#include <QFile>
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<QObject> ScriptedQuickSceneEffect::data()
{
return QQmlListProperty<QObject>(this, nullptr,
data_append,
data_count,
data_at,
data_clear);
}
void ScriptedQuickSceneEffect::data_append(QQmlListProperty<QObject> *objects, QObject *object)
{
if (!object) {
return;
}
ScriptedQuickSceneEffect *effect = static_cast<ScriptedQuickSceneEffect *>(objects->object);
if (!effect->m_children.contains(object)) {
object->setParent(effect);
effect->m_children.append(object);
}
}
qsizetype ScriptedQuickSceneEffect::data_count(QQmlListProperty<QObject> *objects)
{
ScriptedQuickSceneEffect *effect = static_cast<ScriptedQuickSceneEffect *>(objects->object);
return effect->m_children.count();
}
QObject *ScriptedQuickSceneEffect::data_at(QQmlListProperty<QObject> *objects, qsizetype index)
{
ScriptedQuickSceneEffect *effect = static_cast<ScriptedQuickSceneEffect *>(objects->object);
return effect->m_children.value(index);
}
void ScriptedQuickSceneEffect::data_clear(QQmlListProperty<QObject> *objects)
{
ScriptedQuickSceneEffect *effect = static_cast<ScriptedQuickSceneEffect *>(objects->object);
while (!effect->m_children.isEmpty()) {
QObject *child = effect->m_children.takeLast();
child->setParent(nullptr);
}
}
} // namespace KWin
#include "moc_scriptedquicksceneeffect.cpp"

View file

@ -0,0 +1,93 @@
/*
SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "libkwineffects/kwinquickeffect.h"
#include <KConfigPropertyMap>
#include <QTimer>
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<QObject> 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<QObject> data();
KConfigPropertyMap *configuration() const;
static void data_append(QQmlListProperty<QObject> *objects, QObject *object);
static qsizetype data_count(QQmlListProperty<QObject> *objects);
static QObject *data_at(QQmlListProperty<QObject> *objects, qsizetype index);
static void data_clear(QQmlListProperty<QObject> *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

View file

@ -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 <KConfigGroup>
#include <KConfigPropertyMap>
#include <KGlobalAccel>
#include <KLocalizedContext>
#include <KPackage/PackageLoader>
@ -652,12 +654,14 @@ void KWin::Scripting::init()
qmlRegisterType<WindowFilterModel>("org.kde.kwin", 3, 0, "WindowFilterModel");
qmlRegisterType<VirtualDesktopModel>("org.kde.kwin", 3, 0, "VirtualDesktopModel");
qmlRegisterUncreatableType<KWin::QuickSceneView>("org.kde.kwin", 3, 0, "SceneView", QStringLiteral("Can't instantiate an object of type SceneView"));
qmlRegisterType<ScriptedQuickSceneEffect>("org.kde.kwin", 3, 0, "SceneEffect");
qmlRegisterSingletonType<DeclarativeScriptWorkspaceWrapper>("org.kde.kwin", 3, 0, "Workspace", [](QQmlEngine *qmlEngine, QJSEngine *jsEngine) {
return new DeclarativeScriptWorkspaceWrapper();
});
qmlRegisterSingletonInstance("org.kde.kwin", 3, 0, "Options", options);
qmlRegisterAnonymousType<KConfigPropertyMap>("org.kde.kwin", 3);
qmlRegisterAnonymousType<KWin::Output>("org.kde.kwin", 3);
qmlRegisterAnonymousType<KWin::Window>("org.kde.kwin", 3);
qmlRegisterAnonymousType<KWin::VirtualDesktop>("org.kde.kwin", 3);