diff --git a/autotests/integration/scripting/screenedge_test.cpp b/autotests/integration/scripting/screenedge_test.cpp index e82861932..7ca92b56e 100644 --- a/autotests/integration/scripting/screenedge_test.cpp +++ b/autotests/integration/scripting/screenedge_test.cpp @@ -1,149 +1,217 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "cursor.h" #include "effectloader.h" #include "platform.h" #include "wayland_server.h" #include "workspace.h" #include "scripting/scripting.h" #include "effect_builtins.h" +#include "workspace.h" + +#define private public +#include "screenedge.h" +#undef private #include Q_DECLARE_METATYPE(KWin::ElectricBorder) using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_scripting_screenedge-0"); class ScreenEdgeTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testEdge_data(); void testEdge(); + void testEdgeUnregister(); + +private: + void triggerConfigReload(); }; void ScreenEdgeTest::initTestCase() { QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); // empty config to have defaults auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); // disable all effects to prevent them grabbing edges KConfigGroup plugins(config, QStringLiteral("Plugins")); ScriptedEffectLoader loader; const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); for (QString name : builtinNames) { plugins.writeEntry(name + QStringLiteral("Enabled"), false); } - // disable electric border pushaback + // disable electric border pushback config->group("Windows").writeEntry("ElectricBorderPushbackPixels", 0); config->sync(); kwinApp()->setConfig(config); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QVERIFY(Scripting::self()); + + ScreenEdges::self()->setTimeThreshold(0); + ScreenEdges::self()->setReActivationThreshold(0); } void ScreenEdgeTest::init() { KWin::Cursor::setPos(640, 512); if (workspace()->showingDesktop()) { workspace()->slotToggleShowDesktop(); } QVERIFY(!workspace()->showingDesktop()); } void ScreenEdgeTest::cleanup() { // try to unload the script - const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedge.js"); - if (!scriptToLoad.isEmpty()) { - if (Scripting::self()->isScriptLoaded(scriptToLoad)) { - QVERIFY(Scripting::self()->unloadScript(scriptToLoad)); - QTRY_VERIFY(!Scripting::self()->isScriptLoaded(scriptToLoad)); + const QStringList scripts = {QFINDTESTDATA("./scripts/screenedge.js"), QFINDTESTDATA("./scripts/screenedgeunregister.js")}; + for (const QString &script: scripts) { + if (!script.isEmpty()) { + if (Scripting::self()->isScriptLoaded(script)) { + QVERIFY(Scripting::self()->unloadScript(script)); + QTRY_VERIFY(!Scripting::self()->isScriptLoaded(script)); + } } } } void ScreenEdgeTest::testEdge_data() { QTest::addColumn("edge"); QTest::addColumn("triggerPos"); QTest::newRow("Top") << KWin::ElectricTop << QPoint(512, 0); QTest::newRow("TopRight") << KWin::ElectricTopRight << QPoint(1279, 0); QTest::newRow("Right") << KWin::ElectricRight << QPoint(1279, 512); QTest::newRow("BottomRight") << KWin::ElectricBottomRight << QPoint(1279, 1023); QTest::newRow("Bottom") << KWin::ElectricBottom << QPoint(512, 1023); QTest::newRow("BottomLeft") << KWin::ElectricBottomLeft << QPoint(0, 1023); QTest::newRow("Left") << KWin::ElectricLeft << QPoint(0, 512); QTest::newRow("TopLeft") << KWin::ElectricTopLeft << QPoint(0, 0); + + //repeat a row to show previously unloading and re-registering works + QTest::newRow("Top") << KWin::ElectricTop << QPoint(512, 0); } void ScreenEdgeTest::testEdge() { const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedge.js"); QVERIFY(!scriptToLoad.isEmpty()); // mock the config auto config = kwinApp()->config(); QFETCH(KWin::ElectricBorder, edge); config->group(QLatin1String("Script-") + scriptToLoad).writeEntry("Edge", int(edge)); config->sync(); QVERIFY(!Scripting::self()->isScriptLoaded(scriptToLoad)); const int id = Scripting::self()->loadScript(scriptToLoad); QVERIFY(id != -1); QVERIFY(Scripting::self()->isScriptLoaded(scriptToLoad)); auto s = Scripting::self()->findScript(scriptToLoad); QVERIFY(s); QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); QVERIFY(runningChangedSpy.isValid()); s->run(); QVERIFY(runningChangedSpy.wait()); QCOMPARE(runningChangedSpy.count(), 1); QCOMPARE(runningChangedSpy.first().first().toBool(), true); // triggering the edge will result in show desktop being triggered QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); QVERIFY(showDesktopSpy.isValid()); // trigger the edge QFETCH(QPoint, triggerPos); KWin::Cursor::setPos(triggerPos); QCOMPARE(showDesktopSpy.count(), 1); QVERIFY(workspace()->showingDesktop()); } +void ScreenEdgeTest::triggerConfigReload() { + workspace()->slotReconfigure(); +} + +void ScreenEdgeTest::testEdgeUnregister() +{ + const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedgeunregister.js"); + QVERIFY(!scriptToLoad.isEmpty()); + + Scripting::self()->loadScript(scriptToLoad); + auto s = Scripting::self()->findScript(scriptToLoad); + auto configGroup = s->config(); + configGroup.writeEntry("Edge", int(KWin::ElectricLeft)); + configGroup.sync(); + const QPoint triggerPos = QPoint(0, 512); + + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + QVERIFY(runningChangedSpy.wait()); + + QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); + QVERIFY(showDesktopSpy.isValid()); + + //trigger the edge + KWin::Cursor::setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + //reset + KWin::Cursor::setPos(500,500); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + //trigger again, to show that retriggering works + KWin::Cursor::setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + //reset + KWin::Cursor::setPos(500,500); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + //make the script unregister the edge + configGroup.writeEntry("mode", "unregister"); + triggerConfigReload(); + KWin::Cursor::setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 0); //not triggered + + //force the script to unregister a non-registered edge to prove it doesn't explode + triggerConfigReload(); +} + WAYLANDTEST_MAIN(ScreenEdgeTest) #include "screenedge_test.moc" diff --git a/autotests/integration/scripting/scripts/screenedgeunregister.js b/autotests/integration/scripting/scripts/screenedgeunregister.js new file mode 100644 index 000000000..a73b68e7f --- /dev/null +++ b/autotests/integration/scripting/scripts/screenedgeunregister.js @@ -0,0 +1,12 @@ +function init() { + var edge = readConfig("Edge", 1); + if (readConfig("mode", "") == "unregister") { + unregisterScreenEdge(edge); + } else { + registerScreenEdge(edge, function() { workspace.slotToggleShowDesktop(); }); + } +} +options.configChanged.connect(init); + +init(); + diff --git a/scripting/documentation-global.xml b/scripting/documentation-global.xml index 6ab70cd34..af4d9a0d5 100644 --- a/scripting/documentation-global.xml +++ b/scripting/documentation-global.xml @@ -1,134 +1,142 @@ Global Methods and properties added to the global JavaScript object. KWin::Options options Global property to all configuration values of KWin core. KWin::Workspace workspace Global property to the core wrapper of KWin. object KWin Provides access to enums defined in KWin::WorkspaceWrapper Q_SCRIPTABLE void void KWin::Scripting::print (QVariant ... values) print Prints all provided values to kDebug and as a D-Bus signal Q_SCRIPTABLE QVariant QVariant KWin::Scripting::readConfig (QString key, QVariant defaultValue = QVariant()) readConfig Reads the config value for key in the Script's configuration with the optional default value. If not providing a default value and no value stored in the configuration an undefined value is returned. Q_SCRIPTABLE bool bool KWin::Scripting::registerScreenEdge (ElectricBorder border, QScriptValue callback) registerScreenEdge Registers the callback for the screen edge. When the mouse gets pushed against the given edge the callback will be invoked. + + Q_SCRIPTABLE bool + bool KWin::Scripting::unregisterScreenEdge + (ElectricBorder border) + unregisterScreenEdge + + Unregisters the callback for the screen edge. This will disconnect all callbacks from this script to that edge. + Q_SCRIPTABLE bool bool KWin::Scripting::registerShortcut (QString title, QString text, QString keySequence, QScriptValue callback) registerShortcut Registers keySequence as a global shortcut. When the shortcut is invoked the callback will be called. Title and text are used to name the shortcut and make it available to the global shortcut configuration module. Q_SCRIPTABLE bool bool KWin::Scripting::assert (bool value, QString message = QString()) assert Aborts the execution of the script if value does not evaluate to true. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. Q_SCRIPTABLE bool bool KWin::Scripting::assertTrue (bool value, QString message = QString()) assertTrue Aborts the execution of the script if value does not evaluate to true. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. Q_SCRIPTABLE bool bool KWin::Scripting::assertFalse (bool value, QString message = QString()) assertFalse Aborts the execution of the script if value does not evaluate to false. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. Q_SCRIPTABLE bool bool KWin::Scripting::assertEquals (QVariant expected, QVariant actual, QString message = QString()) assertEquals Aborts the execution of the script if the actual value is not equal to the expected value. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. Q_SCRIPTABLE bool bool KWin::Scripting::assertNull (QVariant value, QString message = QString()) assertNull Aborts the execution of the script if value is not null. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. Q_SCRIPTABLE bool bool KWin::Scripting::assertNotNull (QVariant value, QString message = QString()) assertNotNull Aborts the execution of the script if value is null. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. Q_SCRIPTABLE void void KWin::Scripting::callDBus (QString service, QString path, QString interface, QString method, QVariant arg..., QScriptValue callback = QScriptValue()) callDBus Call a D-Bus method at (service, path, interface and method). A variable number of arguments can be added to the method call. The D-Bus call is always performed in an async way invoking the callback provided as the last (optional) argument. The reply values of the D-Bus method call are passed to the callback. Q_SCRIPTABLE void void KWin::Scripting::registerUserActionsMenu (QScriptValue callback) registerUserActionsMenu Registers the passed in callback to be invoked whenever the User actions menu (Alt+F3 or right click on window decoration) is about to be shown. The callback is invoked with a reference to the Client for which the menu is shown. The callback can return either a single menu entry to be added to the menu or an own sub menu with multiple entries. The object for a menu entry should be {title: "My Menu entry", checkable: true, checked: false, triggered: function (action) { // callback with triggered QAction}}, for a menu it should be {title: "My menu", items: [{...}, {...}, ...] /*list with entries as described*/} diff --git a/scripting/scripting.cpp b/scripting/scripting.cpp index 7ce6d59fd..b63bee6cb 100644 --- a/scripting/scripting.cpp +++ b/scripting/scripting.cpp @@ -1,802 +1,810 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2010 Rohan Prabhu Copyright (C) 2011 Martin Gräßlin 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #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 "../client.h" #include "../thumbnailitem.h" #include "../options.h" #include "../workspace.h" // KDE #include #include // Qt #include #include #include #include #include #include #include #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 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) , m_pluginName(pluginName) , m_running(false) { m_scriptFile.setFileName(scriptName); if (m_pluginName.isNull()) { m_pluginName = scriptName; } } KWin::AbstractScript::~AbstractScript() { } KConfigGroup KWin::AbstractScript::config() const { return kwinApp()->config()->group(QLatin1String("Script-") + m_pluginName); } void KWin::AbstractScript::stop() { deleteLater(); } void KWin::AbstractScript::printMessage(const QString &message) { qCDebug(KWIN_SCRIPTING) << scriptFile().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); + // add user actions menu register function regesterUserActionsMenuFunction(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(&WorkspaceWrapper::staticMetaObject)); QScriptValue workspace = engine->newQObject(Scripting::self()->workspaceWrapper(), QScriptEngine::QtOwnership, QScriptEngine::ExcludeSuperClassContents | 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_starting(false) , m_agent(new ScriptUnloaderAgent(this)) { QDBusConnection::sessionBus().registerObject(QLatin1Char('/') + QString::number(scriptId()), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables); } KWin::Script::~Script() { QDBusConnection::sessionBus().unregisterObject(QLatin1Char('/') + QString::number(scriptId())); } void KWin::Script::run() { if (running() || m_starting) { return; } m_starting = true; QFutureWatcher *watcher = new QFutureWatcher(this); connect(watcher, SIGNAL(finished()), SLOT(slotScriptLoadedFromFile())); watcher->setFuture(QtConcurrent::run(this, &KWin::Script::loadScriptFromFile)); } QByteArray KWin::Script::loadScriptFromFile() { if (!scriptFile().open(QIODevice::ReadOnly)) { return QByteArray(); } QByteArray result(scriptFile().readAll()); scriptFile().close(); return result; } void KWin::Script::slotScriptLoadedFromFile() { QFutureWatcher *watcher = dynamic_cast< QFutureWatcher< QByteArray>* >(sender()); if (!watcher) { // not invoked from a QFutureWatcher return; } if (watcher->result().isNull()) { // do not load empty script deleteLater(); watcher->deleteLater(); 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); deleteLater(); } watcher->deleteLater(); setRunning(true); m_starting = false; } void KWin::Script::sigException(const QScriptValue& exception) { 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) << "-----------------"; QScriptValueIterator iter(ret); while (iter.hasNext()) { iter.next(); qCDebug(KWIN_SCRIPTING) << " " << iter.name() << ": " << iter.value().toString(); } } emit printError(exception.toString()); stop(); } KWin::ScriptUnloaderAgent::ScriptUnloaderAgent(KWin::Script *script) : QScriptEngineAgent(script->engine()) , m_script(script) { script->engine()->setAgent(this); } void KWin::ScriptUnloaderAgent::scriptUnload(qint64 id) { Q_UNUSED(id) m_script->stop(); } KWin::DeclarativeScript::DeclarativeScript(int id, QString scriptName, QString pluginName, QObject* parent) : AbstractScript(id, scriptName, pluginName, parent) , m_context(new QQmlContext(Scripting::self()->qmlEngine(), this)) , m_component(new QQmlComponent(Scripting::self()->qmlEngine(), this)) { m_context->setContextProperty(QStringLiteral("KWin"), new JSEngineGlobalMethodsWrapper(this)); } KWin::DeclarativeScript::~DeclarativeScript() { } void KWin::DeclarativeScript::run() { if (running()) { return; } m_component->loadUrl(QUrl::fromLocalFile(scriptFile().fileName())); if (m_component->isLoading()) { connect(m_component, &QQmlComponent::statusChanged, this, &DeclarativeScript::createComponent); } else { createComponent(); } } void KWin::DeclarativeScript::createComponent() { if (m_component->isError()) { qCDebug(KWIN_SCRIPTING) << "Component failed to load: " << m_component->errors(); } else { if (QObject *object = m_component->create(m_context)) { object->setParent(this); } } setRunning(true); } KWin::JSEngineGlobalMethodsWrapper::JSEngineGlobalMethodsWrapper(KWin::DeclarativeScript *parent) : QObject(parent) , m_script(parent) { } KWin::JSEngineGlobalMethodsWrapper::~JSEngineGlobalMethodsWrapper() { } QVariant KWin::JSEngineGlobalMethodsWrapper::readConfig(const QString &key, QVariant defaultValue) { return m_script->config().readEntry(key, defaultValue); } void KWin::JSEngineGlobalMethodsWrapper::registerWindow(QQuickWindow *window) { connect(window, &QWindow::visibilityChanged, [window](QWindow::Visibility visibility) { if (visibility == QWindow::Hidden) { window->destroy(); } }); } KWin::Scripting *KWin::Scripting::s_self = nullptr; KWin::Scripting *KWin::Scripting::create(QObject *parent) { Q_ASSERT(!s_self); s_self = new Scripting(parent); return s_self; } KWin::Scripting::Scripting(QObject *parent) : QObject(parent) , m_scriptsLock(new QMutex(QMutex::Recursive)) , m_qmlEngine(new QQmlEngine(this)) , m_workspaceWrapper(new WorkspaceWrapper(this)) { init(); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting"), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables); connect(Workspace::self(), SIGNAL(configChanged()), SLOT(start())); connect(Workspace::self(), SIGNAL(workspaceInitialized()), SLOT(start())); } void KWin::Scripting::init() { qmlRegisterType("org.kde.kwin", 2, 0, "DesktopThumbnailItem"); qmlRegisterType("org.kde.kwin", 2, 0, "ThumbnailItem"); qmlRegisterType("org.kde.kwin", 2, 0, "DBusCall"); qmlRegisterType("org.kde.kwin", 2, 0, "ScreenEdgeItem"); qmlRegisterType(); qmlRegisterType("org.kde.kwin", 2, 0, "ClientModel"); qmlRegisterType("org.kde.kwin", 2, 0, "ClientModelByScreen"); qmlRegisterType("org.kde.kwin", 2, 0, "ClientModelByScreenAndDesktop"); qmlRegisterType("org.kde.kwin", 2, 0, "ClientFilterModel"); qmlRegisterType(); qmlRegisterType(); qmlRegisterType(); m_qmlEngine->rootContext()->setContextProperty(QStringLiteral("workspace"), m_workspaceWrapper); m_qmlEngine->rootContext()->setContextProperty(QStringLiteral("options"), options); } void KWin::Scripting::start() { #if 0 // TODO make this threaded again once KConfigGroup is sufficiently thread safe, bug #305361 and friends // perform querying for the services in a thread QFutureWatcher *watcher = new QFutureWatcher(this); connect(watcher, SIGNAL(finished()), this, SLOT(slotScriptsQueried())); watcher->setFuture(QtConcurrent::run(this, &KWin::Scripting::queryScriptsToLoad, pluginStates, offers)); #else LoadScriptList scriptsToLoad = queryScriptsToLoad(); for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin(); it != scriptsToLoad.constEnd(); ++it) { if (it->first) { loadScript(it->second.first, it->second.second); } else { loadDeclarativeScript(it->second.first, it->second.second); } } runScripts(); #endif } LoadScriptList KWin::Scripting::queryScriptsToLoad() { KSharedConfig::Ptr _config = kwinApp()->config(); static bool s_started = false; if (s_started) { _config->reparseConfiguration(); } else { s_started = true; } QMap pluginStates = KConfigGroup(_config, "Plugins").entryMap(); const QString scriptFolder = QStringLiteral(KWIN_NAME "/scripts/"); const auto offers = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), scriptFolder); LoadScriptList scriptsToLoad; for (const KPluginMetaData &service: offers) { const QString value = pluginStates.value(service.pluginId() + QLatin1String("Enabled"), QString()); const bool enabled = value.isNull() ? service.isEnabledByDefault() : QVariant(value).toBool(); const bool javaScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("javascript"); const bool declarativeScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("declarativescript"); if (!javaScript && !declarativeScript) { continue; } if (!enabled) { if (isScriptLoaded(service.pluginId())) { // unload the script unloadScript(service.pluginId()); } continue; } const QString pluginName = service.pluginId(); const QString scriptName = service.value(QStringLiteral("X-Plasma-MainScript")); const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, scriptFolder + pluginName + QLatin1String("/contents/") + scriptName); if (file.isNull()) { qCDebug(KWIN_SCRIPTING) << "Could not find script file for " << pluginName; continue; } scriptsToLoad << qMakePair(javaScript, qMakePair(file, pluginName)); } return scriptsToLoad; } void KWin::Scripting::slotScriptsQueried() { QFutureWatcher *watcher = dynamic_cast< QFutureWatcher* >(sender()); if (!watcher) { // slot invoked not from a FutureWatcher return; } LoadScriptList scriptsToLoad = watcher->result(); for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin(); it != scriptsToLoad.constEnd(); ++it) { if (it->first) { loadScript(it->second.first, it->second.second); } else { loadDeclarativeScript(it->second.first, it->second.second); } } runScripts(); watcher->deleteLater(); } bool KWin::Scripting::isScriptLoaded(const QString &pluginName) const { return findScript(pluginName) != nullptr; } KWin::AbstractScript *KWin::Scripting::findScript(const QString &pluginName) const { QMutexLocker locker(m_scriptsLock.data()); foreach (AbstractScript *script, scripts) { if (script->pluginName() == pluginName) { return script; } } return nullptr; } bool KWin::Scripting::unloadScript(const QString &pluginName) { QMutexLocker locker(m_scriptsLock.data()); foreach (AbstractScript *script, scripts) { if (script->pluginName() == pluginName) { script->deleteLater(); return true; } } return false; } void KWin::Scripting::runScripts() { QMutexLocker locker(m_scriptsLock.data()); for (int i = 0; i < scripts.size(); i++) { scripts.at(i)->run(); } } void KWin::Scripting::scriptDestroyed(QObject *object) { QMutexLocker locker(m_scriptsLock.data()); scripts.removeAll(static_cast(object)); } int KWin::Scripting::loadScript(const QString &filePath, const QString& pluginName) { QMutexLocker locker(m_scriptsLock.data()); if (isScriptLoaded(pluginName)) { return -1; } const int id = scripts.size(); KWin::Script *script = new KWin::Script(id, filePath, pluginName, this); connect(script, SIGNAL(destroyed(QObject*)), SLOT(scriptDestroyed(QObject*))); scripts.append(script); return id; } int KWin::Scripting::loadDeclarativeScript(const QString& filePath, const QString& pluginName) { QMutexLocker locker(m_scriptsLock.data()); if (isScriptLoaded(pluginName)) { return -1; } const int id = scripts.size(); KWin::DeclarativeScript *script = new KWin::DeclarativeScript(id, filePath, pluginName, this); connect(script, SIGNAL(destroyed(QObject*)), SLOT(scriptDestroyed(QObject*))); scripts.append(script); return id; } KWin::Scripting::~Scripting() { QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Scripting")); s_self = nullptr; } QList< QAction * > KWin::Scripting::actionsForUserActionMenu(KWin::AbstractClient *c, QMenu *parent) { QList actions; foreach (AbstractScript *script, scripts) { actions << script->actionsForUserActionMenu(c, parent); } return actions; } diff --git a/scripting/scriptingutils.h b/scripting/scriptingutils.h index d1e74cd4a..2dc25d983 100644 --- a/scripting/scriptingutils.h +++ b/scripting/scriptingutils.h @@ -1,283 +1,315 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2012 Martin Gräßlin 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 the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_SCRIPTINGUTILS_H #define KWIN_SCRIPTINGUTILS_H #include "input.h" #include "workspace.h" #include "screenedge.h" #include "scripting_logging.h" #include #include #include #include namespace KWin { /** * Validates that argument at @p index of given @p context is of required type. * Throws a type error in the scripting context if there is a type mismatch. * @param context The scripting context in which the argument type needs to be validated. * @param index The argument index to validate * @returns @c true if the argument is of required type, @c false otherwise **/ template bool validateArgumentType(QScriptContext *context, int index) { const bool result = context->argument(index).toVariant().canConvert(); if (!result) { context->throwError(QScriptContext::TypeError, i18nc("KWin Scripting function received incorrect value for an expected type", "%1 is not of required type", context->argument(index).toString())); } return result; } /** * Validates that the argument of @p context is of specified type. * Throws a type error in the scripting context if there is a type mismatch. * @param context The scripting context in which the argument type needs to be validated. * @returns @c true if the argument is of required type, @c false otherwise **/ template bool validateArgumentType(QScriptContext *context) { return validateArgumentType(context, 0); } template bool validateArgumentType(QScriptContext *context) { if (!validateArgumentType(context)) { return false; } return validateArgumentType(context, 1); } template bool validateArgumentType(QScriptContext *context) { if (!validateArgumentType(context)) { return false; } return validateArgumentType(context, 2); } template bool validateArgumentType(QScriptContext *context) { if (!validateArgumentType(context)) { return false; } return validateArgumentType(context, 3); } /** * Validates that the argument count of @p context is at least @p min and @p max. * Throws a syntax error in the script context if argument count mismatch. * @param context The ScriptContext for which the argument count needs to be validated * @param min The minimum number of arguments. * @param max The maximum number of arguments * @returns @c true if the argument count is correct, otherwise @c false **/ bool validateParameters(QScriptContext *context, int min, int max); template QScriptValue globalShortcut(QScriptContext *context, QScriptEngine *engine) { T script = qobject_cast(context->callee().data().toQObject()); if (!script) { return engine->undefinedValue(); } if (context->argumentCount() != 4) { qCDebug(KWIN_SCRIPTING) << "Incorrect number of arguments! Expected: title, text, keySequence, callback"; return engine->undefinedValue(); } QAction* a = new QAction(script); a->setObjectName(context->argument(0).toString()); a->setText(context->argument(1).toString()); const QKeySequence shortcut = QKeySequence(context->argument(2).toString()); KGlobalAccel::self()->setShortcut(a, QList() << shortcut); script->registerShortcut(a, context->argument(3)); input()->registerShortcut(shortcut, a); return engine->newVariant(true); } template void callGlobalShortcutCallback(T script, QObject *sender) { QAction *a = qobject_cast(sender); if (!a) { return; } QHash::const_iterator it = script->shortcutCallbacks().find(a); if (it == script->shortcutCallbacks().end()) { return; } QScriptValue value(it.value()); QScriptValueList arguments; arguments << value.engine()->newQObject(a); value.call(QScriptValue(), arguments); } template QScriptValue registerScreenEdge(QScriptContext *context, QScriptEngine *engine) { T script = qobject_cast(context->callee().data().toQObject()); if (!script) { return engine->undefinedValue(); } if (!validateParameters(context, 2, 2)) { return engine->undefinedValue(); } if (!validateArgumentType(context)) { return engine->undefinedValue(); } if (!context->argument(1).isFunction()) { context->throwError(QScriptContext::SyntaxError, i18nc("KWin Scripting error thrown due to incorrect argument", "Second argument to registerScreenEdge needs to be a callback")); } const int edge = context->argument(0).toVariant().toInt(); QHash >::iterator it = script->screenEdgeCallbacks().find(edge); if (it == script->screenEdgeCallbacks().end()) { // not yet registered ScreenEdges::self()->reserve(static_cast(edge), script, "borderActivated"); script->screenEdgeCallbacks().insert(edge, QList() << context->argument(1)); } else { it->append(context->argument(1)); } return engine->newVariant(true); } +template +QScriptValue unregisterScreenEdge(QScriptContext *context, QScriptEngine *engine) +{ + T script = qobject_cast(context->callee().data().toQObject()); + if (!script) { + return engine->undefinedValue(); + } + if (!validateParameters(context, 1, 1)) { + return engine->undefinedValue(); + } + if (!validateArgumentType(context)) { + return engine->undefinedValue(); + } + + const int edge = context->argument(0).toVariant().toInt(); + QHash >::iterator it = script->screenEdgeCallbacks().find(edge); + if (it == script->screenEdgeCallbacks().end()) { + //not previously registered + return engine->newVariant(false); + } + ScreenEdges::self()->unreserve(static_cast(edge), script); + script->screenEdgeCallbacks().erase(it); + return engine->newVariant(true); +} + template QScriptValue registerUserActionsMenu(QScriptContext *context, QScriptEngine *engine) { T script = qobject_cast(context->callee().data().toQObject()); if (!script) { return engine->undefinedValue(); } if (!validateParameters(context, 1, 1)) { return engine->undefinedValue(); } if (!context->argument(0).isFunction()) { context->throwError(QScriptContext::SyntaxError, i18nc("KWin Scripting error thrown due to incorrect argument", "Argument for registerUserActionsMenu needs to be a callback")); return engine->undefinedValue(); } script->registerUseractionsMenuCallback(context->argument(0)); return engine->newVariant(true); } template void screenEdgeActivated(T *script, int edge) { QHash >::iterator it = script->screenEdgeCallbacks().find(edge); if (it != script->screenEdgeCallbacks().end()) { foreach (const QScriptValue &value, it.value()) { QScriptValue callback(value); callback.call(); } } } template QScriptValue scriptingAssert(QScriptContext *context, QScriptEngine *engine, int min, int max, T defaultVal = T()) { if (!validateParameters(context, min, max)) { return engine->undefinedValue(); } switch (context->argumentCount()) { case 1: if (!validateArgumentType(context)) { return engine->undefinedValue(); } break; case 2: if (max == 2) { if (!validateArgumentType(context)) { return engine->undefinedValue(); } } else { if (!validateArgumentType(context)) { return engine->undefinedValue(); } } break; case 3: if (!validateArgumentType(context)) { return engine->undefinedValue(); } break; } if (max == 2) { if (context->argument(0).toVariant().value() != defaultVal) { if (context->argumentCount() == max) { context->throwError(QScriptContext::UnknownError, context->argument(max - 1).toString()); } else { context->throwError(QScriptContext::UnknownError, i18nc("Assertion failed in KWin script with given value", "Assertion failed: %1", context->argument(0).toString())); } return engine->undefinedValue(); } } else { if (context->argument(0).toVariant().value() != context->argument(1).toVariant().value()) { if (context->argumentCount() == max) { context->throwError(QScriptContext::UnknownError, context->argument(max - 1).toString()); } else { context->throwError(QScriptContext::UnknownError, i18nc("Assertion failed in KWin script with expected value and actual value", "Assertion failed: Expected %1, got %2", context->argument(0).toString(), context->argument(1).toString())); } return engine->undefinedValue(); } } return engine->newVariant(true); } inline void registerGlobalShortcutFunction(QObject *parent, QScriptEngine *engine, QScriptEngine::FunctionSignature function) { QScriptValue shortcutFunc = engine->newFunction(function); shortcutFunc.setData(engine->newQObject(parent)); engine->globalObject().setProperty(QStringLiteral("registerShortcut"), shortcutFunc); } inline void registerScreenEdgeFunction(QObject *parent, QScriptEngine *engine, QScriptEngine::FunctionSignature function) { QScriptValue shortcutFunc = engine->newFunction(function); shortcutFunc.setData(engine->newQObject(parent)); engine->globalObject().setProperty(QStringLiteral("registerScreenEdge"), shortcutFunc); } +inline void unregisterScreenEdgeFunction(QObject *parent, QScriptEngine *engine, QScriptEngine::FunctionSignature function) +{ + QScriptValue shortcutFunc = engine->newFunction(function); + shortcutFunc.setData(engine->newQObject(parent)); + engine->globalObject().setProperty(QStringLiteral("unregisterScreenEdge"), shortcutFunc); +} + inline void regesterUserActionsMenuFunction(QObject *parent, QScriptEngine *engine, QScriptEngine::FunctionSignature function) { QScriptValue shortcutFunc = engine->newFunction(function); shortcutFunc.setData(engine->newQObject(parent)); engine->globalObject().setProperty(QStringLiteral("registerUserActionsMenu"), shortcutFunc); } } // namespace KWin #endif // KWIN_SCRIPTINGUTILS_H