KWin
Loading...
Searching...
No Matches
scripting.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2010 Rohan Prabhu <rohan@rohanprabhu.com>
6 SPDX-FileCopyrightText: 2011 Martin Gräßlin <mgraesslin@kde.org>
7 SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
8
9 SPDX-License-Identifier: GPL-2.0-or-later
10*/
11
12#include "scripting.h"
13// own
14#include "dbuscall.h"
16#include "effect/quickeffect.h"
17#include "gesturehandler.h"
18#include "screenedgehandler.h"
20#include "scripting_logging.h"
21#include "scriptingutils.h"
22#include "shortcuthandler.h"
23#include "virtualdesktopmodel.h"
24#include "windowmodel.h"
25#include "windowthumbnailitem.h"
26#include "workspace_wrapper.h"
27
28#include "core/output.h"
29#include "input.h"
30#include "options.h"
31#include "screenedge.h"
32#include "tiles/tilemanager.h"
33#include "virtualdesktops.h"
34#include "window.h"
35#include "workspace.h"
36// KDE
37#include <KConfigGroup>
38#include <KConfigPropertyMap>
39#include <KGlobalAccel>
40#include <KLocalizedContext>
41#include <KPackage/PackageLoader>
42// Qt
43#include <QDBusConnection>
44#include <QDBusPendingCallWatcher>
45#include <QDebug>
46#include <QFutureWatcher>
47#include <QMenu>
48#include <QQmlContext>
49#include <QQmlEngine>
50#include <QQmlExpression>
51#include <QQuickWindow>
52#include <QSettings>
53#include <QStandardPaths>
54#include <QtConcurrentRun>
55
56#include "scriptadaptor.h"
57
58static QRect scriptValueToRect(const QJSValue &value)
59{
60 return QRect(value.property(QStringLiteral("x")).toInt(),
61 value.property(QStringLiteral("y")).toInt(),
62 value.property(QStringLiteral("width")).toInt(),
63 value.property(QStringLiteral("height")).toInt());
64}
65
66static QRectF scriptValueToRectF(const QJSValue &value)
67{
68 return QRectF(value.property(QStringLiteral("x")).toNumber(),
69 value.property(QStringLiteral("y")).toNumber(),
70 value.property(QStringLiteral("width")).toNumber(),
71 value.property(QStringLiteral("height")).toNumber());
72}
73
74static QPoint scriptValueToPoint(const QJSValue &value)
75{
76 return QPoint(value.property(QStringLiteral("x")).toInt(),
77 value.property(QStringLiteral("y")).toInt());
78}
79
80static QPointF scriptValueToPointF(const QJSValue &value)
81{
82 return QPointF(value.property(QStringLiteral("x")).toNumber(),
83 value.property(QStringLiteral("y")).toNumber());
84}
85
86static QSize scriptValueToSize(const QJSValue &value)
87{
88 return QSize(value.property(QStringLiteral("width")).toInt(),
89 value.property(QStringLiteral("height")).toInt());
90}
91
92static QSizeF scriptValueToSizeF(const QJSValue &value)
93{
94 return QSizeF(value.property(QStringLiteral("width")).toNumber(),
95 value.property(QStringLiteral("height")).toNumber());
96}
97
98KWin::AbstractScript::AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent)
99 : QObject(parent)
100 , m_scriptId(id)
101 , m_fileName(scriptName)
102 , m_pluginName(pluginName)
103 , m_running(false)
104{
105 if (m_pluginName.isNull()) {
106 m_pluginName = scriptName;
107 }
108
109 new ScriptAdaptor(this);
110 QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting/Script") + QString::number(scriptId()), this, QDBusConnection::ExportAdaptors);
111}
112
116
118{
119 return kwinApp()->config()->group(QLatin1String("Script-") + m_pluginName);
120}
121
123{
124 deleteLater();
125}
126
128 : QTimer(parent)
129{
130}
131
132KWin::Script::Script(int id, QString scriptName, QString pluginName, QObject *parent)
133 : AbstractScript(id, scriptName, pluginName, parent)
134 , m_engine(new QJSEngine(this))
135 , m_starting(false)
136{
137 // TODO: Remove in kwin 6. We have these converters only for compatibility reasons.
138 if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QRect>()) {
139 QMetaType::registerConverter<QJSValue, QRect>(scriptValueToRect);
140 }
141 if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QRectF>()) {
142 QMetaType::registerConverter<QJSValue, QRectF>(scriptValueToRectF);
143 }
144
145 if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QPoint>()) {
146 QMetaType::registerConverter<QJSValue, QPoint>(scriptValueToPoint);
147 }
148 if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QPointF>()) {
149 QMetaType::registerConverter<QJSValue, QPointF>(scriptValueToPointF);
150 }
151
152 if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QSize>()) {
153 QMetaType::registerConverter<QJSValue, QSize>(scriptValueToSize);
154 }
155 if (!QMetaType::hasRegisteredConverterFunction<QJSValue, QSizeF>()) {
156 QMetaType::registerConverter<QJSValue, QSizeF>(scriptValueToSizeF);
157 }
158}
159
163
165{
166 if (running() || m_starting) {
167 return;
168 }
169
170 if (calledFromDBus()) {
171 m_invocationContext = message();
172 setDelayedReply(true);
173 }
174
175 m_starting = true;
177 connect(watcher, &QFutureWatcherBase::finished, this, &Script::slotScriptLoadedFromFile);
178 watcher->setFuture(QtConcurrent::run(&KWin::Script::loadScriptFromFile, this, fileName()));
179}
180
181QByteArray KWin::Script::loadScriptFromFile(const QString &fileName)
182{
183 QFile file(fileName);
184 if (!file.open(QIODevice::ReadOnly)) {
185 return QByteArray();
186 }
187 QByteArray result(file.readAll());
188 return result;
189}
190
191void KWin::Script::slotScriptLoadedFromFile()
192{
193 QFutureWatcher<QByteArray> *watcher = dynamic_cast<QFutureWatcher<QByteArray> *>(sender());
194 if (!watcher) {
195 // not invoked from a QFutureWatcher
196 return;
197 }
198 if (watcher->result().isNull()) {
199 // do not load empty script
200 deleteLater();
201 watcher->deleteLater();
202
203 if (m_invocationContext.type() == QDBusMessage::MethodCallMessage) {
204 auto reply = m_invocationContext.createErrorReply("org.kde.kwin.Scripting.FileError", QString("Could not open %1").arg(fileName()));
205 QDBusConnection::sessionBus().send(reply);
206 m_invocationContext = QDBusMessage();
207 }
208
209 return;
210 }
211
212 // Install console functions (e.g. console.assert(), console.log(), etc).
213 m_engine->installExtensions(QJSEngine::ConsoleExtension);
214
215 // Make the timer visible to QJSEngine.
216 QJSValue timerMetaObject = m_engine->newQMetaObject(&ScriptTimer::staticMetaObject);
217 m_engine->globalObject().setProperty("QTimer", timerMetaObject);
218
219 // Expose enums.
220 m_engine->globalObject().setProperty(QStringLiteral("KWin"), m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject));
221
222 // Make the options object visible to QJSEngine.
223 QJSValue optionsObject = m_engine->newQObject(options);
224 QQmlEngine::setObjectOwnership(options, QQmlEngine::CppOwnership);
225 m_engine->globalObject().setProperty(QStringLiteral("options"), optionsObject);
226
227 // Make the workspace visible to QJSEngine.
228 QJSValue workspaceObject = m_engine->newQObject(Scripting::self()->workspaceWrapper());
229 QQmlEngine::setObjectOwnership(Scripting::self()->workspaceWrapper(), QQmlEngine::CppOwnership);
230 m_engine->globalObject().setProperty(QStringLiteral("workspace"), workspaceObject);
231
232 QJSValue self = m_engine->newQObject(this);
233 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
234
235 static const QStringList globalProperties{
236 QStringLiteral("readConfig"),
237 QStringLiteral("callDBus"),
238
239 QStringLiteral("registerShortcut"),
240 QStringLiteral("registerScreenEdge"),
241 QStringLiteral("unregisterScreenEdge"),
242 QStringLiteral("registerTouchScreenEdge"),
243 QStringLiteral("unregisterTouchScreenEdge"),
244 QStringLiteral("registerUserActionsMenu"),
245 };
246
247 for (const QString &propertyName : globalProperties) {
248 m_engine->globalObject().setProperty(propertyName, self.property(propertyName));
249 }
250
251 // Inject assertion functions. It would be better to create a module with all
252 // this assert functions or just deprecate them in favor of console.assert().
253 QJSValue result = m_engine->evaluate(QStringLiteral(R"(
254 function assert(condition, message) {
255 console.assert(condition, message || 'Assertion failed');
256 }
257 function assertTrue(condition, message) {
258 console.assert(condition, message || 'Assertion failed');
259 }
260 function assertFalse(condition, message) {
261 console.assert(!condition, message || 'Assertion failed');
262 }
263 function assertNull(value, message) {
264 console.assert(value === null, message || 'Assertion failed');
265 }
266 function assertNotNull(value, message) {
267 console.assert(value !== null, message || 'Assertion failed');
268 }
269 function assertEquals(expected, actual, message) {
270 console.assert(expected === actual, message || 'Assertion failed');
271 }
272 )"));
273 Q_ASSERT(!result.isError());
274
275 result = m_engine->evaluate(QString::fromUtf8(watcher->result()), fileName());
276 if (result.isError()) {
277 qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(fileName()),
278 result.property(QStringLiteral("lineNumber")).toInt(),
279 qPrintable(result.property(QStringLiteral("message")).toString()));
280 deleteLater();
281 }
282
283 if (m_invocationContext.type() == QDBusMessage::MethodCallMessage) {
284 auto reply = m_invocationContext.createReply();
285 QDBusConnection::sessionBus().send(reply);
286 m_invocationContext = QDBusMessage();
287 }
288
289 watcher->deleteLater();
290 setRunning(true);
291 m_starting = false;
292}
293
294QVariant KWin::Script::readConfig(const QString &key, const QVariant &defaultValue)
295{
296 return config().readEntry(key, defaultValue);
297}
298
299void KWin::Script::callDBus(const QString &service, const QString &path, const QString &interface,
300 const QString &method, const QJSValue &arg1, const QJSValue &arg2,
301 const QJSValue &arg3, const QJSValue &arg4, const QJSValue &arg5,
302 const QJSValue &arg6, const QJSValue &arg7, const QJSValue &arg8,
303 const QJSValue &arg9)
304{
305 QJSValueList jsArguments;
306 jsArguments.reserve(9);
307
308 if (!arg1.isUndefined()) {
309 jsArguments << arg1;
310 }
311 if (!arg2.isUndefined()) {
312 jsArguments << arg2;
313 }
314 if (!arg3.isUndefined()) {
315 jsArguments << arg3;
316 }
317 if (!arg4.isUndefined()) {
318 jsArguments << arg4;
319 }
320 if (!arg5.isUndefined()) {
321 jsArguments << arg5;
322 }
323 if (!arg6.isUndefined()) {
324 jsArguments << arg6;
325 }
326 if (!arg7.isUndefined()) {
327 jsArguments << arg7;
328 }
329 if (!arg8.isUndefined()) {
330 jsArguments << arg8;
331 }
332 if (!arg9.isUndefined()) {
333 jsArguments << arg9;
334 }
335
336 QJSValue callback;
337 if (!jsArguments.isEmpty() && jsArguments.last().isCallable()) {
338 callback = jsArguments.takeLast();
339 }
340
341 QVariantList dbusArguments;
342 dbusArguments.reserve(jsArguments.count());
343 for (const QJSValue &jsArgument : std::as_const(jsArguments)) {
344 dbusArguments << jsArgument.toVariant();
345 }
346
347 QDBusMessage message = QDBusMessage::createMethodCall(service, path, interface, method);
348 message.setArguments(dbusArguments);
349
350 const QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message);
351 if (callback.isUndefined()) {
352 return;
353 }
354
355 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
356 connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, callback](QDBusPendingCallWatcher *self) {
357 self->deleteLater();
358
359 if (self->isError()) {
360 qCWarning(KWIN_SCRIPTING) << "Received D-Bus message is error:" << self->error().message();
361 return;
362 }
363
364 QJSValueList arguments;
365 const QVariantList reply = self->reply().arguments();
366 for (const QVariant &variant : reply) {
367 arguments << m_engine->toScriptValue(dbusToVariant(variant));
368 }
369
370 QJSValue(callback).call(arguments);
371 });
372}
373
374bool KWin::Script::registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback)
375{
376 if (!callback.isCallable()) {
377 m_engine->throwError(QStringLiteral("Shortcut handler must be callable"));
378 return false;
379 }
380
381 QAction *action = new QAction(this);
382 action->setObjectName(objectName);
383 action->setText(text);
384
385 const QKeySequence shortcut = keySequence;
386 KGlobalAccel::self()->setShortcut(action, {shortcut});
387
388 connect(action, &QAction::triggered, this, [this, action, callback]() {
389 QJSValue(callback).call({m_engine->toScriptValue(action)});
390 });
391
392 return true;
393}
394
395bool KWin::Script::registerScreenEdge(int edge, const QJSValue &callback)
396{
397 if (!callback.isCallable()) {
398 m_engine->throwError(QStringLiteral("Screen edge handler must be callable"));
399 return false;
400 }
401
402 QJSValueList &callbacks = m_screenEdgeCallbacks[edge];
403 if (callbacks.isEmpty()) {
404 workspace()->screenEdges()->reserve(static_cast<KWin::ElectricBorder>(edge), this, "slotBorderActivated");
405 }
406
407 callbacks << callback;
408
409 return true;
410}
411
413{
414 auto it = m_screenEdgeCallbacks.find(edge);
415 if (it == m_screenEdgeCallbacks.end()) {
416 return false;
417 }
418
419 workspace()->screenEdges()->unreserve(static_cast<KWin::ElectricBorder>(edge), this);
420 m_screenEdgeCallbacks.erase(it);
421
422 return true;
423}
424
425bool KWin::Script::registerTouchScreenEdge(int edge, const QJSValue &callback)
426{
427 if (!callback.isCallable()) {
428 m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable"));
429 return false;
430 }
431 if (m_touchScreenEdgeCallbacks.contains(edge)) {
432 return false;
433 }
434
435 QAction *action = new QAction(this);
437 m_touchScreenEdgeCallbacks.insert(edge, action);
438
439 connect(action, &QAction::triggered, this, [callback]() {
440 QJSValue(callback).call();
441 });
442
443 return true;
444}
445
447{
448 auto it = m_touchScreenEdgeCallbacks.find(edge);
449 if (it == m_touchScreenEdgeCallbacks.end()) {
450 return false;
451 }
452
453 delete it.value();
454 m_touchScreenEdgeCallbacks.erase(it);
455
456 return true;
457}
458
459void KWin::Script::registerUserActionsMenu(const QJSValue &callback)
460{
461 if (!callback.isCallable()) {
462 m_engine->throwError(QStringLiteral("User action handler must be callable"));
463 return;
464 }
465 m_userActionsMenuCallbacks.append(callback);
466}
467
468QList<QAction *> KWin::Script::actionsForUserActionMenu(KWin::Window *client, QMenu *parent)
469{
470 QList<QAction *> actions;
471 actions.reserve(m_userActionsMenuCallbacks.count());
472
473 for (QJSValue callback : std::as_const(m_userActionsMenuCallbacks)) {
474 const QJSValue result = callback.call({m_engine->toScriptValue(client)});
475 if (result.isError()) {
476 continue;
477 }
478 if (!result.isObject()) {
479 continue;
480 }
481 if (QAction *action = scriptValueToAction(result, parent)) {
482 actions << action;
483 }
484 }
485
486 return actions;
487}
488
489bool KWin::Script::slotBorderActivated(ElectricBorder border)
490{
491 const QJSValueList callbacks = m_screenEdgeCallbacks.value(border);
492 if (callbacks.isEmpty()) {
493 return false;
494 }
495 std::for_each(callbacks.begin(), callbacks.end(), [](QJSValue callback) {
496 callback.call();
497 });
498 return true;
499}
500
501QAction *KWin::Script::scriptValueToAction(const QJSValue &value, QMenu *parent)
502{
503 const QString title = value.property(QStringLiteral("text")).toString();
504 if (title.isEmpty()) {
505 return nullptr;
506 }
507
508 // Either a menu or a menu item.
509 const QJSValue itemsValue = value.property(QStringLiteral("items"));
510 if (!itemsValue.isUndefined()) {
511 return createMenu(title, itemsValue, parent);
512 }
513
514 return createAction(title, value, parent);
515}
516
517QAction *KWin::Script::createAction(const QString &title, const QJSValue &item, QMenu *parent)
518{
519 const QJSValue callback = item.property(QStringLiteral("triggered"));
520 if (!callback.isCallable()) {
521 return nullptr;
522 }
523
524 const bool checkable = item.property(QStringLiteral("checkable")).toBool();
525 const bool checked = item.property(QStringLiteral("checked")).toBool();
526
527 QAction *action = new QAction(title, parent);
528 action->setCheckable(checkable);
529 action->setChecked(checked);
530
531 connect(action, &QAction::triggered, this, [this, action, callback]() {
532 QJSValue(callback).call({m_engine->toScriptValue(action)});
533 });
534
535 return action;
536}
537
538QAction *KWin::Script::createMenu(const QString &title, const QJSValue &items, QMenu *parent)
539{
540 if (!items.isArray()) {
541 return nullptr;
542 }
543
544 const int length = items.property(QStringLiteral("length")).toInt();
545 if (!length) {
546 return nullptr;
547 }
548
549 QMenu *menu = new QMenu(title, parent);
550 for (int i = 0; i < length; ++i) {
551 const QJSValue value = items.property(QString::number(i));
552 if (!value.isObject()) {
553 continue;
554 }
555 if (QAction *action = scriptValueToAction(value, menu)) {
556 menu->addAction(action);
557 }
558 }
559
560 return menu->menuAction();
561}
562
563KWin::DeclarativeScript::DeclarativeScript(int id, QString scriptName, QString pluginName, QObject *parent)
564 : AbstractScript(id, scriptName, pluginName, parent)
565 , m_context(new QQmlContext(Scripting::self()->declarativeScriptSharedContext(), this))
566 , m_component(new QQmlComponent(Scripting::self()->qmlEngine(), this))
567{
568 m_context->setContextProperty(QStringLiteral("KWin"), new JSEngineGlobalMethodsWrapper(this));
569}
570
574
576{
577 if (running()) {
578 return;
579 }
580
581 m_component->loadUrl(QUrl::fromLocalFile(fileName()));
582 if (m_component->isLoading()) {
583 connect(m_component, &QQmlComponent::statusChanged, this, &DeclarativeScript::createComponent);
584 } else {
585 createComponent();
586 }
587}
588
589void KWin::DeclarativeScript::createComponent()
590{
591 if (m_component->isError()) {
592 qCWarning(KWIN_SCRIPTING) << "Component failed to load: " << m_component->errors();
593 } else {
594 if (QObject *object = m_component->create(m_context)) {
595 object->setParent(this);
596 }
597 }
598 setRunning(true);
599}
600
602 : QObject(parent)
603 , m_script(parent)
604{
605}
606
610
611QVariant KWin::JSEngineGlobalMethodsWrapper::readConfig(const QString &key, QVariant defaultValue)
612{
613 return m_script->config().readEntry(key, defaultValue);
614}
615
616KWin::Scripting *KWin::Scripting::s_self = nullptr;
617
619{
620 Q_ASSERT(!s_self);
621 s_self = new Scripting(parent);
622 return s_self;
623}
624
625KWin::Scripting::Scripting(QObject *parent)
626 : QObject(parent)
627 , m_scriptsLock(new QRecursiveMutex)
628 , m_qmlEngine(new QQmlEngine(this))
629 , m_declarativeScriptSharedContext(new QQmlContext(m_qmlEngine, this))
630 , m_workspaceWrapper(new QtScriptWorkspaceWrapper(this))
631{
632 m_qmlEngine->setProperty("_kirigamiTheme", QStringLiteral("KirigamiPlasmaStyle"));
633 m_qmlEngine->rootContext()->setContextObject(new KLocalizedContext(m_qmlEngine));
634 init();
635 QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting"), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables);
638}
639
640void KWin::Scripting::init()
641{
642 qRegisterMetaType<QList<KWin::Output *>>();
643 qRegisterMetaType<QList<KWin::Window *>>();
644 qRegisterMetaType<QList<KWin::VirtualDesktop *>>();
645
646 qmlRegisterType<DesktopBackgroundItem>("org.kde.kwin", 3, 0, "DesktopBackground");
647 qmlRegisterType<WindowThumbnailItem>("org.kde.kwin", 3, 0, "WindowThumbnail");
648 qmlRegisterType<DBusCall>("org.kde.kwin", 3, 0, "DBusCall");
649 qmlRegisterType<ScreenEdgeHandler>("org.kde.kwin", 3, 0, "ScreenEdgeHandler");
650 qmlRegisterType<ShortcutHandler>("org.kde.kwin", 3, 0, "ShortcutHandler");
651 qmlRegisterType<SwipeGestureHandler>("org.kde.kwin", 3, 0, "SwipeGestureHandler");
652 qmlRegisterType<PinchGestureHandler>("org.kde.kwin", 3, 0, "PinchGestureHandler");
653 qmlRegisterType<WindowModel>("org.kde.kwin", 3, 0, "WindowModel");
654 qmlRegisterType<WindowFilterModel>("org.kde.kwin", 3, 0, "WindowFilterModel");
655 qmlRegisterType<VirtualDesktopModel>("org.kde.kwin", 3, 0, "VirtualDesktopModel");
656 qmlRegisterUncreatableType<KWin::QuickSceneView>("org.kde.kwin", 3, 0, "SceneView", QStringLiteral("Can't instantiate an object of type SceneView"));
657 qmlRegisterType<ScriptedQuickSceneEffect>("org.kde.kwin", 3, 0, "SceneEffect");
658
659 qmlRegisterSingletonType<DeclarativeScriptWorkspaceWrapper>("org.kde.kwin", 3, 0, "Workspace", [](QQmlEngine *qmlEngine, QJSEngine *jsEngine) {
660 return new DeclarativeScriptWorkspaceWrapper();
661 });
662 qmlRegisterSingletonInstance("org.kde.kwin", 3, 0, "Options", options);
663
664 qmlRegisterAnonymousType<KConfigPropertyMap>("org.kde.kwin", 3);
665 qmlRegisterAnonymousType<KWin::Output>("org.kde.kwin", 3);
666 qmlRegisterAnonymousType<KWin::Window>("org.kde.kwin", 3);
667 qmlRegisterAnonymousType<KWin::VirtualDesktop>("org.kde.kwin", 3);
668 qmlRegisterAnonymousType<QAbstractItemModel>("org.kde.kwin", 3);
669 qmlRegisterAnonymousType<KWin::TileManager>("org.kde.kwin", 3);
670 // TODO: call the qml types as the C++ types?
671 qmlRegisterUncreatableType<KWin::CustomTile>("org.kde.kwin", 3, 0, "CustomTile", QStringLiteral("Cannot create objects of type Tile"));
672 qmlRegisterUncreatableType<KWin::Tile>("org.kde.kwin", 3, 0, "Tile", QStringLiteral("Cannot create objects of type AbstractTile"));
673}
674
676{
677#if 0
678 // TODO make this threaded again once KConfigGroup is sufficiently thread safe, bug #305361 and friends
679 // perform querying for the services in a thread
681 connect(watcher, &QFutureWatcher<LoadScriptList>::finished, this, &Scripting::slotScriptsQueried);
682 watcher->setFuture(QtConcurrent::run(this, &KWin::Scripting::queryScriptsToLoad, pluginStates, offers));
683#else
684 LoadScriptList scriptsToLoad = queryScriptsToLoad();
685 for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin();
686 it != scriptsToLoad.constEnd();
687 ++it) {
688 if (it->first) {
689 loadScript(it->second.first, it->second.second);
690 } else {
691 loadDeclarativeScript(it->second.first, it->second.second);
692 }
693 }
694
695 runScripts();
696#endif
697}
698
699LoadScriptList KWin::Scripting::queryScriptsToLoad()
700{
701 KSharedConfig::Ptr _config = kwinApp()->config();
702 static bool s_started = false;
703 if (s_started) {
704 _config->reparseConfiguration();
705 } else {
706 s_started = true;
707 }
708 QMap<QString, QString> pluginStates = KConfigGroup(_config, QStringLiteral("Plugins")).entryMap();
709 const QString scriptFolder = QStringLiteral("kwin/scripts/");
710 const auto offers = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), scriptFolder);
711
712 LoadScriptList scriptsToLoad;
713
714 for (const KPluginMetaData &service : offers) {
715 const QString value = pluginStates.value(service.pluginId() + QLatin1String("Enabled"), QString());
716 const bool enabled = value.isNull() ? service.isEnabledByDefault() : QVariant(value).toBool();
717 const bool javaScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("javascript");
718 const bool declarativeScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("declarativescript");
719 if (!javaScript && !declarativeScript) {
720 continue;
721 }
722
723 if (!enabled) {
724 if (isScriptLoaded(service.pluginId())) {
725 // unload the script
726 unloadScript(service.pluginId());
727 }
728 continue;
729 }
730 const QString pluginName = service.pluginId();
731 // 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
732 const QString relScriptPath = scriptFolder + pluginName + QLatin1String("/contents/") + (javaScript ? QLatin1String("code/main.js") : QLatin1String("ui/main.qml"));
733 const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, relScriptPath);
734 if (file.isEmpty()) {
735 qCDebug(KWIN_SCRIPTING) << "Could not find script file for " << pluginName;
736 continue;
737 }
738 scriptsToLoad << qMakePair(javaScript, qMakePair(file, pluginName));
739 }
740 return scriptsToLoad;
741}
742
743void KWin::Scripting::slotScriptsQueried()
744{
745 QFutureWatcher<LoadScriptList> *watcher = dynamic_cast<QFutureWatcher<LoadScriptList> *>(sender());
746 if (!watcher) {
747 // slot invoked not from a FutureWatcher
748 return;
749 }
750
751 LoadScriptList scriptsToLoad = watcher->result();
752 for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin();
753 it != scriptsToLoad.constEnd();
754 ++it) {
755 if (it->first) {
756 loadScript(it->second.first, it->second.second);
757 } else {
758 loadDeclarativeScript(it->second.first, it->second.second);
759 }
760 }
761
762 runScripts();
763 watcher->deleteLater();
764}
765
766bool KWin::Scripting::isScriptLoaded(const QString &pluginName) const
767{
768 return findScript(pluginName) != nullptr;
769}
770
771KWin::AbstractScript *KWin::Scripting::findScript(const QString &pluginName) const
772{
773 QMutexLocker locker(m_scriptsLock.get());
774 for (AbstractScript *script : std::as_const(scripts)) {
775 if (script->pluginName() == pluginName) {
776 return script;
777 }
778 }
779 return nullptr;
780}
781
782bool KWin::Scripting::unloadScript(const QString &pluginName)
783{
784 QMutexLocker locker(m_scriptsLock.get());
785 for (AbstractScript *script : std::as_const(scripts)) {
786 if (script->pluginName() == pluginName) {
787 script->deleteLater();
788 return true;
789 }
790 }
791 return false;
792}
793
794void KWin::Scripting::runScripts()
795{
796 QMutexLocker locker(m_scriptsLock.get());
797 for (int i = 0; i < scripts.size(); i++) {
798 scripts.at(i)->run();
799 }
800}
801
803{
804 QMutexLocker locker(m_scriptsLock.get());
805 scripts.removeAll(static_cast<KWin::Script *>(object));
806}
807
808int KWin::Scripting::loadScript(const QString &filePath, const QString &pluginName)
809{
810 QMutexLocker locker(m_scriptsLock.get());
811 if (isScriptLoaded(pluginName)) {
812 return -1;
813 }
814 const int id = scripts.size();
815 KWin::Script *script = new KWin::Script(id, filePath, pluginName, this);
816 connect(script, &QObject::destroyed, this, &Scripting::scriptDestroyed);
817 scripts.append(script);
818 return id;
819}
820
821int KWin::Scripting::loadDeclarativeScript(const QString &filePath, const QString &pluginName)
822{
823 QMutexLocker locker(m_scriptsLock.get());
824 if (isScriptLoaded(pluginName)) {
825 return -1;
826 }
827 const int id = scripts.size();
828 KWin::DeclarativeScript *script = new KWin::DeclarativeScript(id, filePath, pluginName, this);
829 connect(script, &QObject::destroyed, this, &Scripting::scriptDestroyed);
830 scripts.append(script);
831 return id;
832}
833
835{
836 QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Scripting"));
837 s_self = nullptr;
838}
839
841{
842 QList<QAction *> actions;
843 for (AbstractScript *s : std::as_const(scripts)) {
844 // TODO: Allow declarative scripts to add their own user actions.
845 if (Script *script = qobject_cast<Script *>(s)) {
846 actions << script->actionsForUserActionMenu(c, parent);
847 }
848 }
849 return actions;
850}
851
852#include "moc_scripting.cpp"
AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent=nullptr)
Definition scripting.cpp:98
KConfigGroup config() const
int scriptId() const
Definition scripting.h:48
~AbstractScript() override
Q_SCRIPTABLE void run() override
~DeclarativeScript() override
DeclarativeScript(int id, QString scriptName, QString pluginName, QObject *parent=nullptr)
JSEngineGlobalMethodsWrapper(DeclarativeScript *parent)
QVariant readConfig(const QString &key, QVariant defaultValue=QVariant())
void reserve(ElectricBorder border, QObject *object, const char *callback)
void unreserve(ElectricBorder border, QObject *object)
void reserveTouch(ElectricBorder border, QAction *action, TouchCallback::CallbackFunction callback=nullptr)
Q_INVOKABLE QVariant readConfig(const QString &key, const QVariant &defaultValue=QVariant())
Q_INVOKABLE bool unregisterTouchScreenEdge(int edge)
Script(int id, QString scriptName, QString pluginName, QObject *parent=nullptr)
Q_INVOKABLE void callDBus(const QString &service, const QString &path, const QString &interface, const QString &method, const QJSValue &arg1=QJSValue(), const QJSValue &arg2=QJSValue(), const QJSValue &arg3=QJSValue(), const QJSValue &arg4=QJSValue(), const QJSValue &arg5=QJSValue(), const QJSValue &arg6=QJSValue(), const QJSValue &arg7=QJSValue(), const QJSValue &arg8=QJSValue(), const QJSValue &arg9=QJSValue())
Q_INVOKABLE bool registerTouchScreenEdge(int edge, const QJSValue &callback)
void run() override
Q_INVOKABLE bool registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback)
QList< QAction * > actionsForUserActionMenu(Window *client, QMenu *parent)
Creates actions for the UserActionsMenu by invoking the registered callbacks.
virtual ~Script()
Q_INVOKABLE void registerUserActionsMenu(const QJSValue &callback)
Registers the given callback to be invoked whenever the UserActionsMenu is about to be showed....
Q_INVOKABLE bool unregisterScreenEdge(int edge)
Q_INVOKABLE bool registerScreenEdge(int edge, const QJSValue &callback)
Q_INVOKABLE ScriptTimer(QObject *parent=nullptr)
AbstractScript * findScript(const QString &pluginName) const
static Scripting * create(QObject *parent)
Q_SCRIPTABLE Q_INVOKABLE bool isScriptLoaded(const QString &pluginName) const
~Scripting() override
Q_SCRIPTABLE Q_INVOKABLE bool unloadScript(const QString &pluginName)
QList< QAction * > actionsForUserActionMenu(Window *c, QMenu *parent)
Invokes all registered callbacks to add actions to the UserActionsMenu.
Q_SCRIPTABLE Q_INVOKABLE int loadDeclarativeScript(const QString &filePath, const QString &pluginName=QString())
Q_SCRIPTABLE Q_INVOKABLE int loadScript(const QString &filePath, const QString &pluginName=QString())
static Scripting * self()
Definition scripting.h:393
void scriptDestroyed(QObject *object)
Q_SCRIPTABLE void start()
ScreenEdges * screenEdges() const
void workspaceInitialized()
static Workspace * self()
Definition workspace.h:91
void configChanged()
QVariant dbusToVariant(const QVariant &variant)
ElectricBorder
Definition globals.h:60
Workspace * workspace()
Definition workspace.h:830
Options * options
Definition main.cpp:73
QList< QPair< bool, QPair< QString, QString > > > LoadScriptList
true == javascript, false == qml
Definition scripting.h:35