diff --git a/scripting/scripting.cpp b/scripting/scripting.cpp index 3069bd04b..14434d3fa 100644 --- a/scripting/scripting.cpp +++ b/scripting/scripting.cpp @@ -1,785 +1,796 @@ /******************************************************************** 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 "../client.h" #include "../thumbnailitem.h" #include "../options.h" #include "../workspace.h" // KDE #include #include #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) { qDebug() << "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 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_workspace(new WorkspaceWrapper(this)) { m_scriptFile.setFileName(scriptName); if (m_pluginName.isNull()) { m_pluginName = scriptName; } } KWin::AbstractScript::~AbstractScript() { } KConfigGroup KWin::AbstractScript::config() const { return KSharedConfig::openConfig()->group(QStringLiteral("Script-") + m_pluginName); } void KWin::AbstractScript::stop() { deleteLater(); } void KWin::AbstractScript::printMessage(const QString &message) { qDebug() << 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); // 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(AbstractScript::workspace(), 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()) { qDebug() << "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::Client *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 NULL; } 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 NULL; } QScriptValue lengthValue = itemsValue.property(QStringLiteral("length")); if (!lengthValue.isValid() || !lengthValue.isNumber() || lengthValue.toInteger() == 0) { // length property missing return NULL; } return createMenu(title, itemsValue, parent); } else if (triggeredValue.isValid()) { // normal item return createAction(title, checkable, checked, triggeredValue, parent); } return NULL; } 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(QStringLiteral("/") + QString::number(scriptId()), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables); } KWin::Script::~Script() { QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/") + 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()) { qDebug() << "defaultscript encountered an error at [Line " << m_engine->uncaughtExceptionLineNumber() << "]"; qDebug() << "Message: " << ret.toString(); qDebug() << "-----------------"; QScriptValueIterator iter(ret); while (iter.hasNext()) { iter.next(); qDebug() << " " << 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_engine(new QQmlEngine(this)) , m_component(new QQmlComponent(m_engine, this)) { } KWin::DeclarativeScript::~DeclarativeScript() { } void KWin::DeclarativeScript::run() { if (running()) { return; } KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(m_engine); kdeclarative.initialize(); kdeclarative.setupBindings(); 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(); m_engine->rootContext()->setContextProperty(QStringLiteral("workspace"), AbstractScript::workspace()); m_engine->rootContext()->setContextProperty(QStringLiteral("options"), options); m_engine->rootContext()->setContextProperty(QStringLiteral("KWin"), new JSEngineGlobalMethodsWrapper(this)); 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()) { qDebug() << "Component failed to load: " << m_component->errors(); } else { m_component->create(); } 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 = NULL; 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)) { QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting"), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables); QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwin.Scripting")); connect(Workspace::self(), SIGNAL(configChanged()), SLOT(start())); connect(Workspace::self(), SIGNAL(workspaceInitialized()), SLOT(start())); } 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 = KSharedConfig::openConfig(); static bool s_started = false; if (s_started) { _config->reparseConfiguration(); } else { s_started = true; } QMap pluginStates = KConfigGroup(_config, "Plugins").entryMap(); KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("KWin/Script")); LoadScriptList scriptsToLoad; foreach (const KService::Ptr & service, offers) { KPluginInfo plugininfo(service); const QString value = pluginStates.value(plugininfo.pluginName() + QString::fromLatin1("Enabled"), QString()); plugininfo.setPluginEnabled(value.isNull() ? plugininfo.isPluginEnabledByDefault() : QVariant(value).toBool()); const bool javaScript = service->property(QStringLiteral("X-Plasma-API")).toString() == QStringLiteral("javascript"); const bool declarativeScript = service->property(QStringLiteral("X-Plasma-API")).toString() == QStringLiteral("declarativescript"); if (!javaScript && !declarativeScript) { continue; } if (!plugininfo.isPluginEnabled()) { if (isScriptLoaded(plugininfo.pluginName())) { // unload the script unloadScript(plugininfo.pluginName()); } continue; } const QString pluginName = service->property(QStringLiteral("X-KDE-PluginInfo-Name")).toString(); const QString scriptName = service->property(QStringLiteral("X-Plasma-MainScript")).toString(); const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral(KWIN_NAME) + QStringLiteral("/scripts/") + pluginName + QStringLiteral("/contents/") + scriptName); if (file.isNull()) { qDebug() << "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 { QMutexLocker locker(m_scriptsLock.data()); foreach (AbstractScript *script, scripts) { if (script->pluginName() == pluginName) { return true; } } return false; } 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")); QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.kwin.Scripting")); s_self = NULL; } QList< QAction * > KWin::Scripting::actionsForUserActionMenu(KWin::Client *c, QMenu *parent) { QList actions; foreach (AbstractScript *script, scripts) { actions << script->actionsForUserActionMenu(c, parent); } return actions; } #include "scripting.moc" diff --git a/scripting/scripting.h b/scripting/scripting.h index 3a0f89a6e..a7f991d88 100644 --- a/scripting/scripting.h +++ b/scripting/scripting.h @@ -1,380 +1,382 @@ /******************************************************************** 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 . *********************************************************************/ #ifndef KWIN_SCRIPTING_H #define KWIN_SCRIPTING_H #include #include #include #include #include #include class QQmlComponent; class QQmlEngine; class QAction; class QDBusPendingCallWatcher; class QGraphicsScene; class QMenu; class QMutex; class QScriptEngine; class QScriptValue; +class QQuickWindow; class KConfigGroup; /// @c true == javascript, @c false == qml typedef QList< QPair > > LoadScriptList; namespace KWin { class Client; class ScriptUnloaderAgent; class WorkspaceWrapper; class AbstractScript : public QObject { Q_OBJECT public: AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent = NULL); ~AbstractScript(); QString fileName() const { return m_scriptFile.fileName(); } const QString &pluginName() { return m_pluginName; } void printMessage(const QString &message); void registerShortcut(QAction *a, QScriptValue callback); /** * @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 * added to the UserActionsMenu. * * @param callback Script method to execute when the UserActionsMenu is about to be shown. * @return void * @see actionsForUserActionMenu **/ void registerUseractionsMenuCallback(QScriptValue callback); /** * @brief Creates actions for the UserActionsMenu by invoking the registered callbacks. * * This method invokes all the callbacks previously registered with registerUseractionsMenuCallback. * The Client @p c is passed in as an argument to the invoked method. * * The invoked method is supposed to return a JavaScript object containing either the menu or * menu entry to be added. In case the callback returns a null or undefined or any other invalid * value, it is not considered for adding to the menu. * * The JavaScript object structure for a menu entry looks like the following: * @code * { * title: "My Menu Entry", * checkable: true, * checked: false, * triggered: function (action) { * // callback when the menu entry is triggered with the QAction as argument * } * } * @endcode * * To construct a complete Menu the JavaScript object looks like the following: * @code * { * title: "My Menu Title", * items: [{...}, {...}, ...] // list of menu entries as described above * } * @endcode * * The returned JavaScript object is introspected and for a menu entry a QAction is created, * while for a menu a QMenu is created and QActions for the individual entries. Of course it * is allowed to have nested structures. * * All created objects are (grand) children to the passed in @p parent menu, so that they get * deleted whenever the menu is destroyed. * * @param c The Client for which the menu is invoked, passed to the callback * @param parent The Parent for the created Menus or Actions * @return QList< QAction* > List of QActions obtained from asking the registered callbacks * @see registerUseractionsMenuCallback **/ QList actionsForUserActionMenu(Client *c, QMenu *parent); KConfigGroup config() const; const QHash &shortcutCallbacks() const { return m_shortcutCallbacks; } QHash > &screenEdgeCallbacks() { return m_screenEdgeCallbacks; } int registerCallback(QScriptValue value); public Q_SLOTS: Q_SCRIPTABLE void stop(); Q_SCRIPTABLE virtual void run() = 0; void slotPendingDBusCall(QDBusPendingCallWatcher *watcher); 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 **/ void actionDestroyed(QObject *object); Q_SIGNALS: Q_SCRIPTABLE void print(const QString &text); protected: QFile &scriptFile() { return m_scriptFile; } bool running() const { return m_running; } void setRunning(bool running) { m_running = running; } int scriptId() const { return m_scriptId; } WorkspaceWrapper *workspace() { return m_workspace; } private: /** * @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); /** * @brief Creates a new QAction from the provided data and registers it for invoking the * @p callback when the action is triggered. * * The created action is added to the map of actions and callbacks shared with the global * shortcuts. * * @param title The title of the action * @param checkable Whether the action is checkable * @param checked Whether the checkable action is checked * @param callback The callback to invoke when the action is triggered * @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); /** * @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; QFile m_scriptFile; QString m_pluginName; bool m_running; WorkspaceWrapper *m_workspace; 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 { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") public: Script(int id, QString scriptName, QString pluginName, QObject *parent = NULL); virtual ~Script(); QScriptEngine *engine() { return m_engine; } 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(); QScriptEngine *m_engine; bool m_starting; QScopedPointer m_agent; }; class ScriptUnloaderAgent : public QScriptEngineAgent { public: explicit ScriptUnloaderAgent(Script *script); virtual void scriptUnload(qint64 id); private: Script *m_script; }; class DeclarativeScript : public AbstractScript { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") public: explicit DeclarativeScript(int id, QString scriptName, QString pluginName, QObject *parent = 0); virtual ~DeclarativeScript(); public Q_SLOTS: Q_SCRIPTABLE void run(); private Q_SLOTS: void createComponent(); private: QQmlEngine *m_engine; QQmlComponent *m_component; }; class JSEngineGlobalMethodsWrapper : public QObject { Q_OBJECT Q_ENUMS(ClientAreaOption) public: //------------------------------------------------------------------ //enums copy&pasted from kwinglobals.h for exporting enum ClientAreaOption { ///< geometry where a window will be initially placed after being mapped PlacementArea, ///< window movement snapping area? ignore struts MovementArea, ///< geometry to which a window will be maximized MaximizeArea, ///< like MaximizeArea, but ignore struts - used e.g. for topmenu MaximizeFullArea, ///< area for fullscreen windows FullScreenArea, ///< whole workarea (all screens together) WorkArea, ///< whole area (all screens together), ignore struts FullArea, ///< one whole screen, ignore struts ScreenArea }; explicit JSEngineGlobalMethodsWrapper(DeclarativeScript *parent); virtual ~JSEngineGlobalMethodsWrapper(); public Q_SLOTS: QVariant readConfig(const QString &key, QVariant defaultValue = QVariant()); + void registerWindow(QQuickWindow *window); private: DeclarativeScript *m_script; }; /** * The heart of KWin::Scripting. Infinite power lies beyond */ class Scripting : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Scripting") private: explicit Scripting(QObject *parent); QStringList scriptList; QList scripts; /** * Lock to protect the scripts member variable. **/ QScopedPointer m_scriptsLock; // Preferably call ONLY at load time void runScripts(); public: ~Scripting(); Q_SCRIPTABLE Q_INVOKABLE int loadScript(const QString &filePath, const QString &pluginName = QString()); Q_SCRIPTABLE Q_INVOKABLE int loadDeclarativeScript(const QString &filePath, const QString &pluginName = QString()); Q_SCRIPTABLE Q_INVOKABLE bool isScriptLoaded(const QString &pluginName) const; Q_SCRIPTABLE Q_INVOKABLE bool unloadScript(const QString &pluginName); /** * @brief Invokes all registered callbacks to add actions to the UserActionsMenu. * * @param c The Client for which the UserActionsMenu is about to be shown * @param parent The parent menu to which to add created child menus and items * @return QList< QAction* > List of all actions aggregated from all scripts. **/ QList actionsForUserActionMenu(Client *c, QMenu *parent); static Scripting *self(); static Scripting *create(QObject *parent); public Q_SLOTS: void scriptDestroyed(QObject *object); Q_SCRIPTABLE void start(); private Q_SLOTS: void slotScriptsQueried(); private: LoadScriptList queryScriptsToLoad(); static Scripting *s_self; }; inline Scripting *Scripting::self() { return s_self; } } #endif diff --git a/scripts/desktopchangeosd/contents/ui/osd.qml b/scripts/desktopchangeosd/contents/ui/osd.qml index 44032f3fb..dc038d39e 100644 --- a/scripts/desktopchangeosd/contents/ui/osd.qml +++ b/scripts/desktopchangeosd/contents/ui/osd.qml @@ -1,297 +1,301 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2012, 2013 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 . *********************************************************************/ import QtQuick 2.0; import org.kde.plasma.core 2.0 as PlasmaCore; import org.kde.plasma.components 2.0 as Plasma; import org.kde.qtextracomponents 2.0 as QtExtra; import org.kde.kwin 2.0; PlasmaCore.Dialog { id: dialog visible: false windowFlags: Qt.X11BypassWindowManagerHint mainItem: Item { function loadConfig() { dialogItem.animationDuration = KWin.readConfig("PopupHideDelay", 1000); if (KWin.readConfig("TextOnly", "false") == "true") { dialogItem.showGrid = false; } else { dialogItem.showGrid = true; } } function show() { if (dialogItem.currentDesktop == workspace.currentDesktop - 1) { return; } dialogItem.previousDesktop = dialogItem.currentDesktop; timer.stop(); dialogItem.currentDesktop = workspace.currentDesktop - 1; textElement.text = workspace.desktopName(workspace.currentDesktop); // screen geometry might have changed var screen = workspace.clientArea(KWin.FullScreenArea, workspace.activeScreen, workspace.currentDesktop); dialogItem.screenWidth = screen.width; dialogItem.screenHeight = screen.height; if (dialogItem.showGrid) { // non dependable properties might have changed view.columns = workspace.desktopGridWidth; view.rows = workspace.desktopGridHeight; } dialog.visible = true; // position might have changed dialog.x = screen.x + screen.width/2 - dialogItem.width/2; dialog.y = screen.y + screen.height/2 - dialogItem.height/2; // start the hide timer timer.start(); } id: dialogItem property int screenWidth: 0 property int screenHeight: 0 // we count desktops starting from 0 to have it better match the layout in the Grid property int currentDesktop: 0 property int previousDesktop: 0 property int animationDuration: 1000 property bool showGrid: true width: dialogItem.showGrid ? view.itemWidth * view.columns : textElement.width height: dialogItem.showGrid ? view.itemHeight * view.rows + textElement.height : textElement.height Plasma.Label { id: textElement anchors.top: dialogItem.showGrid ? parent.top : undefined anchors.horizontalCenter: parent.horizontalCenter text: workspace.desktopName(workspace.currentDesktop) } Grid { id: view columns: 1 rows: 1 property int itemWidth: dialogItem.screenWidth * Math.min(0.8/columns, 0.1) property int itemHeight: Math.min(itemWidth * (dialogItem.screenHeight / dialogItem.screenWidth), dialogItem.screenHeight * Math.min(0.8/rows, 0.1)) anchors { top: textElement.bottom left: parent.left right: parent.right bottom: parent.bottom } visible: dialogItem.showGrid Repeater { id: repeater model: workspace.desktops Item { width: view.itemWidth height: view.itemHeight PlasmaCore.FrameSvgItem { anchors.fill: parent imagePath: "widgets/pager" prefix: "normal" } PlasmaCore.FrameSvgItem { id: activeElement anchors.fill: parent imagePath: "widgets/pager" prefix: "active" opacity: 0.0 Behavior on opacity { NumberAnimation { duration: dialogItem.animationDuration/2 } } } Item { id: arrowsContainer anchors.fill: parent QtExtra.QIconItem { anchors.fill: parent icon: "go-up" visible: false } QtExtra.QIconItem { anchors.fill: parent icon: "go-down" visible: { if (dialogItem.currentDesktop <= index) { // don't show for target desktop return false; } if (index < dialogItem.previousDesktop) { return false; } if (dialogItem.currentDesktop < dialogItem.previousDesktop) { // we only go down if the new desktop is higher return false; } if (Math.floor(dialogItem.currentDesktop/view.columns) == Math.floor(index/view.columns)) { // don't show icons in same row as target desktop return false; } if (dialogItem.previousDesktop % view.columns == index % view.columns) { // show arrows for icons in same column as the previous desktop return true; } return false; } } QtExtra.QIconItem { anchors.fill: parent icon: "go-up" visible: { if (dialogItem.currentDesktop >= index) { // don't show for target desktop return false; } if (index > dialogItem.previousDesktop) { return false; } if (dialogItem.currentDesktop > dialogItem.previousDesktop) { // we only go down if the new desktop is higher return false; } if (Math.floor(dialogItem.currentDesktop/view.columns) == Math.floor(index/view.columns)) { // don't show icons in same row as target desktop return false; } if (dialogItem.previousDesktop % view.columns == index % view.columns) { // show arrows for icons in same column as the previous desktop return true; } return false; } } QtExtra.QIconItem { anchors.fill: parent icon: "go-next" visible: { if (dialogItem.currentDesktop <= index) { // we don't show for desktops not on the path return false; } if (index < dialogItem.previousDesktop) { // we might have to show this icon in case we go up and to the right if (Math.floor(dialogItem.currentDesktop/view.columns) == Math.floor(index/view.columns)) { // can only happen in same row if (index % view.columns >= dialogItem.previousDesktop % view.columns) { // but only for items in the same column or after of the previous desktop return true; } } return false; } if (dialogItem.currentDesktop < dialogItem.previousDesktop) { // we only go right if the new desktop is higher return false; } if (Math.floor(dialogItem.currentDesktop/view.columns) == Math.floor(index/view.columns)) { // show icons in same row as target desktop if (index % view.columns < dialogItem.previousDesktop % view.columns) { // but only for items in the same column or after of the previous desktop return false; } return true; } return false; } } QtExtra.QIconItem { anchors.fill: parent icon: "go-previous" visible: { if (dialogItem.currentDesktop >= index) { // we don't show for desktops not on the path return false; } if (index > dialogItem.previousDesktop) { // we might have to show this icon in case we go down and to the left if (Math.floor(dialogItem.currentDesktop/view.columns) == Math.floor(index/view.columns)) { // can only happen in same row if (index % view.columns <= dialogItem.previousDesktop % view.columns) { // but only for items in the same column or before the previous desktop return true; } } return false; } if (dialogItem.currentDesktop > dialogItem.previousDesktop) { // we only go left if the new desktop is lower return false; } if (Math.floor(dialogItem.currentDesktop/view.columns) == Math.floor(index/view.columns)) { // show icons in same row as target desktop if (index % view.columns > dialogItem.previousDesktop % view.columns) { // but only for items in the same column or before of the previous desktop return false; } return true; } return false; } } } states: [ State { name: "NORMAL" when: index != dialogItem.currentDesktop PropertyChanges { target: activeElement opacity: 0.0 } }, State { name: "SELECTED" when: index == dialogItem.currentDesktop PropertyChanges { target: activeElement opacity: 1.0 } } ] Component.onCompleted: { view.state = (index == dialogItem.currentDesktop) ? "SELECTED" : "NORMAL" } } } } Timer { id: timer repeat: false interval: dialogItem.animationDuration onTriggered: dialog.visible = false } Connections { target: workspace onCurrentDesktopChanged: dialogItem.show() onNumberDesktopsChanged: { repeater.model = workspace.desktops; } } Connections { target: options onConfigChanged: dialogItem.loadConfig() } Component.onCompleted: { view.columns = workspace.desktopGridWidth; view.rows = workspace.desktopGridHeight; dialogItem.loadConfig(); dialogItem.show(); } } + + Component.onCompleted: { + KWin.registerWindow(dialog); + } }