File indexing completed on 2024-05-19 05:32:24

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2010 Rohan Prabhu <rohan@rohanprabhu.com>
0006     SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
0007     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "scripting.h"
0013 // own
0014 #include "dbuscall.h"
0015 #include "desktopbackgrounditem.h"
0016 #include "effect/quickeffect.h"
0017 #include "gesturehandler.h"
0018 #include "screenedgehandler.h"
0019 #include "scriptedquicksceneeffect.h"
0020 #include "scripting_logging.h"
0021 #include "scriptingutils.h"
0022 #include "shortcuthandler.h"
0023 #include "virtualdesktopmodel.h"
0024 #include "windowmodel.h"
0025 #include "windowthumbnailitem.h"
0026 #include "workspace_wrapper.h"
0027 
0028 #include "core/output.h"
0029 #include "input.h"
0030 #include "options.h"
0031 #include "screenedge.h"
0032 #include "tiles/tilemanager.h"
0033 #include "virtualdesktops.h"
0034 #include "window.h"
0035 #include "workspace.h"
0036 // KDE
0037 #include <KConfigGroup>
0038 #include <KConfigPropertyMap>
0039 #include <KGlobalAccel>
0040 #include <KLocalizedContext>
0041 #include <KPackage/PackageLoader>
0042 // Qt
0043 #include <QDBusConnection>
0044 #include <QDBusPendingCallWatcher>
0045 #include <QDebug>
0046 #include <QFutureWatcher>
0047 #include <QMenu>
0048 #include <QQmlContext>
0049 #include <QQmlEngine>
0050 #include <QQmlExpression>
0051 #include <QQuickWindow>
0052 #include <QSettings>
0053 #include <QStandardPaths>
0054 #include <QtConcurrentRun>
0055 
0056 #include "scriptadaptor.h"
0057 
0058 static QRect scriptValueToRect(const QJSValue &value)
0059 {
0060     return QRect(value.property(QStringLiteral("x")).toInt(),
0061                  value.property(QStringLiteral("y")).toInt(),
0062                  value.property(QStringLiteral("width")).toInt(),
0063                  value.property(QStringLiteral("height")).toInt());
0064 }
0065 
0066 static QRectF scriptValueToRectF(const QJSValue &value)
0067 {
0068     return QRectF(value.property(QStringLiteral("x")).toNumber(),
0069                   value.property(QStringLiteral("y")).toNumber(),
0070                   value.property(QStringLiteral("width")).toNumber(),
0071                   value.property(QStringLiteral("height")).toNumber());
0072 }
0073 
0074 static QPoint scriptValueToPoint(const QJSValue &value)
0075 {
0076     return QPoint(value.property(QStringLiteral("x")).toInt(),
0077                   value.property(QStringLiteral("y")).toInt());
0078 }
0079 
0080 static QPointF scriptValueToPointF(const QJSValue &value)
0081 {
0082     return QPointF(value.property(QStringLiteral("x")).toNumber(),
0083                    value.property(QStringLiteral("y")).toNumber());
0084 }
0085 
0086 static QSize scriptValueToSize(const QJSValue &value)
0087 {
0088     return QSize(value.property(QStringLiteral("width")).toInt(),
0089                  value.property(QStringLiteral("height")).toInt());
0090 }
0091 
0092 static QSizeF scriptValueToSizeF(const QJSValue &value)
0093 {
0094     return QSizeF(value.property(QStringLiteral("width")).toNumber(),
0095                   value.property(QStringLiteral("height")).toNumber());
0096 }
0097 
0098 KWin::AbstractScript::AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent)
0099     : QObject(parent)
0100     , m_scriptId(id)
0101     , m_fileName(scriptName)
0102     , m_pluginName(pluginName)
0103     , m_running(false)
0104 {
0105     if (m_pluginName.isNull()) {
0106         m_pluginName = scriptName;
0107     }
0108 
0109     new ScriptAdaptor(this);
0110     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting/Script") + QString::number(scriptId()), this, QDBusConnection::ExportAdaptors);
0111 }
0112 
0113 KWin::AbstractScript::~AbstractScript()
0114 {
0115 }
0116 
0117 KConfigGroup KWin::AbstractScript::config() const
0118 {
0119     return kwinApp()->config()->group(QLatin1String("Script-") + m_pluginName);
0120 }
0121 
0122 void KWin::AbstractScript::stop()
0123 {
0124     deleteLater();
0125 }
0126 
0127 KWin::ScriptTimer::ScriptTimer(QObject *parent)
0128     : QTimer(parent)
0129 {
0130 }
0131 
0132 KWin::Script::Script(int id, QString scriptName, QString pluginName, QObject *parent)
0133     : AbstractScript(id, scriptName, pluginName, parent)
0134     , m_engine(new QJSEngine(this))
0135     , m_starting(false)
0136 {
0137     // TODO: Remove in kwin 6. We have these converters only for compatibility reasons.
0138     if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QRect>()) {
0139         QMetaType::registerConverter<QJSValue, QRect>(scriptValueToRect);
0140     }
0141     if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QRectF>()) {
0142         QMetaType::registerConverter<QJSValue, QRectF>(scriptValueToRectF);
0143     }
0144 
0145     if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QPoint>()) {
0146         QMetaType::registerConverter<QJSValue, QPoint>(scriptValueToPoint);
0147     }
0148     if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QPointF>()) {
0149         QMetaType::registerConverter<QJSValue, QPointF>(scriptValueToPointF);
0150     }
0151 
0152     if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QSize>()) {
0153         QMetaType::registerConverter<QJSValue, QSize>(scriptValueToSize);
0154     }
0155     if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QSizeF>()) {
0156         QMetaType::registerConverter<QJSValue, QSizeF>(scriptValueToSizeF);
0157     }
0158 }
0159 
0160 KWin::Script::~Script()
0161 {
0162 }
0163 
0164 void KWin::Script::run()
0165 {
0166     if (running() || m_starting) {
0167         return;
0168     }
0169 
0170     if (calledFromDBus()) {
0171         m_invocationContext = message();
0172         setDelayedReply(true);
0173     }
0174 
0175     m_starting = true;
0176     QFutureWatcher<QByteArray> *watcher = new QFutureWatcher<QByteArray>(this);
0177     connect(watcher, &QFutureWatcherBase::finished, this, &Script::slotScriptLoadedFromFile);
0178     watcher->setFuture(QtConcurrent::run(&KWin::Script::loadScriptFromFile, this, fileName()));
0179 }
0180 
0181 QByteArray KWin::Script::loadScriptFromFile(const QString &fileName)
0182 {
0183     QFile file(fileName);
0184     if (!file.open(QIODevice::ReadOnly)) {
0185         return QByteArray();
0186     }
0187     QByteArray result(file.readAll());
0188     return result;
0189 }
0190 
0191 void KWin::Script::slotScriptLoadedFromFile()
0192 {
0193     QFutureWatcher<QByteArray> *watcher = dynamic_cast<QFutureWatcher<QByteArray> *>(sender());
0194     if (!watcher) {
0195         // not invoked from a QFutureWatcher
0196         return;
0197     }
0198     if (watcher->result().isNull()) {
0199         // do not load empty script
0200         deleteLater();
0201         watcher->deleteLater();
0202 
0203         if (m_invocationContext.type() == QDBusMessage::MethodCallMessage) {
0204             auto reply = m_invocationContext.createErrorReply("org.kde.kwin.Scripting.FileError", QString("Could not open %1").arg(fileName()));
0205             QDBusConnection::sessionBus().send(reply);
0206             m_invocationContext = QDBusMessage();
0207         }
0208 
0209         return;
0210     }
0211 
0212     // Install console functions (e.g. console.assert(), console.log(), etc).
0213     m_engine->installExtensions(QJSEngine::ConsoleExtension);
0214 
0215     // Make the timer visible to QJSEngine.
0216     QJSValue timerMetaObject = m_engine->newQMetaObject(&ScriptTimer::staticMetaObject);
0217     m_engine->globalObject().setProperty("QTimer", timerMetaObject);
0218 
0219     // Expose enums.
0220     m_engine->globalObject().setProperty(QStringLiteral("KWin"), m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject));
0221 
0222     // Make the options object visible to QJSEngine.
0223     QJSValue optionsObject = m_engine->newQObject(options);
0224     QQmlEngine::setObjectOwnership(options, QQmlEngine::CppOwnership);
0225     m_engine->globalObject().setProperty(QStringLiteral("options"), optionsObject);
0226 
0227     // Make the workspace visible to QJSEngine.
0228     QJSValue workspaceObject = m_engine->newQObject(Scripting::self()->workspaceWrapper());
0229     QQmlEngine::setObjectOwnership(Scripting::self()->workspaceWrapper(), QQmlEngine::CppOwnership);
0230     m_engine->globalObject().setProperty(QStringLiteral("workspace"), workspaceObject);
0231 
0232     QJSValue self = m_engine->newQObject(this);
0233     QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
0234 
0235     static const QStringList globalProperties{
0236         QStringLiteral("readConfig"),
0237         QStringLiteral("callDBus"),
0238 
0239         QStringLiteral("registerShortcut"),
0240         QStringLiteral("registerScreenEdge"),
0241         QStringLiteral("unregisterScreenEdge"),
0242         QStringLiteral("registerTouchScreenEdge"),
0243         QStringLiteral("unregisterTouchScreenEdge"),
0244         QStringLiteral("registerUserActionsMenu"),
0245     };
0246 
0247     for (const QString &propertyName : globalProperties) {
0248         m_engine->globalObject().setProperty(propertyName, self.property(propertyName));
0249     }
0250 
0251     // Inject assertion functions. It would be better to create a module with all
0252     // this assert functions or just deprecate them in favor of console.assert().
0253     QJSValue result = m_engine->evaluate(QStringLiteral(R"(
0254         function assert(condition, message) {
0255             console.assert(condition, message || 'Assertion failed');
0256         }
0257         function assertTrue(condition, message) {
0258             console.assert(condition, message || 'Assertion failed');
0259         }
0260         function assertFalse(condition, message) {
0261             console.assert(!condition, message || 'Assertion failed');
0262         }
0263         function assertNull(value, message) {
0264             console.assert(value === null, message || 'Assertion failed');
0265         }
0266         function assertNotNull(value, message) {
0267             console.assert(value !== null, message || 'Assertion failed');
0268         }
0269         function assertEquals(expected, actual, message) {
0270             console.assert(expected === actual, message || 'Assertion failed');
0271         }
0272     )"));
0273     Q_ASSERT(!result.isError());
0274 
0275     result = m_engine->evaluate(QString::fromUtf8(watcher->result()), fileName());
0276     if (result.isError()) {
0277         qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(fileName()),
0278                   result.property(QStringLiteral("lineNumber")).toInt(),
0279                   qPrintable(result.property(QStringLiteral("message")).toString()));
0280         deleteLater();
0281     }
0282 
0283     if (m_invocationContext.type() == QDBusMessage::MethodCallMessage) {
0284         auto reply = m_invocationContext.createReply();
0285         QDBusConnection::sessionBus().send(reply);
0286         m_invocationContext = QDBusMessage();
0287     }
0288 
0289     watcher->deleteLater();
0290     setRunning(true);
0291     m_starting = false;
0292 }
0293 
0294 QVariant KWin::Script::readConfig(const QString &key, const QVariant &defaultValue)
0295 {
0296     return config().readEntry(key, defaultValue);
0297 }
0298 
0299 void KWin::Script::callDBus(const QString &service, const QString &path, const QString &interface,
0300                             const QString &method, const QJSValue &arg1, const QJSValue &arg2,
0301                             const QJSValue &arg3, const QJSValue &arg4, const QJSValue &arg5,
0302                             const QJSValue &arg6, const QJSValue &arg7, const QJSValue &arg8,
0303                             const QJSValue &arg9)
0304 {
0305     QJSValueList jsArguments;
0306     jsArguments.reserve(9);
0307 
0308     if (!arg1.isUndefined()) {
0309         jsArguments << arg1;
0310     }
0311     if (!arg2.isUndefined()) {
0312         jsArguments << arg2;
0313     }
0314     if (!arg3.isUndefined()) {
0315         jsArguments << arg3;
0316     }
0317     if (!arg4.isUndefined()) {
0318         jsArguments << arg4;
0319     }
0320     if (!arg5.isUndefined()) {
0321         jsArguments << arg5;
0322     }
0323     if (!arg6.isUndefined()) {
0324         jsArguments << arg6;
0325     }
0326     if (!arg7.isUndefined()) {
0327         jsArguments << arg7;
0328     }
0329     if (!arg8.isUndefined()) {
0330         jsArguments << arg8;
0331     }
0332     if (!arg9.isUndefined()) {
0333         jsArguments << arg9;
0334     }
0335 
0336     QJSValue callback;
0337     if (!jsArguments.isEmpty() && jsArguments.last().isCallable()) {
0338         callback = jsArguments.takeLast();
0339     }
0340 
0341     QVariantList dbusArguments;
0342     dbusArguments.reserve(jsArguments.count());
0343     for (const QJSValue &jsArgument : std::as_const(jsArguments)) {
0344         dbusArguments << jsArgument.toVariant();
0345     }
0346 
0347     QDBusMessage message = QDBusMessage::createMethodCall(service, path, interface, method);
0348     message.setArguments(dbusArguments);
0349 
0350     const QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message);
0351     if (callback.isUndefined()) {
0352         return;
0353     }
0354 
0355     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
0356     connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, callback](QDBusPendingCallWatcher *self) {
0357         self->deleteLater();
0358 
0359         if (self->isError()) {
0360             qCWarning(KWIN_SCRIPTING) << "Received D-Bus message is error:" << self->error().message();
0361             return;
0362         }
0363 
0364         QJSValueList arguments;
0365         const QVariantList reply = self->reply().arguments();
0366         for (const QVariant &variant : reply) {
0367             arguments << m_engine->toScriptValue(dbusToVariant(variant));
0368         }
0369 
0370         QJSValue(callback).call(arguments);
0371     });
0372 }
0373 
0374 bool KWin::Script::registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback)
0375 {
0376     if (!callback.isCallable()) {
0377         m_engine->throwError(QStringLiteral("Shortcut handler must be callable"));
0378         return false;
0379     }
0380 
0381     QAction *action = new QAction(this);
0382     action->setObjectName(objectName);
0383     action->setText(text);
0384 
0385     const QKeySequence shortcut = keySequence;
0386     KGlobalAccel::self()->setShortcut(action, {shortcut});
0387 
0388     connect(action, &QAction::triggered, this, [this, action, callback]() {
0389         QJSValue(callback).call({m_engine->toScriptValue(action)});
0390     });
0391 
0392     return true;
0393 }
0394 
0395 bool KWin::Script::registerScreenEdge(int edge, const QJSValue &callback)
0396 {
0397     if (!callback.isCallable()) {
0398         m_engine->throwError(QStringLiteral("Screen edge handler must be callable"));
0399         return false;
0400     }
0401 
0402     QJSValueList &callbacks = m_screenEdgeCallbacks[edge];
0403     if (callbacks.isEmpty()) {
0404         workspace()->screenEdges()->reserve(static_cast<KWin::ElectricBorder>(edge), this, "slotBorderActivated");
0405     }
0406 
0407     callbacks << callback;
0408 
0409     return true;
0410 }
0411 
0412 bool KWin::Script::unregisterScreenEdge(int edge)
0413 {
0414     auto it = m_screenEdgeCallbacks.find(edge);
0415     if (it == m_screenEdgeCallbacks.end()) {
0416         return false;
0417     }
0418 
0419     workspace()->screenEdges()->unreserve(static_cast<KWin::ElectricBorder>(edge), this);
0420     m_screenEdgeCallbacks.erase(it);
0421 
0422     return true;
0423 }
0424 
0425 bool KWin::Script::registerTouchScreenEdge(int edge, const QJSValue &callback)
0426 {
0427     if (!callback.isCallable()) {
0428         m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable"));
0429         return false;
0430     }
0431     if (m_touchScreenEdgeCallbacks.contains(edge)) {
0432         return false;
0433     }
0434 
0435     QAction *action = new QAction(this);
0436     workspace()->screenEdges()->reserveTouch(KWin::ElectricBorder(edge), action);
0437     m_touchScreenEdgeCallbacks.insert(edge, action);
0438 
0439     connect(action, &QAction::triggered, this, [callback]() {
0440         QJSValue(callback).call();
0441     });
0442 
0443     return true;
0444 }
0445 
0446 bool KWin::Script::unregisterTouchScreenEdge(int edge)
0447 {
0448     auto it = m_touchScreenEdgeCallbacks.find(edge);
0449     if (it == m_touchScreenEdgeCallbacks.end()) {
0450         return false;
0451     }
0452 
0453     delete it.value();
0454     m_touchScreenEdgeCallbacks.erase(it);
0455 
0456     return true;
0457 }
0458 
0459 void KWin::Script::registerUserActionsMenu(const QJSValue &callback)
0460 {
0461     if (!callback.isCallable()) {
0462         m_engine->throwError(QStringLiteral("User action handler must be callable"));
0463         return;
0464     }
0465     m_userActionsMenuCallbacks.append(callback);
0466 }
0467 
0468 QList<QAction *> KWin::Script::actionsForUserActionMenu(KWin::Window *client, QMenu *parent)
0469 {
0470     QList<QAction *> actions;
0471     actions.reserve(m_userActionsMenuCallbacks.count());
0472 
0473     for (QJSValue callback : std::as_const(m_userActionsMenuCallbacks)) {
0474         const QJSValue result = callback.call({m_engine->toScriptValue(client)});
0475         if (result.isError()) {
0476             continue;
0477         }
0478         if (!result.isObject()) {
0479             continue;
0480         }
0481         if (QAction *action = scriptValueToAction(result, parent)) {
0482             actions << action;
0483         }
0484     }
0485 
0486     return actions;
0487 }
0488 
0489 bool KWin::Script::slotBorderActivated(ElectricBorder border)
0490 {
0491     const QJSValueList callbacks = m_screenEdgeCallbacks.value(border);
0492     if (callbacks.isEmpty()) {
0493         return false;
0494     }
0495     std::for_each(callbacks.begin(), callbacks.end(), [](QJSValue callback) {
0496         callback.call();
0497     });
0498     return true;
0499 }
0500 
0501 QAction *KWin::Script::scriptValueToAction(const QJSValue &value, QMenu *parent)
0502 {
0503     const QString title = value.property(QStringLiteral("text")).toString();
0504     if (title.isEmpty()) {
0505         return nullptr;
0506     }
0507 
0508     // Either a menu or a menu item.
0509     const QJSValue itemsValue = value.property(QStringLiteral("items"));
0510     if (!itemsValue.isUndefined()) {
0511         return createMenu(title, itemsValue, parent);
0512     }
0513 
0514     return createAction(title, value, parent);
0515 }
0516 
0517 QAction *KWin::Script::createAction(const QString &title, const QJSValue &item, QMenu *parent)
0518 {
0519     const QJSValue callback = item.property(QStringLiteral("triggered"));
0520     if (!callback.isCallable()) {
0521         return nullptr;
0522     }
0523 
0524     const bool checkable = item.property(QStringLiteral("checkable")).toBool();
0525     const bool checked = item.property(QStringLiteral("checked")).toBool();
0526 
0527     QAction *action = new QAction(title, parent);
0528     action->setCheckable(checkable);
0529     action->setChecked(checked);
0530 
0531     connect(action, &QAction::triggered, this, [this, action, callback]() {
0532         QJSValue(callback).call({m_engine->toScriptValue(action)});
0533     });
0534 
0535     return action;
0536 }
0537 
0538 QAction *KWin::Script::createMenu(const QString &title, const QJSValue &items, QMenu *parent)
0539 {
0540     if (!items.isArray()) {
0541         return nullptr;
0542     }
0543 
0544     const int length = items.property(QStringLiteral("length")).toInt();
0545     if (!length) {
0546         return nullptr;
0547     }
0548 
0549     QMenu *menu = new QMenu(title, parent);
0550     for (int i = 0; i < length; ++i) {
0551         const QJSValue value = items.property(QString::number(i));
0552         if (!value.isObject()) {
0553             continue;
0554         }
0555         if (QAction *action = scriptValueToAction(value, menu)) {
0556             menu->addAction(action);
0557         }
0558     }
0559 
0560     return menu->menuAction();
0561 }
0562 
0563 KWin::DeclarativeScript::DeclarativeScript(int id, QString scriptName, QString pluginName, QObject *parent)
0564     : AbstractScript(id, scriptName, pluginName, parent)
0565     , m_context(new QQmlContext(Scripting::self()->declarativeScriptSharedContext(), this))
0566     , m_component(new QQmlComponent(Scripting::self()->qmlEngine(), this))
0567 {
0568     m_context->setContextProperty(QStringLiteral("KWin"), new JSEngineGlobalMethodsWrapper(this));
0569 }
0570 
0571 KWin::DeclarativeScript::~DeclarativeScript()
0572 {
0573 }
0574 
0575 void KWin::DeclarativeScript::run()
0576 {
0577     if (running()) {
0578         return;
0579     }
0580 
0581     m_component->loadUrl(QUrl::fromLocalFile(fileName()));
0582     if (m_component->isLoading()) {
0583         connect(m_component, &QQmlComponent::statusChanged, this, &DeclarativeScript::createComponent);
0584     } else {
0585         createComponent();
0586     }
0587 }
0588 
0589 void KWin::DeclarativeScript::createComponent()
0590 {
0591     if (m_component->isError()) {
0592         qCWarning(KWIN_SCRIPTING) << "Component failed to load: " << m_component->errors();
0593     } else {
0594         if (QObject *object = m_component->create(m_context)) {
0595             object->setParent(this);
0596         }
0597     }
0598     setRunning(true);
0599 }
0600 
0601 KWin::JSEngineGlobalMethodsWrapper::JSEngineGlobalMethodsWrapper(KWin::DeclarativeScript *parent)
0602     : QObject(parent)
0603     , m_script(parent)
0604 {
0605 }
0606 
0607 KWin::JSEngineGlobalMethodsWrapper::~JSEngineGlobalMethodsWrapper()
0608 {
0609 }
0610 
0611 QVariant KWin::JSEngineGlobalMethodsWrapper::readConfig(const QString &key, QVariant defaultValue)
0612 {
0613     return m_script->config().readEntry(key, defaultValue);
0614 }
0615 
0616 KWin::Scripting *KWin::Scripting::s_self = nullptr;
0617 
0618 KWin::Scripting *KWin::Scripting::create(QObject *parent)
0619 {
0620     Q_ASSERT(!s_self);
0621     s_self = new Scripting(parent);
0622     return s_self;
0623 }
0624 
0625 KWin::Scripting::Scripting(QObject *parent)
0626     : QObject(parent)
0627     , m_scriptsLock(new QRecursiveMutex)
0628     , m_qmlEngine(new QQmlEngine(this))
0629     , m_declarativeScriptSharedContext(new QQmlContext(m_qmlEngine, this))
0630     , m_workspaceWrapper(new QtScriptWorkspaceWrapper(this))
0631 {
0632     m_qmlEngine->setProperty("_kirigamiTheme", QStringLiteral("KirigamiPlasmaStyle"));
0633     m_qmlEngine->rootContext()->setContextObject(new KLocalizedContext(m_qmlEngine));
0634     init();
0635     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting"), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables);
0636     connect(Workspace::self(), &Workspace::configChanged, this, &Scripting::start);
0637     connect(Workspace::self(), &Workspace::workspaceInitialized, this, &Scripting::start);
0638 }
0639 
0640 void KWin::Scripting::init()
0641 {
0642     qRegisterMetaType<QList<KWin::Output *>>();
0643     qRegisterMetaType<QList<KWin::Window *>>();
0644     qRegisterMetaType<QList<KWin::VirtualDesktop *>>();
0645 
0646     qmlRegisterType<DesktopBackgroundItem>("org.kde.kwin", 3, 0, "DesktopBackground");
0647     qmlRegisterType<WindowThumbnailItem>("org.kde.kwin", 3, 0, "WindowThumbnail");
0648     qmlRegisterType<DBusCall>("org.kde.kwin", 3, 0, "DBusCall");
0649     qmlRegisterType<ScreenEdgeHandler>("org.kde.kwin", 3, 0, "ScreenEdgeHandler");
0650     qmlRegisterType<ShortcutHandler>("org.kde.kwin", 3, 0, "ShortcutHandler");
0651     qmlRegisterType<SwipeGestureHandler>("org.kde.kwin", 3, 0, "SwipeGestureHandler");
0652     qmlRegisterType<PinchGestureHandler>("org.kde.kwin", 3, 0, "PinchGestureHandler");
0653     qmlRegisterType<WindowModel>("org.kde.kwin", 3, 0, "WindowModel");
0654     qmlRegisterType<WindowFilterModel>("org.kde.kwin", 3, 0, "WindowFilterModel");
0655     qmlRegisterType<VirtualDesktopModel>("org.kde.kwin", 3, 0, "VirtualDesktopModel");
0656     qmlRegisterUncreatableType<KWin::QuickSceneView>("org.kde.kwin", 3, 0, "SceneView", QStringLiteral("Can't instantiate an object of type SceneView"));
0657     qmlRegisterType<ScriptedQuickSceneEffect>("org.kde.kwin", 3, 0, "SceneEffect");
0658 
0659     qmlRegisterSingletonType<DeclarativeScriptWorkspaceWrapper>("org.kde.kwin", 3, 0, "Workspace", [](QQmlEngine *qmlEngine, QJSEngine *jsEngine) {
0660         return new DeclarativeScriptWorkspaceWrapper();
0661     });
0662     qmlRegisterSingletonInstance("org.kde.kwin", 3, 0, "Options", options);
0663 
0664     qmlRegisterAnonymousType<KConfigPropertyMap>("org.kde.kwin", 3);
0665     qmlRegisterAnonymousType<KWin::Output>("org.kde.kwin", 3);
0666     qmlRegisterAnonymousType<KWin::Window>("org.kde.kwin", 3);
0667     qmlRegisterAnonymousType<KWin::VirtualDesktop>("org.kde.kwin", 3);
0668     qmlRegisterAnonymousType<QAbstractItemModel>("org.kde.kwin", 3);
0669     qmlRegisterAnonymousType<KWin::TileManager>("org.kde.kwin", 3);
0670     // TODO: call the qml types as the C++ types?
0671     qmlRegisterUncreatableType<KWin::CustomTile>("org.kde.kwin", 3, 0, "CustomTile", QStringLiteral("Cannot create objects of type Tile"));
0672     qmlRegisterUncreatableType<KWin::Tile>("org.kde.kwin", 3, 0, "Tile", QStringLiteral("Cannot create objects of type AbstractTile"));
0673 }
0674 
0675 void KWin::Scripting::start()
0676 {
0677 #if 0
0678     // TODO make this threaded again once KConfigGroup is sufficiently thread safe, bug #305361 and friends
0679     // perform querying for the services in a thread
0680     QFutureWatcher<LoadScriptList> *watcher = new QFutureWatcher<LoadScriptList>(this);
0681     connect(watcher, &QFutureWatcher<LoadScriptList>::finished, this, &Scripting::slotScriptsQueried);
0682     watcher->setFuture(QtConcurrent::run(this, &KWin::Scripting::queryScriptsToLoad, pluginStates, offers));
0683 #else
0684     LoadScriptList scriptsToLoad = queryScriptsToLoad();
0685     for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin();
0686          it != scriptsToLoad.constEnd();
0687          ++it) {
0688         if (it->first) {
0689             loadScript(it->second.first, it->second.second);
0690         } else {
0691             loadDeclarativeScript(it->second.first, it->second.second);
0692         }
0693     }
0694 
0695     runScripts();
0696 #endif
0697 }
0698 
0699 LoadScriptList KWin::Scripting::queryScriptsToLoad()
0700 {
0701     KSharedConfig::Ptr _config = kwinApp()->config();
0702     static bool s_started = false;
0703     if (s_started) {
0704         _config->reparseConfiguration();
0705     } else {
0706         s_started = true;
0707     }
0708     QMap<QString, QString> pluginStates = KConfigGroup(_config, QStringLiteral("Plugins")).entryMap();
0709     const QString scriptFolder = QStringLiteral("kwin/scripts/");
0710     const auto offers = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), scriptFolder);
0711 
0712     LoadScriptList scriptsToLoad;
0713 
0714     for (const KPluginMetaData &service : offers) {
0715         const QString value = pluginStates.value(service.pluginId() + QLatin1String("Enabled"), QString());
0716         const bool enabled = value.isNull() ? service.isEnabledByDefault() : QVariant(value).toBool();
0717         const bool javaScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("javascript");
0718         const bool declarativeScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("declarativescript");
0719         if (!javaScript && !declarativeScript) {
0720             continue;
0721         }
0722 
0723         if (!enabled) {
0724             if (isScriptLoaded(service.pluginId())) {
0725                 // unload the script
0726                 unloadScript(service.pluginId());
0727             }
0728             continue;
0729         }
0730         const QString pluginName = service.pluginId();
0731         // The file we want to load depends on the specified API. We could check if one or the other file exists, but that is more error prone and causes IO overhead
0732         const QString relScriptPath = scriptFolder + pluginName + QLatin1String("/contents/") + (javaScript ? QLatin1String("code/main.js") : QLatin1String("ui/main.qml"));
0733         const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, relScriptPath);
0734         if (file.isEmpty()) {
0735             qCDebug(KWIN_SCRIPTING) << "Could not find script file for " << pluginName;
0736             continue;
0737         }
0738         scriptsToLoad << qMakePair(javaScript, qMakePair(file, pluginName));
0739     }
0740     return scriptsToLoad;
0741 }
0742 
0743 void KWin::Scripting::slotScriptsQueried()
0744 {
0745     QFutureWatcher<LoadScriptList> *watcher = dynamic_cast<QFutureWatcher<LoadScriptList> *>(sender());
0746     if (!watcher) {
0747         // slot invoked not from a FutureWatcher
0748         return;
0749     }
0750 
0751     LoadScriptList scriptsToLoad = watcher->result();
0752     for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin();
0753          it != scriptsToLoad.constEnd();
0754          ++it) {
0755         if (it->first) {
0756             loadScript(it->second.first, it->second.second);
0757         } else {
0758             loadDeclarativeScript(it->second.first, it->second.second);
0759         }
0760     }
0761 
0762     runScripts();
0763     watcher->deleteLater();
0764 }
0765 
0766 bool KWin::Scripting::isScriptLoaded(const QString &pluginName) const
0767 {
0768     return findScript(pluginName) != nullptr;
0769 }
0770 
0771 KWin::AbstractScript *KWin::Scripting::findScript(const QString &pluginName) const
0772 {
0773     QMutexLocker locker(m_scriptsLock.get());
0774     for (AbstractScript *script : std::as_const(scripts)) {
0775         if (script->pluginName() == pluginName) {
0776             return script;
0777         }
0778     }
0779     return nullptr;
0780 }
0781 
0782 bool KWin::Scripting::unloadScript(const QString &pluginName)
0783 {
0784     QMutexLocker locker(m_scriptsLock.get());
0785     for (AbstractScript *script : std::as_const(scripts)) {
0786         if (script->pluginName() == pluginName) {
0787             script->deleteLater();
0788             return true;
0789         }
0790     }
0791     return false;
0792 }
0793 
0794 void KWin::Scripting::runScripts()
0795 {
0796     QMutexLocker locker(m_scriptsLock.get());
0797     for (int i = 0; i < scripts.size(); i++) {
0798         scripts.at(i)->run();
0799     }
0800 }
0801 
0802 void KWin::Scripting::scriptDestroyed(QObject *object)
0803 {
0804     QMutexLocker locker(m_scriptsLock.get());
0805     scripts.removeAll(static_cast<KWin::Script *>(object));
0806 }
0807 
0808 int KWin::Scripting::loadScript(const QString &filePath, const QString &pluginName)
0809 {
0810     QMutexLocker locker(m_scriptsLock.get());
0811     if (isScriptLoaded(pluginName)) {
0812         return -1;
0813     }
0814     const int id = scripts.size();
0815     KWin::Script *script = new KWin::Script(id, filePath, pluginName, this);
0816     connect(script, &QObject::destroyed, this, &Scripting::scriptDestroyed);
0817     scripts.append(script);
0818     return id;
0819 }
0820 
0821 int KWin::Scripting::loadDeclarativeScript(const QString &filePath, const QString &pluginName)
0822 {
0823     QMutexLocker locker(m_scriptsLock.get());
0824     if (isScriptLoaded(pluginName)) {
0825         return -1;
0826     }
0827     const int id = scripts.size();
0828     KWin::DeclarativeScript *script = new KWin::DeclarativeScript(id, filePath, pluginName, this);
0829     connect(script, &QObject::destroyed, this, &Scripting::scriptDestroyed);
0830     scripts.append(script);
0831     return id;
0832 }
0833 
0834 KWin::Scripting::~Scripting()
0835 {
0836     QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Scripting"));
0837     s_self = nullptr;
0838 }
0839 
0840 QList<QAction *> KWin::Scripting::actionsForUserActionMenu(KWin::Window *c, QMenu *parent)
0841 {
0842     QList<QAction *> actions;
0843     for (AbstractScript *s : std::as_const(scripts)) {
0844         // TODO: Allow declarative scripts to add their own user actions.
0845         if (Script *script = qobject_cast<Script *>(s)) {
0846             actions << script->actionsForUserActionMenu(c, parent);
0847         }
0848     }
0849     return actions;
0850 }
0851 
0852 #include "moc_scripting.cpp"