diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -6,3 +6,16 @@ runnercontexttest.cpp LINK_LIBRARIES Qt5::Test KF5::KIOCore KF5Runner ) + +ecm_add_tests( + dbusrunnertest.cpp + LINK_LIBRARIES Qt5::Test KF5::KIOCore KF5Runner Qt5::Widgets +) + +set(demoapp_SRCS testremoterunner.cpp) +qt5_add_dbus_adaptor(demoapp_SRCS "../src/data/org.kde.krunner1.xml" testremoterunner.h TestRemoteRunner) +add_executable(testremoterunner ${demoapp_SRCS}) +target_link_libraries(testremoterunner + Qt5::DBus + KF5::Runner +) diff --git a/autotests/dbusrunnertest.cpp b/autotests/dbusrunnertest.cpp new file mode 100644 --- /dev/null +++ b/autotests/dbusrunnertest.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2017 David Edmundson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License version 2 as + * published by the Free Software Foundation + * + * 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +#include "runnermanager.h" +#include +#include +#include + +#include + +using namespace Plasma; + +Q_DECLARE_METATYPE(Plasma::QueryMatch); +Q_DECLARE_METATYPE(QList); + +class DBusRunnerTest : public QObject +{ + Q_OBJECT +public: + DBusRunnerTest(); + ~DBusRunnerTest(); + +private Q_SLOTS: + void initTestCase(); + void testMatch(); +private: + QProcess *m_process; +}; + +DBusRunnerTest::DBusRunnerTest() + : QObject(), + m_process(new QProcess(this)) +{ + m_process->start(QFINDTESTDATA("testremoterunner")); + QVERIFY(m_process->waitForStarted()); + qRegisterMetaType >(); +} + +DBusRunnerTest::~DBusRunnerTest() +{ + m_process->kill(); + m_process->waitForFinished(); +} + +void DBusRunnerTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)).mkpath("kservices5"); + const QString fakeServicePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/dbusrunnertest.desktop"); + QFile::copy(QFINDTESTDATA("dbusrunnertest.desktop"), fakeServicePath); + KSycoca::self()->ensureCacheValid(); +} + +void DBusRunnerTest::testMatch() +{ + RunnerManager m; + auto s = KService::serviceByDesktopPath("dbusrunnertest.desktop"); + QVERIFY(s); + m.loadRunner(s); + m.launchQuery("foo"); + + QSignalSpy spy(&m, &RunnerManager::matchesChanged); + QVERIFY(spy.wait()); + + //verify matches + QCOMPARE(m.matches().count(), 1); + auto result = m.matches().first(); + + //see testremoterunner.cpp + QCOMPARE(result.id(), QStringLiteral("dbusrunnertest_id1")); //note the runner name is prepended + QCOMPARE(result.text(), QStringLiteral("Match 1")); + QCOMPARE(result.iconName(), QStringLiteral("icon1")); + QCOMPARE(result.type(), Plasma::QueryMatch::ExactMatch); + //relevance can't be compared easily becuase RunnerContext meddles with it + + //verify actions + auto actions = m.actionsForMatch(result); + QCOMPARE(actions.count(), 1); + auto action = actions.first(); + + QCOMPARE(action->text(), QStringLiteral("Action 1")); + + QSignalSpy processSpy(m_process, &QProcess::readyRead); + m.run(result); + processSpy.wait(); + QCOMPARE(m_process->readAllStandardOutput().trimmed(), QByteArray("Running:id1:")); + + result.setSelectedAction(action); + m.run(result); + processSpy.wait(); + QCOMPARE(m_process->readAllStandardOutput().trimmed(), QByteArray("Running:id1:action1")); +} + + + +QTEST_MAIN(DBusRunnerTest) + + +#include "dbusrunnertest.moc" diff --git a/autotests/dbusrunnertest.desktop b/autotests/dbusrunnertest.desktop new file mode 100644 --- /dev/null +++ b/autotests/dbusrunnertest.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Name=DBus runner test +Comment=DBus runner test +X-KDE-ServiceTypes=Plasma/Runner +Type=Service +Icon=internet-web-browser +X-KDE-PluginInfo-Author=Some Developer +X-KDE-PluginInfo-Email=kde@example.com +X-KDE-PluginInfo-Name=dbusrunnertest +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-License=LGPL +X-KDE-PluginInfo-EnabledByDefault=true +X-Plasma-API=DBus +X-Plasma-DBusRunner-Service=net.dave +X-Plasma-DBusRunner-Path=/dave diff --git a/autotests/testremoterunner.h b/autotests/testremoterunner.h new file mode 100644 --- /dev/null +++ b/autotests/testremoterunner.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "../src/dbusutils_p.h" + +class TestRemoteRunner : public QObject +{ + Q_OBJECT +public: + TestRemoteRunner(); + +public Q_SLOTS: + RemoteActions Actions(); + RemoteMatches Match(const QString &searchTerm); + void Run(const QString &id, const QString &actionId); +}; diff --git a/autotests/testremoterunner.cpp b/autotests/testremoterunner.cpp new file mode 100644 --- /dev/null +++ b/autotests/testremoterunner.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 David Edmundson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License version 2 as + * published by the Free Software Foundation + * + * 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include + +#include "testremoterunner.h" +#include "krunner1adaptor.h" + +//Test DBus runner, if the search term contains "foo" it returns a match, otherwise nothing +//Run prints a line to stdout + +TestRemoteRunner::TestRemoteRunner() +{ + new Krunner1Adaptor(this); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + QDBusConnection::sessionBus().registerService("net.dave"); + QDBusConnection::sessionBus().registerObject("/dave", this); +} + +RemoteMatches TestRemoteRunner::Match(const QString& searchTerm) +{ + RemoteMatches ms; + if (searchTerm.contains("foo")) { + RemoteMatch m; + m.id = "id1"; + m.text = "Match 1"; + m.iconName = "icon1"; + m.type = Plasma::QueryMatch::ExactMatch; + m.relevance = 0.8; + ms << m; + } + return ms; + +} + +RemoteActions TestRemoteRunner::Actions() +{ + RemoteAction action; + action.id = "action1"; + action.text = "Action 1"; + action.iconName = "document-browser"; + + return RemoteActions({action}); +} + +void TestRemoteRunner::Run(const QString &id, const QString &actionId) +{ + std::cout << "Running:" << qPrintable(id) << ":" << qPrintable(actionId) << std::endl; + std::cout.flush(); +} + +int main(int argc, char ** argv) +{ + QCoreApplication app(argc, argv); + TestRemoteRunner r; + app.exec(); +} + +#include "testremoterunner.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,19 @@ add_subdirectory(declarative) -add_library(KF5Runner +set (KF5Runner_SRCS abstractrunner.cpp + dbusrunner.cpp runnerjobs.cpp querymatch.cpp runnercontext.cpp runnermanager.cpp runnersyntax.cpp krunner_debug.cpp) +set_property(SOURCE "data/org.kde.krunner1.xml" PROPERTY INCLUDE dbusutils_p.h) +qt5_add_dbus_interface(KF5Runner_SRCS "data/org.kde.krunner1.xml" krunner_iface) + +add_library(KF5Runner + ${KF5Runner_SRCS}) generate_export_header(KF5Runner BASE_NAME KRunner) add_library(KF5::Runner ALIAS KF5Runner) @@ -21,6 +27,7 @@ Qt5::Core KF5::Plasma # Must be public because abstractrunner.h needs plasma/version.h PRIVATE + Qt5::DBus Qt5::Gui Qt5::Widgets KF5::ConfigCore @@ -96,3 +103,8 @@ ecm_generate_pri_file(BASE_NAME KRunner LIB_NAME KF5Runner DEPS "core" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KF5_INCLUDE_INSTALL_DIR}/KRunner) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) + +install(FILES + "data/org.kde.krunner1.xml" + DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} + RENAME kf5_org.kde.krunner1.xml) diff --git a/src/abstractrunner.cpp b/src/abstractrunner.cpp --- a/src/abstractrunner.cpp +++ b/src/abstractrunner.cpp @@ -19,7 +19,7 @@ #include "abstractrunner.h" -#include +#include #include #include #include diff --git a/src/data/org.kde.krunner1.xml b/src/data/org.kde.krunner1.xml new file mode 100644 --- /dev/null +++ b/src/data/org.kde.krunner1.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/data/servicetypes/plasma-runner.desktop b/src/data/servicetypes/plasma-runner.desktop --- a/src/data/servicetypes/plasma-runner.desktop +++ b/src/data/servicetypes/plasma-runner.desktop @@ -58,3 +58,11 @@ [PropertyDef::X-Plasma-Args] Type=QStringList +[PropertyDef::X-Plasma-Api] +Type=QString + +[PropertyDef::X-Plasma-DBusRunner-Service] +Type=QString + +[PropertyDef::X-Plasma-DBusRunner-Path] +Type=QString diff --git a/src/dbusrunner.cpp b/src/dbusrunner.cpp new file mode 100644 --- /dev/null +++ b/src/dbusrunner.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2017 David Edmundson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License version 2 as + * published by the Free Software Foundation + * + * 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "dbusrunner_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "krunner_debug.h" +#include "krunner_iface.h" + +DBusRunner::DBusRunner(const KService::Ptr service, QObject *parent) + : Plasma::AbstractRunner(service, parent) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + QString serviceName = service->property("X-Plasma-DBusRunner-Service").toString(); + QString path = service->property("X-Plasma-DBusRunner-Path").toString(); + + if (serviceName.isEmpty() || path.isEmpty()) { + qCWarning(KRUNNER) << "Invalid entry:" << service->name(); + return; + } + + m_interface = new OrgKdeKrunner1Interface(serviceName, path, QDBusConnection::sessionBus(), this); + + connect(this, &AbstractRunner::prepare, this, &DBusRunner::requestActions); +} + +DBusRunner::~DBusRunner() = default; + +void DBusRunner::requestActions() +{ + auto reply = m_interface->Actions(); + auto watcher = new QDBusPendingCallWatcher(reply); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() { + watcher->deleteLater(); + QDBusReply reply = *watcher; + if (!reply.isValid()) { + return; + } + for(const RemoteAction &action: reply.value()) { + auto a = addAction(action.id, QIcon::fromTheme(action.iconName), action.text); + a->setData(action.id); + } + }); +} + +void DBusRunner::match(Plasma::RunnerContext &context) +{ + if (!m_interface) { + return; + } + + auto reply = m_interface->Match(context.query()); + reply.waitForFinished(); //AbstractRunner::match is called in a new thread, may as well block + if (reply.isError()) { + qCDebug(KRUNNER) << "Error calling" << m_interface->service() << " :" << reply.error().name() << reply.error().message(); + return; + } + for(const RemoteMatch &match: reply.value()) { + Plasma::QueryMatch m(this); + + m.setText(match.text); + m.setData(match.id); + m.setIconName(match.iconName); + m.setType(match.type); + m.setRelevance(match.relevance); + + //split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse) + m.setUrls(QUrl::fromStringList(match.properties.value("urls").toStringList())); + m.setMatchCategory(match.properties.value("category").toString()); + m.setSubtext(match.properties.value("subtext").toString()); + + context.addMatch(m); + } +} + +QList DBusRunner::actionsForMatch(const Plasma::QueryMatch &match) +{ + Q_UNUSED(match) + return actions().values(); +} + +void DBusRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) +{ + Q_UNUSED(context); + if (!m_interface) { + return; + } + + QString actionId; + QString matchId = match.data().toString(); + + if (match.selectedAction()) { + actionId = match.selectedAction()->data().toString(); + } + + m_interface->Run(matchId, actionId); //don't wait for reply before returning to process +} diff --git a/src/dbusrunner_p.h b/src/dbusrunner_p.h new file mode 100644 --- /dev/null +++ b/src/dbusrunner_p.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 David Edmundson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License version 2 as + * published by the Free Software Foundation + * + * 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 Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "dbusutils_p.h" +class OrgKdeKrunner1Interface; + +class DBusRunner : public Plasma::AbstractRunner +{ + Q_OBJECT + +public: + explicit DBusRunner(const KService::Ptr service, QObject *parent = nullptr); + ~DBusRunner() override; + + void match(Plasma::RunnerContext &context) override; + void run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &action) override; + QList actionsForMatch(const Plasma::QueryMatch &match) override; + +private: + void requestActions(); + void setActions(const RemoteActions &remoteActions); + OrgKdeKrunner1Interface *m_interface = nullptr; +}; diff --git a/src/dbusutils_p.h b/src/dbusutils_p.h new file mode 100644 --- /dev/null +++ b/src/dbusutils_p.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct RemoteMatch +{ + //sssuda{sv} + QString id; + QString text; + QString iconName; + Plasma::QueryMatch::Type type = Plasma::QueryMatch::NoMatch; + qreal relevance = 0; + QVariantMap properties; +}; + +typedef QList RemoteMatches; + +struct RemoteAction +{ + QString id; + QString text; + QString iconName; +}; + +typedef QList RemoteActions; + +inline QDBusArgument &operator<< (QDBusArgument &argument, const RemoteMatch &match) { + argument.beginStructure(); + argument << match.id; + argument << match.text; + argument << match.iconName; + argument << match.type; + argument << match.relevance; + argument << match.properties; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteMatch &match) { + argument.beginStructure(); + argument >> match.id; + argument >> match.text; + argument >> match.iconName; + uint type; + argument >> type; + match.type = (Plasma::QueryMatch::Type)type; + argument >> match.relevance; + argument >> match.properties; + argument.endStructure(); + + return argument; +} + +inline QDBusArgument &operator<< (QDBusArgument &argument, const RemoteAction &action) +{ + argument.beginStructure(); + argument << action.id; + argument << action.text; + argument << action.iconName; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteAction &action) { + argument.beginStructure(); + argument >> action.id; + argument >> action.text; + argument >> action.iconName; + argument.endStructure(); + return argument; +} + +Q_DECLARE_METATYPE(RemoteMatch); +Q_DECLARE_METATYPE(RemoteMatches); +Q_DECLARE_METATYPE(RemoteAction); +Q_DECLARE_METATYPE(RemoteActions); + diff --git a/src/querymatch.h b/src/querymatch.h --- a/src/querymatch.h +++ b/src/querymatch.h @@ -74,7 +74,7 @@ * * @param runner the runner this match belongs to */ - explicit QueryMatch(AbstractRunner *runner); + explicit QueryMatch(AbstractRunner *runner = nullptr); /** * Copy constructor @@ -180,7 +180,7 @@ void setId(const QString &id); /** - * @ruetnr a string that can be used as an ID for this match, + * @return a string that can be used as an ID for this match, * even between different queries. It is based in part * on the source of the match (the AbstractRunner) and * distinguishing information provided by the runner, diff --git a/src/querymatch.cpp b/src/querymatch.cpp --- a/src/querymatch.cpp +++ b/src/querymatch.cpp @@ -19,7 +19,7 @@ #include "querymatch.h" -#include +#include #include #include #include diff --git a/src/runnermanager.cpp b/src/runnermanager.cpp --- a/src/runnermanager.cpp +++ b/src/runnermanager.cpp @@ -39,6 +39,7 @@ #include #include +#include "dbusrunner_p.h" #include "runnerjobs_p.h" #include "plasma/pluginloader.h" #include @@ -312,6 +313,8 @@ #endif } } + } else if (api == QLatin1String("DBus")){ + runner = new DBusRunner(service, q); } else { //qCDebug(KRUNNER) << "got a script runner known as" << api; runner = new AbstractRunner(service, q);