90a33a4bc2
I did some research with the result that such comparisons are indeed not standardized to be legal before C++14. In C++14 they are. These microoptimizations can thus be re-enabled when C++14 becomes the required version.
878 lines
32 KiB
C++
878 lines
32 KiB
C++
/********************************************************************
|
|
KWin - the KDE window manager
|
|
This file is part of the KDE project.
|
|
|
|
Copyright (C) 2010 Rohan Prabhu <rohan@rohanprabhu.com>
|
|
Copyright (C) 2011 Martin Gräßlin <mgraesslin@kde.org>
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*********************************************************************/
|
|
|
|
#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 <KConfigGroup>
|
|
#include <KPackage/PackageLoader>
|
|
// Qt
|
|
#include <QtDBus/QDBusConnection>
|
|
#include <QtDBus/QDBusMessage>
|
|
#include <QtDBus/QDBusPendingCallWatcher>
|
|
#include <QDebug>
|
|
#include <QFutureWatcher>
|
|
#include <QSettings>
|
|
#include <QtConcurrentRun>
|
|
#include <QMenu>
|
|
#include <QQmlContext>
|
|
#include <QQmlEngine>
|
|
#include <QQmlExpression>
|
|
#include <QtScript/QScriptEngine>
|
|
#include <QtScript/QScriptValue>
|
|
#include <QtCore/QStandardPaths>
|
|
#include <QQuickWindow>
|
|
|
|
QScriptValue kwinScriptPrint(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
KWin::AbstractScript *script = qobject_cast<KWin::Script*>(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<KWin::Client*>(argument)) {
|
|
client->print<QTextStream>(stream);
|
|
} else {
|
|
stream << argument.toString();
|
|
}
|
|
}
|
|
script->printMessage(result);
|
|
|
|
return engine->undefinedValue();
|
|
}
|
|
|
|
QScriptValue kwinScriptReadConfig(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
KWin::AbstractScript *script = qobject_cast<KWin::AbstractScript*>(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<KWin::AbstractScript*>(context, engine);
|
|
}
|
|
|
|
QScriptValue kwinAssertTrue(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::scriptingAssert<bool>(context, engine, 1, 2, true);
|
|
}
|
|
|
|
QScriptValue kwinAssertFalse(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::scriptingAssert<bool>(context, engine, 1, 2, false);
|
|
}
|
|
|
|
QScriptValue kwinAssertEquals(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::scriptingAssert<QVariant>(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<KWin::AbstractScript*>(context, engine);
|
|
}
|
|
|
|
QScriptValue kwinUnregisterScreenEdge(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::unregisterScreenEdge<KWin::AbstractScript*>(context, engine);
|
|
}
|
|
|
|
QScriptValue kwinRegisterTouchScreenEdge(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::registerTouchScreenEdge<KWin::Script*>(context, engine);
|
|
}
|
|
|
|
QScriptValue kwinUnregisterTouchScreenEdge(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::unregisterTouchScreenEdge<KWin::Script*>(context, engine);
|
|
}
|
|
|
|
QScriptValue kwinRegisterUserActionsMenu(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
return KWin::registerUserActionsMenu<KWin::AbstractScript*>(context, engine);
|
|
}
|
|
|
|
QScriptValue kwinCallDBus(QScriptContext *context, QScriptEngine *engine)
|
|
{
|
|
KWin::AbstractScript *script = qobject_cast<KWin::AbstractScript*>(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<QString, QString, QString, QString>(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; i<argumentsCount; ++i) {
|
|
if (context->argument(i).isArray()) {
|
|
QStringList stringArray = engine->fromScriptValue<QStringList>(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<KWin::AbstractScript*>(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<QAction*> returnActions;
|
|
for (QList<QScriptValue>::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<int>(items.property(QStringLiteral("length")).toInteger());
|
|
for (int i=0; i<length; ++i) {
|
|
QScriptValue value = items.property(QString::number(i));
|
|
if (!value.isValid()) {
|
|
continue;
|
|
}
|
|
if (value.isObject()) {
|
|
QAction *a = scriptValueToAction(value, menu);
|
|
if (a) {
|
|
menu->addAction(a);
|
|
}
|
|
}
|
|
}
|
|
return menu->menuAction();
|
|
}
|
|
|
|
void KWin::AbstractScript::actionDestroyed(QObject *object)
|
|
{
|
|
// TODO: Qt 5 - change to lambda function
|
|
m_shortcutCallbacks.remove(static_cast<QAction*>(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<QByteArray> *watcher = new QFutureWatcher<QByteArray>(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<QByteArray> *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();
|
|
}
|
|
|
|
bool KWin::Script::registerTouchScreenCallback(int edge, QScriptValue callback)
|
|
{
|
|
if (m_touchScreenEdgeCallbacks.constFind(edge) != m_touchScreenEdgeCallbacks.constEnd()) {
|
|
return false;
|
|
}
|
|
QAction *action = new QAction(this);
|
|
connect(action, &QAction::triggered, this,
|
|
[callback] {
|
|
QScriptValue invoke(callback);
|
|
invoke.call();
|
|
}
|
|
);
|
|
ScreenEdges::self()->reserveTouch(KWin::ElectricBorder(edge), action);
|
|
m_touchScreenEdgeCallbacks.insert(edge, action);
|
|
return true;
|
|
}
|
|
|
|
bool KWin::Script::unregisterTouchScreenCallback(int edge)
|
|
{
|
|
auto it = m_touchScreenEdgeCallbacks.find(edge);
|
|
if (it == m_touchScreenEdgeCallbacks.end()) {
|
|
return false;
|
|
}
|
|
delete it.value();
|
|
m_touchScreenEdgeCallbacks.erase(it);
|
|
return true;
|
|
}
|
|
|
|
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()->declarativeScriptSharedContext(), 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();
|
|
}
|
|
});
|
|
}
|
|
|
|
bool KWin::JSEngineGlobalMethodsWrapper::registerShortcut(const QString &name, const QString &text, const QKeySequence& keys, QJSValue function)
|
|
{
|
|
if (!function.isCallable()) {
|
|
qCDebug(KWIN_SCRIPTING) << "Fourth and final argument must be a javascript function";
|
|
return false;
|
|
}
|
|
|
|
QAction *a = new QAction(this);
|
|
a->setObjectName(name);
|
|
a->setText(text);
|
|
const QKeySequence shortcut = QKeySequence(keys);
|
|
KGlobalAccel::self()->setShortcut(a, QList<QKeySequence>{shortcut});
|
|
KWin::input()->registerShortcut(shortcut, a);
|
|
|
|
connect(a, &QAction::triggered, this, [=]() mutable {
|
|
QJSValueList arguments;
|
|
arguments << Scripting::self()->qmlEngine()->toScriptValue(a);
|
|
function.call(arguments);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
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_declarativeScriptSharedContext(new QQmlContext(m_qmlEngine, this))
|
|
, m_workspaceWrapper(new QtScriptWorkspaceWrapper(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<DesktopThumbnailItem>("org.kde.kwin", 2, 0, "DesktopThumbnailItem");
|
|
qmlRegisterType<WindowThumbnailItem>("org.kde.kwin", 2, 0, "ThumbnailItem");
|
|
qmlRegisterType<DBusCall>("org.kde.kwin", 2, 0, "DBusCall");
|
|
qmlRegisterType<ScreenEdgeItem>("org.kde.kwin", 2, 0, "ScreenEdgeItem");
|
|
qmlRegisterType<KWin::ScriptingClientModel::ClientModel>();
|
|
qmlRegisterType<KWin::ScriptingClientModel::SimpleClientModel>("org.kde.kwin", 2, 0, "ClientModel");
|
|
qmlRegisterType<KWin::ScriptingClientModel::ClientModelByScreen>("org.kde.kwin", 2, 0, "ClientModelByScreen");
|
|
qmlRegisterType<KWin::ScriptingClientModel::ClientModelByScreenAndDesktop>("org.kde.kwin", 2, 0, "ClientModelByScreenAndDesktop");
|
|
qmlRegisterType<KWin::ScriptingClientModel::ClientFilterModel>("org.kde.kwin", 2, 0, "ClientFilterModel");
|
|
qmlRegisterType<KWin::AbstractClient>();
|
|
qmlRegisterType<KWin::Client>();
|
|
qmlRegisterType<QAbstractItemModel>();
|
|
|
|
m_qmlEngine->rootContext()->setContextProperty(QStringLiteral("workspace"), m_workspaceWrapper);
|
|
m_qmlEngine->rootContext()->setContextProperty(QStringLiteral("options"), options);
|
|
|
|
m_declarativeScriptSharedContext->setContextProperty(QStringLiteral("workspace"), new DeclarativeScriptWorkspaceWrapper(this));
|
|
// QQmlListProperty interfaces only work via properties, rebind them as functions here
|
|
QQmlExpression expr(m_declarativeScriptSharedContext, nullptr, "workspace.clientList = function() { return workspace.clients }");
|
|
expr.evaluate();
|
|
}
|
|
|
|
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<LoadScriptList> *watcher = new QFutureWatcher<LoadScriptList>(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<QString,QString> 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<LoadScriptList> *watcher = dynamic_cast< QFutureWatcher<LoadScriptList>* >(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<KWin::Script*>(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<QAction*> actions;
|
|
foreach (AbstractScript *script, scripts) {
|
|
actions << script->actionsForUserActionMenu(c, parent);
|
|
}
|
|
return actions;
|
|
}
|
|
|