diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -457,8 +457,8 @@ scripting/meta.cpp scripting/scriptedeffect.cpp scripting/scriptingutils.cpp - scripting/timer.cpp scripting/scripting_model.cpp + scripting/timer_wrapper.cpp scripting/dbuscall.cpp scripting/screenedgeitem.cpp scripting/scripting_logging.cpp diff --git a/autotests/integration/scripting/scripts/screenedge.js b/autotests/integration/scripting/scripts/screenedge.js --- a/autotests/integration/scripting/scripts/screenedge.js +++ b/autotests/integration/scripting/scripts/screenedge.js @@ -1 +1 @@ -registerScreenEdge(readConfig("Edge", 1), function() { workspace.slotToggleShowDesktop(); }); +registerScreenEdge(readConfig("Edge", 1), () => workspace.slotToggleShowDesktop()); diff --git a/autotests/integration/scripting/scripts/screenedgeunregister.js b/autotests/integration/scripting/scripts/screenedgeunregister.js --- a/autotests/integration/scripting/scripts/screenedgeunregister.js +++ b/autotests/integration/scripting/scripts/screenedgeunregister.js @@ -1,9 +1,9 @@ function init() { - var edge = readConfig("Edge", 1); + let edge = readConfig("Edge", 1); if (readConfig("mode", "") == "unregister") { unregisterScreenEdge(edge); } else { - registerScreenEdge(edge, function() { workspace.slotToggleShowDesktop(); }); + registerScreenEdge(edge, () => workspace.slotToggleShowDesktop()); } } options.configChanged.connect(init); diff --git a/autotests/integration/scripting/scripts/touchScreenedge.js b/autotests/integration/scripting/scripts/touchScreenedge.js --- a/autotests/integration/scripting/scripts/touchScreenedge.js +++ b/autotests/integration/scripting/scripts/touchScreenedge.js @@ -1 +1 @@ -registerTouchScreenEdge(readConfig("Edge", 1), function() { workspace.slotToggleShowDesktop(); }); +registerTouchScreenEdge(readConfig("Edge", 1), () => workspace.slotToggleShowDesktop()); diff --git a/scripting/dbuscall.cpp b/scripting/dbuscall.cpp --- a/scripting/dbuscall.cpp +++ b/scripting/dbuscall.cpp @@ -18,6 +18,7 @@ along with this program. If not, see . *********************************************************************/ #include "dbuscall.h" +#include "scriptingutils.h" #include #include @@ -46,7 +47,11 @@ emit failed(); return; } - emit finished(watcher->reply().arguments()); + QVariantList reply = watcher->reply().arguments(); + std::for_each(reply.begin(), reply.end(), [](QVariant &variant) { + variant = dbusToVariant(variant); + }); + emit finished(reply); }); } diff --git a/scripting/meta.h b/scripting/meta.h --- a/scripting/meta.h +++ b/scripting/meta.h @@ -29,14 +29,6 @@ class QScriptContext; class QSize; -namespace KWin { -class Client; -class Toplevel; -} - -typedef KWin::Client* KClientRef; -typedef KWin::Toplevel* KToplevelRef; - namespace KWin { namespace MetaScripting @@ -75,53 +67,12 @@ void fromScriptValue(const QScriptValue&, QRect&); } -namespace Client -{ -QScriptValue toScriptValue(QScriptEngine *eng, const KClientRef &client); -void fromScriptValue(const QScriptValue &value, KClientRef& client); -} - -namespace Toplevel -{ -QScriptValue toScriptValue(QScriptEngine *eng, const KToplevelRef &client); -void fromScriptValue(const QScriptValue &value, KToplevelRef& client); -} - -/** - * Merges the second QScriptValue in the first one. - **/ -void valueMerge(QScriptValue&, QScriptValue); - /** * Registers all the meta conversion to the provided QScriptEngine **/ void registration(QScriptEngine* eng); -/** - * Functions for the JS function objects, config.exists and config.get. - * Read scripting/IMPLIST for details on how they work - **/ -QScriptValue configExists(QScriptContext*, QScriptEngine*); -QScriptValue getConfigValue(QScriptContext*, QScriptEngine*); - -/** - * Provide a config object to the given QScriptEngine depending - * on the keys provided in the QVariant. The provided QVariant - * MUST returns (true) on isHash() - **/ -void supplyConfig(QScriptEngine*, const QVariant&); - -/** - * For engines whose scripts have no associated configuration. - **/ -void supplyConfig(QScriptEngine*); - } } -/** - * Code linked from plasma for QTimer. - **/ -QScriptValue constructTimerClass(QScriptEngine *eng); - #endif diff --git a/scripting/meta.cpp b/scripting/meta.cpp --- a/scripting/meta.cpp +++ b/scripting/meta.cpp @@ -19,8 +19,8 @@ *********************************************************************/ #include "meta.h" -#include "client.h" +#include #include using namespace KWin::MetaScripting; @@ -96,145 +96,12 @@ } // End of meta for QRect object -QScriptValue Client::toScriptValue(QScriptEngine *eng, const KClientRef &client) -{ - return eng->newQObject(client, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeChildObjects | - QScriptEngine::ExcludeDeleteLater | - QScriptEngine::PreferExistingWrapperObject | - QScriptEngine::AutoCreateDynamicProperties); -} - -void Client::fromScriptValue(const QScriptValue &value, KWin::Client* &client) -{ - client = qobject_cast(value.toQObject()); -} - -QScriptValue Toplevel::toScriptValue(QScriptEngine *eng, const KToplevelRef &client) -{ - return eng->newQObject(client, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeChildObjects | - QScriptEngine::ExcludeDeleteLater | - QScriptEngine::PreferExistingWrapperObject | - QScriptEngine::AutoCreateDynamicProperties); -} - -void Toplevel::fromScriptValue(const QScriptValue &value, KToplevelRef &client) -{ - client = qobject_cast(value.toQObject()); -} - // Other helper functions void KWin::MetaScripting::registration(QScriptEngine* eng) { qScriptRegisterMetaType(eng, Point::toScriptValue, Point::fromScriptValue); qScriptRegisterMetaType(eng, Size::toScriptValue, Size::fromScriptValue); qScriptRegisterMetaType(eng, Rect::toScriptValue, Rect::fromScriptValue); - qScriptRegisterMetaType(eng, Client::toScriptValue, Client::fromScriptValue); - qScriptRegisterMetaType(eng, Toplevel::toScriptValue, Toplevel::fromScriptValue); qScriptRegisterSequenceMetaType(eng); - qScriptRegisterSequenceMetaType< QList >(eng); -} - -QScriptValue KWin::MetaScripting::configExists(QScriptContext* ctx, QScriptEngine* eng) -{ - QHash scriptConfig = (((ctx->thisObject()).data()).toVariant()).toHash(); - QVariant val = scriptConfig.value((ctx->argument(0)).toString(), QVariant()); - - return eng->toScriptValue(val.isValid()); -} - -QScriptValue KWin::MetaScripting::getConfigValue(QScriptContext* ctx, QScriptEngine* eng) -{ - int num = ctx->argumentCount(); - QHash scriptConfig = (((ctx->thisObject()).data()).toVariant()).toHash(); - - /* - * Handle config.get() separately. Compute and return immediately. - **/ - if (num == 0) { - QHash::const_iterator i; - QScriptValue ret = eng->newArray(); - - for (i = scriptConfig.constBegin(); i != scriptConfig.constEnd(); ++i) { - ret.setProperty(i.key(), eng->newVariant(i.value())); - } - - return ret; - } - - if ((num == 1) && !((ctx->argument(0)).isArray())) { - QVariant val = scriptConfig.value((ctx->argument(0)).toString(), QVariant()); - - if (val.isValid()) { - return eng->newVariant(val); - } else { - return QScriptValue(); - } - } else { - QScriptValue ret = eng->newArray(); - int j = 0; - - if ((ctx->argument(0)).isArray()) { - bool simple = (num == 1) ? 0 : (ctx->argument(1)).toBool(); - QScriptValue array = (ctx->argument(0)); - int len = (array.property(QStringLiteral("length")).isValid()) ? array.property(QStringLiteral("length")).toNumber() : 0; - - for (int i = 0; i < len; i++) { - QVariant val = scriptConfig.value(array.property(i).toString(), QVariant()); - - if (val.isValid()) { - if (simple) { - ret.setProperty(j, eng->newVariant(val)); - } else { - ret.setProperty(array.property(i).toString(), eng->newVariant(val)); - } - - j++; - } - } - } else { - for (int i = 0; i < num; i++) { - QVariant val = scriptConfig.value((ctx->argument(i)).toString(), QVariant()); - - if (val.isValid()) { - ret.setProperty((ctx->argument(i)).toString(), eng->newVariant(val)); - j = 1; - } - } - } - - - if (j == 0) { - return QScriptValue(); - } else { - return ret; - } - } -} - -void KWin::MetaScripting::supplyConfig(QScriptEngine* eng, const QVariant& scriptConfig) -{ - QScriptValue configObject = eng->newObject(); - configObject.setData(eng->newVariant(scriptConfig)); - configObject.setProperty(QStringLiteral("get"), eng->newFunction(getConfigValue, 0), QScriptValue::Undeletable); - configObject.setProperty(QStringLiteral("exists"), eng->newFunction(configExists, 0), QScriptValue::Undeletable); - configObject.setProperty(QStringLiteral("loaded"), ((scriptConfig.toHash().empty()) ? eng->newVariant((bool)0) : eng->newVariant((bool)1)), QScriptValue::Undeletable); - (eng->globalObject()).setProperty(QStringLiteral("config"), configObject); -} - -void KWin::MetaScripting::supplyConfig(QScriptEngine* eng) -{ - KWin::MetaScripting::supplyConfig(eng, QVariant(QHash())); -} - -void KWin::MetaScripting::valueMerge(QScriptValue& first, QScriptValue second) -{ - QScriptValueIterator value_it(second); - - while (value_it.hasNext()) { - value_it.next(); - first.setProperty(value_it.name(), value_it.value()); - } } diff --git a/scripting/scripting.h b/scripting/scripting.h --- a/scripting/scripting.h +++ b/scripting/scripting.h @@ -4,6 +4,7 @@ Copyright (C) 2010 Rohan Prabhu Copyright (C) 2011 Martin Gräßlin +Copyright (C) 2019 Vlad Zagorodniy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,7 +28,7 @@ #include #include #include -#include +#include #include #include @@ -37,12 +38,8 @@ class QQmlContext; class QQmlEngine; class QAction; -class QDBusPendingCallWatcher; -class QGraphicsScene; class QMenu; class QMutex; -class QScriptEngine; -class QScriptValue; class QQuickWindow; class KConfigGroup; @@ -53,7 +50,6 @@ { class AbstractClient; class Client; -class ScriptUnloaderAgent; class QtScriptWorkspaceWrapper; class KWIN_EXPORT AbstractScript : public QObject @@ -69,8 +65,61 @@ return m_pluginName; } - void printMessage(const QString &message); - void registerShortcut(QAction *a, QScriptValue callback); + KConfigGroup config() const; + +public Q_SLOTS: + void stop(); + virtual void run() = 0; + +Q_SIGNALS: + void runningChanged(bool); + +protected: + bool running() const { + return m_running; + } + void setRunning(bool running) { + if (m_running == running) { + return; + } + m_running = running; + emit runningChanged(m_running); + } + int scriptId() const { + return m_scriptId; + } + +private: + int m_scriptId; + QString m_fileName; + QString m_pluginName; + bool m_running; +}; + +class Script : public AbstractScript, QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") + +public: + Script(int id, QString scriptName, QString pluginName, QObject *parent = nullptr); + virtual ~Script(); + + Q_INVOKABLE QVariant readConfig(const QString &key, const QVariant &defaultValue = QVariant()); + + 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 registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback); + + Q_INVOKABLE bool registerScreenEdge(int edge, const QJSValue &callback); + Q_INVOKABLE bool unregisterScreenEdge(int edge); + + Q_INVOKABLE bool registerTouchScreenEdge(int edge, const QJSValue &callback); + Q_INVOKABLE bool unregisterTouchScreenEdge(int edge); + /** * @brief Registers the given @p callback to be invoked whenever the UserActionsMenu is about * to be showed. In the callback the script can create a further sub menu or menu entry to be @@ -80,7 +129,8 @@ * @return void * @see actionsForUserActionMenu **/ - void registerUseractionsMenuCallback(QScriptValue callback); + Q_INVOKABLE void registerUserActionsMenu(const QJSValue &callback); + /** * @brief Creates actions for the UserActionsMenu by invoking the registered callbacks. * @@ -123,62 +173,38 @@ * @return QList< QAction* > List of QActions obtained from asking the registered callbacks * @see registerUseractionsMenuCallback **/ - QList actionsForUserActionMenu(AbstractClient *c, QMenu *parent); - - KConfigGroup config() const; - const QHash &shortcutCallbacks() const { - return m_shortcutCallbacks; - } - QHash > &screenEdgeCallbacks() { - return m_screenEdgeCallbacks; - } - - int registerCallback(QScriptValue value); + QList actionsForUserActionMenu(AbstractClient *client, QMenu *parent); public Q_SLOTS: - Q_SCRIPTABLE void stop(); - Q_SCRIPTABLE virtual void run() = 0; - void slotPendingDBusCall(QDBusPendingCallWatcher *watcher); + void run(); private Q_SLOTS: - void globalShortcutTriggered(); - bool borderActivated(ElectricBorder edge); /** - * @brief Slot invoked when a menu action is destroyed. Used to remove the action and callback - * from the map of actions. - * - * @param object The destroyed action + * Callback for when loadScriptFromFile has finished. **/ - void actionDestroyed(QObject *object); - -Q_SIGNALS: - Q_SCRIPTABLE void print(const QString &text); - void runningChanged(bool); + void slotScriptLoadedFromFile(); -protected: - bool running() const { - return m_running; - } - void setRunning(bool running) { - if (m_running == running) { - return; - } - m_running = running; - emit runningChanged(m_running); - } - int scriptId() const { - return m_scriptId; - } + /** + * Called when any reserve screen edge is triggered. + **/ + bool slotBorderActivated(ElectricBorder border); private: + /** + * Read the script from file into a byte array. + * If file cannot be read an empty byte array is returned. + **/ + QByteArray loadScriptFromFile(const QString &fileName); + /** * @brief Parses the @p value to either a QMenu or QAction. * * @param value The ScriptValue describing either a menu or action * @param parent The parent to use for the created menu or action * @return QAction* The parsed action or menu action, if parsing fails returns @c null. **/ - QAction *scriptValueToAction(QScriptValue &value, QMenu *parent); + QAction *scriptValueToAction(const QJSValue &value, QMenu *parent); + /** * @brief Creates a new QAction from the provided data and registers it for invoking the * @p callback when the action is triggered. @@ -193,84 +219,24 @@ * @param parent The parent to be used for the new created action * @return QAction* The created action **/ - QAction *createAction(const QString &title, bool checkable, bool checked, QScriptValue &callback, QMenu *parent); + QAction *createAction(const QString &title, const QJSValue &item, QMenu *parent); + /** * @brief Parses the @p items and creates a QMenu from it. * * @param title The title of the Menu. * @param items JavaScript Array containing Menu items. * @param parent The parent to use for the new created menu * @return QAction* The menu action for the new Menu **/ - QAction *createMenu(const QString &title, QScriptValue &items, QMenu *parent); - int m_scriptId; - QString m_fileName; - QString m_pluginName; - bool m_running; - QHash m_shortcutCallbacks; - QHash > m_screenEdgeCallbacks; - QHash m_callbacks; - /** - * @brief List of registered functions to call when the UserActionsMenu is about to show - * to add further entries. - **/ - QList m_userActionsMenuCallbacks; -}; - -class Script : public AbstractScript, QDBusContext -{ - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") -public: + QAction *createMenu(const QString &title, const QJSValue &items, QMenu *parent); - Script(int id, QString scriptName, QString pluginName, QObject *parent = nullptr); - virtual ~Script(); - QScriptEngine *engine() { - return m_engine; - } - - bool registerTouchScreenCallback(int edge, QScriptValue callback); - bool unregisterTouchScreenCallback(int edge); - -public Q_SLOTS: - Q_SCRIPTABLE void run(); - -Q_SIGNALS: - Q_SCRIPTABLE void printError(const QString &text); - -private Q_SLOTS: - /** - * A nice clean way to handle exceptions in scripting. - * TODO: Log to file, show from notifier.. - **/ - void sigException(const QScriptValue &exception); - /** - * Callback for when loadScriptFromFile has finished. - **/ - void slotScriptLoadedFromFile(); - -private: - void installScriptFunctions(QScriptEngine *engine); - /** - * Read the script from file into a byte array. - * If file cannot be read an empty byte array is returned. - **/ - QByteArray loadScriptFromFile(const QString &fileName); - QScriptEngine *m_engine; + QJSEngine *m_engine; QDBusMessage m_invocationContext; bool m_starting; - QScopedPointer m_agent; - QHash m_touchScreenEdgeCallbacks; -}; - -class ScriptUnloaderAgent : public QScriptEngineAgent -{ -public: - explicit ScriptUnloaderAgent(Script *script); - virtual void scriptUnload(qint64 id); - -private: - Script *m_script; + QHash m_screenEdgeCallbacks; + QHash m_touchScreenEdgeCallbacks; + QJSValueList m_userActionsMenuCallbacks; }; class DeclarativeScript : public AbstractScript diff --git a/scripting/scripting.cpp b/scripting/scripting.cpp --- a/scripting/scripting.cpp +++ b/scripting/scripting.cpp @@ -4,6 +4,7 @@ Copyright (C) 2010 Rohan Prabhu Copyright (C) 2011 Martin Gräßlin +Copyright (C) 2019 Vlad Zagorodniy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,12 +23,12 @@ #include "scripting.h" // own #include "dbuscall.h" -#include "meta.h" #include "scriptingutils.h" #include "workspace_wrapper.h" #include "screenedgeitem.h" #include "scripting_model.h" #include "scripting_logging.h" +#include "timer_wrapper.h" #include "../client.h" #include "../thumbnailitem.h" #include "../options.h" @@ -47,186 +48,9 @@ #include #include #include -#include -#include #include #include -QScriptValue kwinScriptPrint(QScriptContext *context, QScriptEngine *engine) -{ - KWin::AbstractScript *script = qobject_cast(context->callee().data().toQObject()); - if (!script) { - return engine->undefinedValue(); - } - QString result; - QTextStream stream(&result); - for (int i = 0; i < context->argumentCount(); ++i) { - if (i > 0) { - stream << " "; - } - QScriptValue argument = context->argument(i); - if (KWin::Client *client = qscriptvalue_cast(argument)) { - client->print(stream); - } else { - stream << argument.toString(); - } - } - script->printMessage(result); - - return engine->undefinedValue(); -} - -QScriptValue kwinScriptReadConfig(QScriptContext *context, QScriptEngine *engine) -{ - KWin::AbstractScript *script = qobject_cast(context->callee().data().toQObject()); - if (!script) { - return engine->undefinedValue(); - } - if (context->argumentCount() < 1 || context->argumentCount() > 2) { - qCDebug(KWIN_SCRIPTING) << "Incorrect number of arguments"; - return engine->undefinedValue(); - } - const QString key = context->argument(0).toString(); - QVariant defaultValue; - if (context->argumentCount() == 2) { - defaultValue = context->argument(1).toVariant(); - } - return engine->newVariant(script->config().readEntry(key, defaultValue)); -} - -QScriptValue kwinScriptGlobalShortcut(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::globalShortcut(context, engine); -} - -QScriptValue kwinAssertTrue(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::scriptingAssert(context, engine, 1, 2, true); -} - -QScriptValue kwinAssertFalse(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::scriptingAssert(context, engine, 1, 2, false); -} - -QScriptValue kwinAssertEquals(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::scriptingAssert(context, engine, 2, 3); -} - -QScriptValue kwinAssertNull(QScriptContext *context, QScriptEngine *engine) -{ - if (!KWin::validateParameters(context, 1, 2)) { - return engine->undefinedValue(); - } - if (!context->argument(0).isNull()) { - if (context->argumentCount() == 2) { - context->throwError(QScriptContext::UnknownError, context->argument(1).toString()); - } else { - context->throwError(QScriptContext::UnknownError, - i18nc("Assertion failed in KWin script with given value", - "Assertion failed: %1 is not null", context->argument(0).toString())); - } - return engine->undefinedValue(); - } - return true; -} - -QScriptValue kwinAssertNotNull(QScriptContext *context, QScriptEngine *engine) -{ - if (!KWin::validateParameters(context, 1, 2)) { - return engine->undefinedValue(); - } - if (context->argument(0).isNull()) { - if (context->argumentCount() == 2) { - context->throwError(QScriptContext::UnknownError, context->argument(1).toString()); - } else { - context->throwError(QScriptContext::UnknownError, - i18nc("Assertion failed in KWin script", - "Assertion failed: argument is null")); - } - return engine->undefinedValue(); - } - return true; -} - -QScriptValue kwinRegisterScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::registerScreenEdge(context, engine); -} - -QScriptValue kwinUnregisterScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::unregisterScreenEdge(context, engine); -} - -QScriptValue kwinRegisterTouchScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::registerTouchScreenEdge(context, engine); -} - -QScriptValue kwinUnregisterTouchScreenEdge(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::unregisterTouchScreenEdge(context, engine); -} - -QScriptValue kwinRegisterUserActionsMenu(QScriptContext *context, QScriptEngine *engine) -{ - return KWin::registerUserActionsMenu(context, engine); -} - -QScriptValue kwinCallDBus(QScriptContext *context, QScriptEngine *engine) -{ - KWin::AbstractScript *script = qobject_cast(context->callee().data().toQObject()); - if (!script) { - context->throwError(QScriptContext::UnknownError, QStringLiteral("Internal Error: script not registered")); - return engine->undefinedValue(); - } - if (context->argumentCount() < 4) { - context->throwError(QScriptContext::SyntaxError, - i18nc("Error in KWin Script", - "Invalid number of arguments. At least service, path, interface and method need to be provided")); - return engine->undefinedValue(); - } - if (!KWin::validateArgumentType(context)) { - context->throwError(QScriptContext::SyntaxError, - i18nc("Error in KWin Script", - "Invalid type. Service, path, interface and method need to be string values")); - return engine->undefinedValue(); - } - const QString service = context->argument(0).toString(); - const QString path = context->argument(1).toString(); - const QString interface = context->argument(2).toString(); - const QString method = context->argument(3).toString(); - int argumentsCount = context->argumentCount(); - if (context->argument(argumentsCount-1).isFunction()) { - --argumentsCount; - } - QDBusMessage msg = QDBusMessage::createMethodCall(service, path, interface, method); - QVariantList arguments; - for (int i=4; iargument(i).isArray()) { - QStringList stringArray = engine->fromScriptValue(context->argument(i)); - arguments << qVariantFromValue(stringArray); - } else { - arguments << context->argument(i).toVariant(); - } - } - if (!arguments.isEmpty()) { - msg.setArguments(arguments); - } - if (argumentsCount == context->argumentCount()) { - // no callback, just fire and forget - QDBusConnection::sessionBus().asyncCall(msg); - } else { - // with a callback - QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(msg), script); - watcher->setProperty("callback", script->registerCallback(context->argument(context->argumentCount()-1))); - QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), script, SLOT(slotPendingDBusCall(QDBusPendingCallWatcher*))); - } - return engine->undefinedValue(); -} - KWin::AbstractScript::AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent) : QObject(parent) , m_scriptId(id) @@ -253,204 +77,10 @@ deleteLater(); } -void KWin::AbstractScript::printMessage(const QString &message) -{ - qCDebug(KWIN_SCRIPTING) << fileName() << ":" << message; - emit print(message); -} - -void KWin::AbstractScript::registerShortcut(QAction *a, QScriptValue callback) -{ - m_shortcutCallbacks.insert(a, callback); - connect(a, SIGNAL(triggered(bool)), SLOT(globalShortcutTriggered())); -} - -void KWin::AbstractScript::globalShortcutTriggered() -{ - callGlobalShortcutCallback(this, sender()); -} - -bool KWin::AbstractScript::borderActivated(KWin::ElectricBorder edge) -{ - screenEdgeActivated(this, edge); - return true; -} - -void KWin::Script::installScriptFunctions(QScriptEngine* engine) -{ - // add our print - QScriptValue printFunc = engine->newFunction(kwinScriptPrint); - printFunc.setData(engine->newQObject(this)); - engine->globalObject().setProperty(QStringLiteral("print"), printFunc); - // add read config - QScriptValue configFunc = engine->newFunction(kwinScriptReadConfig); - configFunc.setData(engine->newQObject(this)); - engine->globalObject().setProperty(QStringLiteral("readConfig"), configFunc); - QScriptValue dbusCallFunc = engine->newFunction(kwinCallDBus); - dbusCallFunc.setData(engine->newQObject(this)); - engine->globalObject().setProperty(QStringLiteral("callDBus"), dbusCallFunc); - // add global Shortcut - registerGlobalShortcutFunction(this, engine, kwinScriptGlobalShortcut); - // add screen edge - registerScreenEdgeFunction(this, engine, kwinRegisterScreenEdge); - unregisterScreenEdgeFunction(this, engine, kwinUnregisterScreenEdge); - registerTouchScreenEdgeFunction(this, engine, kwinRegisterTouchScreenEdge); - unregisterTouchScreenEdgeFunction(this, engine, kwinUnregisterTouchScreenEdge); - - // add user actions menu register function - registerUserActionsMenuFunction(this, engine, kwinRegisterUserActionsMenu); - // add assertions - QScriptValue assertTrueFunc = engine->newFunction(kwinAssertTrue); - engine->globalObject().setProperty(QStringLiteral("assertTrue"), assertTrueFunc); - engine->globalObject().setProperty(QStringLiteral("assert"), assertTrueFunc); - QScriptValue assertFalseFunc = engine->newFunction(kwinAssertFalse); - engine->globalObject().setProperty(QStringLiteral("assertFalse"), assertFalseFunc); - QScriptValue assertEqualsFunc = engine->newFunction(kwinAssertEquals); - engine->globalObject().setProperty(QStringLiteral("assertEquals"), assertEqualsFunc); - QScriptValue assertNullFunc = engine->newFunction(kwinAssertNull); - engine->globalObject().setProperty(QStringLiteral("assertNull"), assertNullFunc); - engine->globalObject().setProperty(QStringLiteral("assertEquals"), assertEqualsFunc); - QScriptValue assertNotNullFunc = engine->newFunction(kwinAssertNotNull); - engine->globalObject().setProperty(QStringLiteral("assertNotNull"), assertNotNullFunc); - // global properties - engine->globalObject().setProperty(QStringLiteral("KWin"), engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject)); - QScriptValue workspace = engine->newQObject(Scripting::self()->workspaceWrapper(), QScriptEngine::QtOwnership, - QScriptEngine::ExcludeDeleteLater); - engine->globalObject().setProperty(QStringLiteral("workspace"), workspace, QScriptValue::Undeletable); - // install meta functions - KWin::MetaScripting::registration(engine); -} - -int KWin::AbstractScript::registerCallback(QScriptValue value) -{ - int id = m_callbacks.size(); - m_callbacks.insert(id, value); - return id; -} - -void KWin::AbstractScript::slotPendingDBusCall(QDBusPendingCallWatcher* watcher) -{ - if (watcher->isError()) { - qCDebug(KWIN_SCRIPTING) << "Received D-Bus message is error"; - watcher->deleteLater(); - return; - } - const int id = watcher->property("callback").toInt(); - QDBusMessage reply = watcher->reply(); - QScriptValue callback (m_callbacks.value(id)); - QScriptValueList arguments; - foreach (const QVariant &argument, reply.arguments()) { - arguments << callback.engine()->newVariant(argument); - } - callback.call(QScriptValue(), arguments); - m_callbacks.remove(id); - watcher->deleteLater(); -} - -void KWin::AbstractScript::registerUseractionsMenuCallback(QScriptValue callback) -{ - m_userActionsMenuCallbacks.append(callback); -} - -QList< QAction * > KWin::AbstractScript::actionsForUserActionMenu(KWin::AbstractClient *c, QMenu *parent) -{ - QList returnActions; - for (QList::const_iterator it = m_userActionsMenuCallbacks.constBegin(); it != m_userActionsMenuCallbacks.constEnd(); ++it) { - QScriptValue callback(*it); - QScriptValueList arguments; - arguments << callback.engine()->newQObject(c); - QScriptValue actions = callback.call(QScriptValue(), arguments); - if (!actions.isValid() || actions.isUndefined() || actions.isNull()) { - // script does not want to handle this Client - continue; - } - if (actions.isObject()) { - QAction *a = scriptValueToAction(actions, parent); - if (a) { - returnActions << a; - } - } - } - - return returnActions; -} - -QAction *KWin::AbstractScript::scriptValueToAction(QScriptValue &value, QMenu *parent) -{ - QScriptValue titleValue = value.property(QStringLiteral("text")); - QScriptValue checkableValue = value.property(QStringLiteral("checkable")); - QScriptValue checkedValue = value.property(QStringLiteral("checked")); - QScriptValue itemsValue = value.property(QStringLiteral("items")); - QScriptValue triggeredValue = value.property(QStringLiteral("triggered")); - - if (!titleValue.isValid()) { - // title not specified - does not make any sense to include - return nullptr; - } - const QString title = titleValue.toString(); - const bool checkable = checkableValue.isValid() && checkableValue.toBool(); - const bool checked = checkable && checkedValue.isValid() && checkedValue.toBool(); - // either a menu or a menu item - if (itemsValue.isValid()) { - if (!itemsValue.isArray()) { - // not an array, so cannot be a menu - return nullptr; - } - QScriptValue lengthValue = itemsValue.property(QStringLiteral("length")); - if (!lengthValue.isValid() || !lengthValue.isNumber() || lengthValue.toInteger() == 0) { - // length property missing - return nullptr; - } - return createMenu(title, itemsValue, parent); - } else if (triggeredValue.isValid()) { - // normal item - return createAction(title, checkable, checked, triggeredValue, parent); - } - return nullptr; -} - -QAction *KWin::AbstractScript::createAction(const QString &title, bool checkable, bool checked, QScriptValue &callback, QMenu *parent) -{ - QAction *action = new QAction(title, parent); - action->setCheckable(checkable); - action->setChecked(checked); - // TODO: rename m_shortcutCallbacks - m_shortcutCallbacks.insert(action, callback); - connect(action, SIGNAL(triggered(bool)), SLOT(globalShortcutTriggered())); - connect(action, SIGNAL(destroyed(QObject*)), SLOT(actionDestroyed(QObject*))); - return action; -} - -QAction *KWin::AbstractScript::createMenu(const QString &title, QScriptValue &items, QMenu *parent) -{ - QMenu *menu = new QMenu(title, parent); - const int length = static_cast(items.property(QStringLiteral("length")).toInteger()); - for (int i=0; iaddAction(a); - } - } - } - return menu->menuAction(); -} - -void KWin::AbstractScript::actionDestroyed(QObject *object) -{ - // TODO: Qt 5 - change to lambda function - m_shortcutCallbacks.remove(static_cast(object)); -} - KWin::Script::Script(int id, QString scriptName, QString pluginName, QObject* parent) : AbstractScript(id, scriptName, pluginName, parent) - , m_engine(new QScriptEngine(this)) + , m_engine(new QJSEngine(this)) , m_starting(false) - , m_agent(new ScriptUnloaderAgent(this)) { QDBusConnection::sessionBus().registerObject(QLatin1Char('/') + QString::number(scriptId()), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables); } @@ -508,18 +138,74 @@ return; } - QScriptValue optionsValue = m_engine->newQObject(options, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeSuperClassContents | QScriptEngine::ExcludeDeleteLater); - m_engine->globalObject().setProperty(QStringLiteral("options"), optionsValue, QScriptValue::Undeletable); - m_engine->globalObject().setProperty(QStringLiteral("QTimer"), constructTimerClass(m_engine)); - QObject::connect(m_engine, SIGNAL(signalHandlerException(QScriptValue)), this, SLOT(sigException(QScriptValue))); - KWin::MetaScripting::supplyConfig(m_engine); - installScriptFunctions(m_engine); - - QScriptValue ret = m_engine->evaluate(QString::fromUtf8(watcher->result())); - - if (ret.isError()) { - sigException(ret); + // Install console functions (e.g. console.assert(), console.log(), etc). + m_engine->installExtensions(QJSEngine::ConsoleExtension); + + // Make the timer visible to QJSEngine. + QJSValue timerMetaObject = m_engine->newQMetaObject(&TimerWrapper::staticMetaObject); + m_engine->globalObject().setProperty("QTimer", timerMetaObject); + m_engine->globalObject().setProperty("Timer", timerMetaObject); + + // Expose enums. + m_engine->globalObject().setProperty(QStringLiteral("KWin"), m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject)); + + // Make the options object visible to QJSEngine. + QJSValue optionsObject = m_engine->newQObject(options); + QQmlEngine::setObjectOwnership(options, QQmlEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("options"), optionsObject); + + // Make the workspace visible to QJSEngine. + QJSValue workspaceObject = m_engine->newQObject(Scripting::self()->workspaceWrapper()); + QQmlEngine::setObjectOwnership(Scripting::self()->workspaceWrapper(), QQmlEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("workspace"), workspaceObject); + + QJSValue self = m_engine->newQObject(this); + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("readConfig"), self.property(QStringLiteral("readConfig"))); + m_engine->globalObject().setProperty(QStringLiteral("callDBus"), self.property(QStringLiteral("callDBus"))); + m_engine->globalObject().setProperty(QStringLiteral("registerShortcut"), self.property(QStringLiteral("registerShortcut"))); + m_engine->globalObject().setProperty(QStringLiteral("registerScreenEdge"), self.property(QStringLiteral("registerScreenEdge"))); + m_engine->globalObject().setProperty(QStringLiteral("unregisterScreenEdge"), self.property(QStringLiteral("unregisterScreenEdge"))); + m_engine->globalObject().setProperty(QStringLiteral("registerTouchScreenEdge"), self.property(QStringLiteral("registerTouchScreenEdge"))); + m_engine->globalObject().setProperty(QStringLiteral("unregisterTouchScreenEdge"), self.property(QStringLiteral("unregisterTouchScreenEdge"))); + m_engine->globalObject().setProperty(QStringLiteral("registerUserActionsMenu"), self.property(QStringLiteral("registerUserActionsMenu"))); + + // Inject assertion functions. It would be better to create a module with all + // this assert functions or just deprecate them in favor of console.assert(). + QJSValue result = m_engine->evaluate(QStringLiteral( + "function assert(condition, message) {" + " console.assert(condition, message || 'Assertion failed');" + "}" + "" + "function assertTrue(condition, message) {" + " console.assert(condition, message || 'Assertion failed');" + "}" + "" + "function assertFalse(condition, message) {" + " console.assert(!condition, message || 'Assertion failed');" + "}" + "" + "function assertNull(value, message) {" + " console.assert(value === null, message || 'Assertion failed');" + "}" + "" + "function assertNotNull(value, message) {" + " console.assert(value !== null, message || 'Assertion failed');" + "}" + "" + "function assertEquals(expected, actual, message) {" + " console.assert(expected === actual, message || 'Assertion failed');" + "}" + )); + Q_ASSERT(!result.isError()); + + result = m_engine->evaluate(QString::fromUtf8(watcher->result()), fileName()); + if (result.isError()) { + const QString message = fileName() + QLatin1String(":") + + result.property(QStringLiteral("lineNumber")).toString() + + QLatin1String(": error: ") + + result.property(QStringLiteral("message")).toString(); + qCWarning(KWIN_SCRIPTING) << qPrintable(message); deleteLater(); } @@ -534,63 +220,277 @@ m_starting = false; } -void KWin::Script::sigException(const QScriptValue& exception) +QVariant KWin::Script::readConfig(const QString &key, const QVariant &defaultValue) { - QScriptValue ret = exception; - if (ret.isError()) { - qCDebug(KWIN_SCRIPTING) << "defaultscript encountered an error at [Line " << m_engine->uncaughtExceptionLineNumber() << "]"; - qCDebug(KWIN_SCRIPTING) << "Message: " << ret.toString(); - qCDebug(KWIN_SCRIPTING) << "-----------------"; + return config().readEntry(key, defaultValue); +} - QScriptValueIterator iter(ret); - while (iter.hasNext()) { - iter.next(); - qCDebug(KWIN_SCRIPTING) << " " << iter.name() << ": " << iter.value().toString(); - } +void KWin::Script::callDBus(const QString &service, const QString &path, const QString &interface, + const QString &method, const QJSValue &arg1, const QJSValue &arg2, const QJSValue &arg3, + const QJSValue &arg4, const QJSValue &arg5, const QJSValue &arg6, const QJSValue &arg7, + const QJSValue &arg8, const QJSValue &arg9) +{ + QJSValueList jsArguments; + jsArguments.reserve(9); + + if (!arg1.isUndefined()) { + jsArguments << arg1; + } + if (!arg2.isUndefined()) { + jsArguments << arg2; + } + if (!arg3.isUndefined()) { + jsArguments << arg3; + } + if (!arg4.isUndefined()) { + jsArguments << arg4; + } + if (!arg5.isUndefined()) { + jsArguments << arg5; + } + if (!arg6.isUndefined()) { + jsArguments << arg6; + } + if (!arg7.isUndefined()) { + jsArguments << arg7; + } + if (!arg8.isUndefined()) { + jsArguments << arg8; + } + if (!arg9.isUndefined()) { + jsArguments << arg9; } - emit printError(exception.toString()); - stop(); + + QJSValue callback; + if (!jsArguments.isEmpty() && jsArguments.last().isCallable()) { + callback = jsArguments.takeLast(); + } + + QVariantList dbusArguments; + dbusArguments.reserve(jsArguments.count()); + for (const QJSValue &jsArgument : jsArguments) { + dbusArguments << jsArgument.toVariant(); + } + + QDBusMessage message = QDBusMessage::createMethodCall(service, path, interface, method); + message.setArguments(dbusArguments); + + const QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message); + if (callback.isUndefined()) { + return; + } + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this, callback](QDBusPendingCallWatcher *self) { + self->deleteLater(); + + if (self->isError()) { + qCDebug(KWIN_SCRIPTING) << "Received D-Bus message is error"; + return; + } + + QJSValueList arguments; + const QVariantList reply = self->reply().arguments(); + for (const QVariant &variant : reply) { + arguments << m_engine->toScriptValue(dbusToVariant(variant)); + } + + QJSValue(callback).call(arguments); + } + ); } -bool KWin::Script::registerTouchScreenCallback(int edge, QScriptValue callback) +bool KWin::Script::registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback) { - if (m_touchScreenEdgeCallbacks.constFind(edge) != m_touchScreenEdgeCallbacks.constEnd()) { + if (!callback.isCallable()) { return false; } + QAction *action = new QAction(this); + action->setObjectName(objectName); + action->setText(text); + + const QKeySequence shortcut = keySequence; + KGlobalAccel::self()->setShortcut(action, { shortcut }); + input()->registerShortcut(shortcut, action); + connect(action, &QAction::triggered, this, - [callback] { - QScriptValue invoke(callback); - invoke.call(); + [this, action, callback] { + QJSValue(callback).call({ m_engine->toScriptValue(action) }); } ); + + return true; +} + +bool KWin::Script::registerScreenEdge(int edge, const QJSValue &callback) +{ + if (!callback.isCallable()) { + return false; + } + + QJSValueList &callbacks = m_screenEdgeCallbacks[edge]; + if (callbacks.isEmpty()) { + ScreenEdges::self()->reserve(static_cast(edge), this, "slotBorderActivated"); + } + + callbacks << callback; + + return true; +} + +bool KWin::Script::unregisterScreenEdge(int edge) +{ + auto it = m_screenEdgeCallbacks.find(edge); + if (it == m_screenEdgeCallbacks.end()) { + return false; + } + + ScreenEdges::self()->unreserve(static_cast(edge), this); + m_screenEdgeCallbacks.erase(it); + + return true; +} + +bool KWin::Script::registerTouchScreenEdge(int edge, const QJSValue &callback) +{ + if (!callback.isCallable()) { + return false; + } + if (m_touchScreenEdgeCallbacks.contains(edge)) { + return false; + } + + QAction *action = new QAction(this); ScreenEdges::self()->reserveTouch(KWin::ElectricBorder(edge), action); m_touchScreenEdgeCallbacks.insert(edge, action); + + connect(action, &QAction::triggered, this, + [callback] { + QJSValue(callback).call(); + } + ); + return true; } -bool KWin::Script::unregisterTouchScreenCallback(int edge) +bool KWin::Script::unregisterTouchScreenEdge(int edge) { auto it = m_touchScreenEdgeCallbacks.find(edge); if (it == m_touchScreenEdgeCallbacks.end()) { return false; } + delete it.value(); m_touchScreenEdgeCallbacks.erase(it); + + return true; +} + +void KWin::Script::registerUserActionsMenu(const QJSValue &callback) +{ + if (!callback.isCallable()) { + return; + } + m_userActionsMenuCallbacks.append(callback); +} + +QList KWin::Script::actionsForUserActionMenu(KWin::AbstractClient *client, QMenu *parent) +{ + QList actions; + actions.reserve(m_userActionsMenuCallbacks.count()); + + for (QJSValue callback : m_userActionsMenuCallbacks) { + QJSValue result = callback.call({ m_engine->toScriptValue(client) }); + if (result.isError()) { + continue; + } + if (!result.isObject()) { + continue; + } + if (QAction *action = scriptValueToAction(result, parent)) { + actions << action; + } + } + + return actions; +} + +bool KWin::Script::slotBorderActivated(ElectricBorder border) +{ + const QJSValueList callbacks = m_screenEdgeCallbacks.value(border); + if (callbacks.isEmpty()) { + return false; + } + std::for_each(callbacks.begin(), callbacks.end(), [](QJSValue callback) { + callback.call(); + }); return true; } -KWin::ScriptUnloaderAgent::ScriptUnloaderAgent(KWin::Script *script) - : QScriptEngineAgent(script->engine()) - , m_script(script) +QAction *KWin::Script::scriptValueToAction(const QJSValue &value, QMenu *parent) +{ + const QString title = value.property(QStringLiteral("text")).toString(); + if (title.isEmpty()) { + return nullptr; + } + + // Either a menu or a menu item. + const QJSValue itemsValue = value.property(QStringLiteral("items")); + if (!itemsValue.isUndefined()) { + return createMenu(title, itemsValue, parent); + } + + return createAction(title, value, parent); +} + +QAction *KWin::Script::createAction(const QString &title, const QJSValue &item, QMenu *parent) { - script->engine()->setAgent(this); + const QJSValue callback = item.property(QStringLiteral("triggered")); + if (!callback.isCallable()) { + return nullptr; + } + + const bool checkable = item.property(QStringLiteral("checkable")).toBool(); + const bool checked = item.property(QStringLiteral("checked")).toBool(); + + QAction *action = new QAction(title, parent); + action->setCheckable(checkable); + action->setChecked(checked); + + connect(action, &QAction::triggered, this, + [this, action, callback] { + QJSValue(callback).call({ m_engine->toScriptValue(action) }); + } + ); + + return action; } -void KWin::ScriptUnloaderAgent::scriptUnload(qint64 id) +QAction *KWin::Script::createMenu(const QString &title, const QJSValue &items, QMenu *parent) { - Q_UNUSED(id) - m_script->stop(); + if (!items.isArray()) { + return nullptr; + } + + const int length = items.property(QStringLiteral("length")).toInt(); + if (!length) { + return nullptr; + } + + QMenu *menu = new QMenu(title, parent); + for (int i = 0; i < length; ++i) { + const QJSValue value = items.property(QString::number(i)); + if (!value.isObject()) { + continue; + } + if (QAction *action = scriptValueToAction(value, menu)) { + menu->addAction(action); + } + } + + return menu->menuAction(); } KWin::DeclarativeScript::DeclarativeScript(int id, QString scriptName, QString pluginName, QObject* parent) @@ -890,8 +790,11 @@ QList< QAction * > KWin::Scripting::actionsForUserActionMenu(KWin::AbstractClient *c, QMenu *parent) { QList actions; - foreach (AbstractScript *script, scripts) { - actions << script->actionsForUserActionMenu(c, parent); + for (AbstractScript *s : scripts) { + // TODO: Allow declarative scripts to add their own user actions. + if (Script *script = qobject_cast