diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 58c03be..98e47fd 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,8 +1,21 @@ find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test) include(ECMAddTests) ecm_add_tests( 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 index 0000000..163f77e --- /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 index 0000000..78c5b21 --- /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.cpp b/autotests/testremoterunner.cpp new file mode 100644 index 0000000..0d21a9f --- /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/autotests/testremoterunner.h b/autotests/testremoterunner.h new file mode 100644 index 0000000..6ad7c79 --- /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/src/CMakeLists.txt b/src/CMakeLists.txt index 4c98c29..f01594e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,98 +1,110 @@ 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) target_include_directories(KF5Runner INTERFACE "$") target_include_directories(KF5Runner PUBLIC "$") target_link_libraries(KF5Runner PUBLIC Qt5::Core KF5::Plasma # Must be public because abstractrunner.h needs plasma/version.h PRIVATE + Qt5::DBus Qt5::Gui Qt5::Widgets KF5::ConfigCore KF5::Service KF5::I18n KF5::ThreadWeaver KF5::Solid KF5::CoreAddons #KShell KF5::KIOCore #KProtocolInfo ) set_target_properties(KF5Runner PROPERTIES VERSION ${KRUNNER_VERSION_STRING} SOVERSION 5 EXPORT_NAME "Runner" ) ecm_generate_headers(KRunner_CamelCase_HEADERS HEADER_NAMES AbstractRunner RunnerContext RunnerManager RunnerSyntax QueryMatch PREFIX KRunner REQUIRED_HEADERS KRunner_HEADERS ) # Install files install(TARGETS KF5Runner EXPORT KF5RunnerTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KRunner_CamelCase_HEADERS} DESTINATION ${KF5_INCLUDE_INSTALL_DIR}/KRunner/KRunner COMPONENT Devel) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/krunner_export.h ${KRunner_HEADERS} DESTINATION ${KF5_INCLUDE_INSTALL_DIR}/KRunner/krunner COMPONENT Devel) install(FILES data/servicetypes/plasma-runner.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) if(BUILD_QCH) ecm_add_qch( KF5Runner_QCH NAME KRunner BASE_NAME KF5Runner VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KRunner_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" LINK_QCHS Qt5Core_QCH BLANK_MACROS KRUNNER_EXPORT KRUNNER_DEPRECATED KRUNNER_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) 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 index a80aba8..4e5c1f3 100644 --- a/src/abstractrunner.cpp +++ b/src/abstractrunner.cpp @@ -1,389 +1,389 @@ /* * Copyright 2006-2007 Aaron Seigo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 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 "abstractrunner.h" -#include +#include #include #include #include #include #include #include #include #include "krunner_debug.h" #include #include #include #include #include #include #include "abstractrunner_p.h" #include "querymatch.h" #include "runnercontext.h" namespace Plasma { AbstractRunner::AbstractRunner(QObject *parent, const QString &path) : QObject(parent), d(new AbstractRunnerPrivate(this)) { d->init(path); } AbstractRunner::AbstractRunner(const KService::Ptr service, QObject *parent) : QObject(parent), d(new AbstractRunnerPrivate(this)) { d->init(service); } AbstractRunner::AbstractRunner(QObject *parent, const QVariantList &args) : QObject(parent), d(new AbstractRunnerPrivate(this)) { if (args.count() > 0) { KService::Ptr service = KService::serviceByStorageId(args[0].toString()); if (service) { d->init(service); } } } AbstractRunner::~AbstractRunner() { delete d; } KConfigGroup AbstractRunner::config() const { QString group = id(); if (group.isEmpty()) { group = QStringLiteral("UnnamedRunner"); } KConfigGroup runners(KSharedConfig::openConfig(), "Runners"); return KConfigGroup(&runners, group); } void AbstractRunner::reloadConfiguration() { } void AbstractRunner::addSyntax(const RunnerSyntax &syntax) { d->syntaxes.append(syntax); } void AbstractRunner::setDefaultSyntax(const RunnerSyntax &syntax) { d->syntaxes.append(syntax); d->defaultSyntax = &(d->syntaxes.last()); } void AbstractRunner::setSyntaxes(const QList &syntaxes) { d->syntaxes = syntaxes; } QList AbstractRunner::syntaxes() const { return d->syntaxes; } RunnerSyntax *AbstractRunner::defaultSyntax() const { return d->defaultSyntax; } void AbstractRunner::performMatch(Plasma::RunnerContext &localContext) { static const int reasonableRunTime = 1500; static const int fastEnoughTime = 250; if (d->suspendMatching) { return; } QTime time; time.restart(); //The local copy is already obtained in the job match(localContext); // automatically rate limit runners that become slooow const int runtime = time.elapsed(); bool slowed = speed() == SlowSpeed; if (!slowed && runtime > reasonableRunTime) { // we punish runners that return too slowly, even if they don't bring // back matches #ifndef NDEBUG // qCDebug(KRUNNER) << id() << "runner is too slow, putting it on the back burner."; #endif d->fastRuns = 0; setSpeed(SlowSpeed); } if (slowed && runtime < fastEnoughTime && localContext.query().size() > 2) { ++d->fastRuns; if (d->fastRuns > 2) { // we reward slowed runners who bring back matches fast enough // 3 times in a row #ifndef NDEBUG // qCDebug(KRUNNER) << id() << "runner is faster than we thought, kicking it up a notch"; #endif setSpeed(NormalSpeed); } } } QList AbstractRunner::actionsForMatch(const Plasma::QueryMatch &match) { Q_UNUSED(match) QList ret; return ret; } QAction* AbstractRunner::addAction(const QString &id, const QIcon &icon, const QString &text) { QAction *a = new QAction(icon, text, this); d->actions.insert(id, a); return a; } void AbstractRunner::addAction(const QString &id, QAction *action) { d->actions.insert(id, action); } void AbstractRunner::removeAction(const QString &id) { QAction *a = d->actions.take(id); delete a; } QAction* AbstractRunner::action(const QString &id) const { return d->actions.value(id); } QHash AbstractRunner::actions() const { return d->actions; } void AbstractRunner::clearActions() { qDeleteAll(d->actions); d->actions.clear(); } QMimeData *AbstractRunner::mimeDataForMatch(const QueryMatch &match) { Q_UNUSED(match) return nullptr; } bool AbstractRunner::hasRunOptions() { return d->hasRunOptions; } void AbstractRunner::setHasRunOptions(bool hasRunOptions) { d->hasRunOptions = hasRunOptions; } void AbstractRunner::createRunOptions(QWidget *parent) { Q_UNUSED(parent); } AbstractRunner::Speed AbstractRunner::speed() const { // the only time the read lock will fail is if we were slow are going to speed up // or if we were fast and are going to slow down; so don't wait in this case, just // say we're slow. we either will be soon or were just a moment ago and it doesn't // hurt to do one more run the slow way if (!d->speedLock.tryLockForRead()) { return SlowSpeed; } Speed s = d->speed; d->speedLock.unlock(); return s; } void AbstractRunner::setSpeed(Speed speed) { d->speedLock.lockForWrite(); d->speed = speed; d->speedLock.unlock(); } AbstractRunner::Priority AbstractRunner::priority() const { return d->priority; } void AbstractRunner::setPriority(Priority priority) { d->priority = priority; } RunnerContext::Types AbstractRunner::ignoredTypes() const { return d->blackListed; } void AbstractRunner::setIgnoredTypes(RunnerContext::Types types) { d->blackListed = types; } void AbstractRunner::run(const Plasma::RunnerContext &search, const Plasma::QueryMatch &action) { Q_UNUSED(search); Q_UNUSED(action); } QStringList AbstractRunner::categories() const { return QStringList() << name(); } QIcon AbstractRunner::categoryIcon(const QString&) const { return icon(); } void AbstractRunner::match(Plasma::RunnerContext &) { } QString AbstractRunner::name() const { if (d->runnerDescription.isValid()) { return d->runnerDescription.name(); } return objectName(); } QIcon AbstractRunner::icon() const { if (d->runnerDescription.isValid()) { return QIcon::fromTheme(d->runnerDescription.icon()); } return QIcon(); } QString AbstractRunner::id() const { if (d->runnerDescription.isValid()) { return d->runnerDescription.pluginName(); } return objectName(); } QString AbstractRunner::description() const { if (d->runnerDescription.isValid()) { return d->runnerDescription.property(QStringLiteral("Comment")).toString(); } return objectName(); } KPluginInfo AbstractRunner::metadata() const { return d->runnerDescription; } Package AbstractRunner::package() const { return d->package ? *d->package : Package(); } void AbstractRunner::init() { reloadConfiguration(); } DataEngine *AbstractRunner::dataEngine(const QString &name) const { return d->dataEngine(name); } bool AbstractRunner::isMatchingSuspended() const { return d->suspendMatching; } void AbstractRunner::suspendMatching(bool suspend) { if (d->suspendMatching == suspend) { return; } d->suspendMatching = suspend; emit matchingSuspended(suspend); } AbstractRunnerPrivate::AbstractRunnerPrivate(AbstractRunner *r) : priority(AbstractRunner::NormalPriority), speed(AbstractRunner::NormalSpeed), blackListed(RunnerContext::None), runner(r), fastRuns(0), package(nullptr), defaultSyntax(nullptr), hasRunOptions(false), suspendMatching(false) { } AbstractRunnerPrivate::~AbstractRunnerPrivate() { delete package; package = nullptr; } void AbstractRunnerPrivate::init(const KService::Ptr service) { runnerDescription = KPluginInfo(service); } void AbstractRunnerPrivate::init(const QString &path) { runnerDescription = KPluginInfo(path + "/metadata.desktop"); const QString api = runnerDescription.property(QStringLiteral("X-Plasma-API")).toString(); } } // Plasma namespace #include "moc_abstractrunner.cpp" diff --git a/src/data/org.kde.krunner1.xml b/src/data/org.kde.krunner1.xml new file mode 100644 index 0000000..b9d39a1 --- /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 index 282ee07..d60f042 100644 --- a/src/data/servicetypes/plasma-runner.desktop +++ b/src/data/servicetypes/plasma-runner.desktop @@ -1,61 +1,69 @@ [Desktop Entry] Type=ServiceType X-KDE-ServiceType=Plasma/Runner Comment=KRunner plugin Comment[ar]=ملحقة «مشغّلك» Comment[ast]=Complementu KRunner Comment[bs]=Priključak za KRunner Comment[ca]=Connector del KRunner Comment[ca@valencia]=Connector del KRunner Comment[cs]=Modul KRunneru Comment[da]=KRunner-plugin Comment[de]=KRunner-Modul Comment[el]=Πρόσθετο KRunner Comment[en_GB]=KRunner plugin Comment[es]=Complemento para KRunner Comment[et]=KRunneri plugin Comment[eu]=KRunner plugina Comment[fi]=KRunner-liitännäinen Comment[fr]=Module externe de KRunner Comment[gd]=Plugan KRunner Comment[gl]=Complemento KRunner Comment[hu]=KRunner bővítmény Comment[ia]=Plugin de KRunner Comment[it]=Estensione di KRunner Comment[ko]=KRunner 플러그인 Comment[lt]=KRunner papildinys Comment[mr]=KRunner प्लगइन Comment[nb]=KRunner-programtillegg Comment[nds]=KRunner-Moduul Comment[nl]=KRunner-plugin Comment[nn]=KRunner-tillegg Comment[pa]=ਕੇਰਨਰ ਪਲੱਗਇਨ Comment[pl]=Wtyczka KRunner Comment[pt]='Plugin' do KRunner Comment[pt_BR]=Plugin do KRunner Comment[ro]=Extensie KRunner Comment[ru]=Расширение KRunner Comment[sk]=KRunner modul Comment[sl]=Vstavek za KRunner Comment[sr]=Прикључак К‑извођача Comment[sr@ijekavian]=Прикључак К‑извођача Comment[sr@ijekavianlatin]=Priključak K‑izvođača Comment[sr@latin]=Priključak K‑izvođača Comment[sv]=Insticksprogram för Kör program Comment[tr]=KRunner eklentisi Comment[ug]=KRunner قىستۇرما Comment[uk]=Додаток KRunner Comment[x-test]=xxKRunner pluginxx Comment[zh_CN]=KRunner 插件 Comment[zh_TW]=KRunner 外掛程式 [PropertyDef::X-Plasma-AdvertiseSingleRunnerQueryMode] Type=bool [PropertyDef::TryExec] Type=QString [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 index 0000000..1106a8c --- /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 index 0000000..0f9b805 --- /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 index 0000000..755308e --- /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.cpp b/src/querymatch.cpp index 0c1fc82..ca0db63 100644 --- a/src/querymatch.cpp +++ b/src/querymatch.cpp @@ -1,352 +1,352 @@ /* * Copyright 2006-2007 Aaron Seigo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 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 "querymatch.h" -#include +#include #include #include #include #include #include #include #include #include "krunner_debug.h" #include "abstractrunner.h" namespace Plasma { class QueryMatchPrivate : public QSharedData { public: QueryMatchPrivate(AbstractRunner *r) : QSharedData(), lock(new QReadWriteLock(QReadWriteLock::Recursive)), runner(r), type(QueryMatch::ExactMatch), relevance(.7), selAction(nullptr), enabled(true), idSetByData(false) { } QueryMatchPrivate(const QueryMatchPrivate &other) : QSharedData(other), lock(new QReadWriteLock(QReadWriteLock::Recursive)) { QReadLocker l(other.lock); runner = other.runner; type = other.type; relevance = other.relevance; selAction = other.selAction; enabled = other.enabled; idSetByData = other.idSetByData; matchCategory = other.matchCategory; id = other.id; text = other.text; subtext = other.subtext; icon = other.icon; iconName = other.iconName; data = other.data; mimeType = other.mimeType; urls = other.urls; } ~QueryMatchPrivate() { delete lock; } QReadWriteLock *lock; QPointer runner; QueryMatch::Type type; QString matchCategory; QString id; QString text; QString subtext; QString mimeType; QList urls; QIcon icon; QString iconName; QVariant data; qreal relevance; QAction *selAction; bool enabled : 1; bool idSetByData : 1; }; QueryMatch::QueryMatch(AbstractRunner *runner) : d(new QueryMatchPrivate(runner)) { // qCDebug(KRUNNER) << "new match created"; } QueryMatch::QueryMatch(const QueryMatch &other) : d(other.d) { } QueryMatch::~QueryMatch() { } bool QueryMatch::isValid() const { return d->runner != 0; } QString QueryMatch::id() const { if (d->id.isEmpty() && d->runner) { return d->runner.data()->id(); } return d->id; } void QueryMatch::setType(Type type) { d->type = type; } QueryMatch::Type QueryMatch::type() const { return d->type; } void QueryMatch::setMatchCategory(const QString &category) { d->matchCategory = category; } QString QueryMatch::matchCategory() const { if (d->matchCategory.isEmpty() && d->runner) { return d->runner->name(); } return d->matchCategory; } void QueryMatch::setRelevance(qreal relevance) { d->relevance = qMax(qreal(0.0), relevance); } qreal QueryMatch::relevance() const { return d->relevance; } AbstractRunner* QueryMatch::runner() const { return d->runner.data(); } void QueryMatch::setText(const QString &text) { QWriteLocker locker(d->lock); d->text = text; } void QueryMatch::setSubtext(const QString &subtext) { QWriteLocker locker(d->lock); d->subtext = subtext; } void QueryMatch::setData(const QVariant & data) { QWriteLocker locker(d->lock); d->data = data; if (d->id.isEmpty() || d->idSetByData) { const QString id = data.toString(); if (!id.isEmpty()) { setId(data.toString()); d->idSetByData = true; } } } void QueryMatch::setId(const QString &id) { QWriteLocker locker(d->lock); if (d->runner) { d->id = d->runner.data()->id(); } if (!id.isEmpty()) { d->id.append('_').append(id); } d->idSetByData = false; } void QueryMatch::setIcon(const QIcon &icon) { QWriteLocker locker(d->lock); d->icon = icon; } void QueryMatch::setIconName(const QString &iconName) { QWriteLocker locker(d->lock); d->iconName = iconName; } QVariant QueryMatch::data() const { QReadLocker locker(d->lock); return d->data; } QString QueryMatch::text() const { QReadLocker locker(d->lock); return d->text; } QString QueryMatch::subtext() const { QReadLocker locker(d->lock); return d->subtext; } QIcon QueryMatch::icon() const { QReadLocker locker(d->lock); return d->icon; } QString QueryMatch::iconName() const { QReadLocker locker(d->lock); return d->iconName; } void QueryMatch::setMimeType(const QString &mimeType) { QWriteLocker locker(d->lock); d->mimeType = mimeType; } QString QueryMatch::mimeType() const { QReadLocker locker(d->lock); return d->mimeType; } void QueryMatch::setUrls(const QList &urls) { QWriteLocker locker(d->lock); d->urls = urls; } QList QueryMatch::urls() const { QReadLocker locker(d->lock); return d->urls; } void QueryMatch::setEnabled(bool enabled) { d->enabled = enabled; } bool QueryMatch::isEnabled() const { return d->enabled && d->runner; } QAction* QueryMatch::selectedAction() const { return d->selAction; } void QueryMatch::setSelectedAction(QAction *action) { d->selAction = action; } bool QueryMatch::operator<(const QueryMatch &other) const { if (d->type == other.d->type) { if (isEnabled() != other.isEnabled()) { return other.isEnabled(); } if (d->relevance != other.d->relevance) { return d->relevance < other.d->relevance; } QReadLocker locker(d->lock); QReadLocker otherLocker(other.d->lock); // when resorting to sort by alpha, we want the // reverse sort order! return d->text > other.d->text; } return d->type < other.d->type; } QueryMatch &QueryMatch::operator=(const QueryMatch &other) { if (d != other.d) { d = other.d; } return *this; } bool QueryMatch::operator==(const QueryMatch &other) const { return (d == other.d); } bool QueryMatch::operator!=(const QueryMatch &other) const { return (d != other.d); } void QueryMatch::run(const RunnerContext &context) const { //qCDebug(KRUNNER) << "we run the term" << context->query() << "whose type is" << context->mimetype(); if (d->runner) { d->runner.data()->run(context, *this); } } bool QueryMatch::hasConfigurationInterface() const { return d->runner && d->runner.data()->hasRunOptions(); } void QueryMatch::createConfigurationInterface(QWidget *parent) { if (hasConfigurationInterface()) { d->runner.data()->createRunOptions(parent); } } } // Plasma namespace diff --git a/src/querymatch.h b/src/querymatch.h index ab76dbf..ff272b6 100644 --- a/src/querymatch.h +++ b/src/querymatch.h @@ -1,312 +1,312 @@ /* * Copyright 2006-2007 Aaron Seigo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 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. */ #ifndef PLASMA_QUERYMATCH_H #define PLASMA_QUERYMATCH_H #include #include #include #include "krunner_export.h" class QAction; class QIcon; class QString; class QVariant; class QWidget; namespace Plasma { class RunnerContext; class AbstractRunner; class QueryMatchPrivate; /** * @class QueryMatch querymatch.h * * @short A match returned by an AbstractRunner in response to a given * RunnerContext. */ class KRUNNER_EXPORT QueryMatch { public: /** * The type of match. Value is important here as it is used for sorting */ enum Type { NoMatch = 0, /**< Null match */ CompletionMatch = 10, /**< Possible completion for the data of the query */ PossibleMatch = 30, /**< Something that may match the query */ InformationalMatch = 50, /**< A purely informational, non-actionable match, such as the answer to a question or calculation*/ HelperMatch = 70, /**< A match that represents an action not directly related to activating the given search term, such as a search in an external tool or a command learning trigger. Helper matches tend to be generic to the query and should not be autoactivated just because the user hits "Enter" while typing. They must be explicitly selected to be activated, but unlike InformationalMatch cause an action to be triggered. */ ExactMatch = 100 /**< An exact match to the query */ }; /** * Constructs a PossibleMatch associated with a given RunnerContext * and runner. * * @param runner the runner this match belongs to */ - explicit QueryMatch(AbstractRunner *runner); + explicit QueryMatch(AbstractRunner *runner = nullptr); /** * Copy constructor */ QueryMatch(const QueryMatch &other); ~QueryMatch(); QueryMatch &operator=(const QueryMatch &other); bool operator==(const QueryMatch &other) const; bool operator!=(const QueryMatch &other) const; bool operator<(const QueryMatch &other) const; /** * @return the runner associated with this action */ AbstractRunner *runner() const; /** * Requests this match to activae using the given context * * @param context the context to use in conjunction with this run * * @sa AbstractRunner::run */ void run(const RunnerContext &context) const; /** * @return true if the match is valid and can therefore be run, * an invalid match does not have an associated AbstractRunner */ bool isValid() const; /** * Sets the type of match this action represents. */ void setType(Type type); /** * The type of action this is. Defaults to PossibleMatch. */ Type type() const; /** * Sets information about the type of the match which can * be used to categorize the match. * * This string should be translated as it can be displayed * in an UI */ void setMatchCategory(const QString& category); /** * Extra information about the match which can be used * to categorize the type. * * By default this returns the internal name of the runner * which returned this result */ QString matchCategory() const; /** * Sets the relevance of this action for the search * it was created for. * * @param relevance a number between 0 and 1. */ void setRelevance(qreal relevance); /** * The relevance of this action to the search. By default, * the relevance is 1. * * @return a number between 0 and 1 */ qreal relevance() const; /** * Sets data to be used internally by the associated * AbstractRunner. * * When set, it is also used to form * part of the id() for this match. If that is innapropriate * as an id, the runner may generate its own id and set that * with setId(const QString&) directly after calling setData */ void setData(const QVariant &data); /** * @return the data associated with this match; usually runner-specific */ QVariant data() const; /** * Sets the id for this match; useful if the id does not * match data().toString(). The id must be unique to all * matches from this runner, and should remain constant * for the same query for best results. * * @param id the new identifying string to use to refer * to this entry */ 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, * ensuring global uniqueness as well as consistency * between query matches. */ QString id() const; /** * Sets the main title text for this match; should be short * enough to fit nicely on one line in a user interface * * @param text the text to use as the title */ void setText(const QString &text); /** * @return the title text for this match */ QString text() const; /** * Sets the descriptive text for this match; can be longer * than the main title text * * @param text the text to use as the description */ void setSubtext(const QString &text); /** * @return the descriptive text for this match */ QString subtext() const; /** * Sets the icon associated with this match * * Prefer using setIconName. * * @param icon the icon to show along with the match */ void setIcon(const QIcon &icon); /** * @return the icon for this match */ QIcon icon() const; /** * Sets the icon name associated with this match * * @param icon the name of the icon to show along with the match * @since 5.24 */ void setIconName(const QString &iconName); /** * @return the name of the icon for this match * @since 5.24 */ QString iconName() const; /** * Sets the MimeType, if any, associated with this match. * This overrides the MimeType provided by QueryContext, and should only be * set when it is different from the QueryContext MimeType */ void setMimeType(const QString &mimeType); /** * @return the mimtype for this match, or QString() is none */ QString mimeType() const; /** * Sets the urls, if any, associated with this match */ void setUrls(const QList &urls); /** * @return the mimtype for this match, or QString() is none */ QList urls() const; /** * Sets whether or not this match can be activited * * @param enable true if the match is enabled and therefore runnable */ void setEnabled(bool enable); /** * @return true if the match is enabled and therefore runnable, otherwise false */ bool isEnabled() const; /** * The current action. */ QAction* selectedAction() const; /** * Sets the selected action */ void setSelectedAction(QAction *action); /** * @return true if this match can be configured before being run * @since 4.3 */ bool hasConfigurationInterface() const; /** * If hasConfigurationInterface() returns true, this method may be called to get * a widget displaying the options the user can interact with to modify * the behaviour of what happens when the match is run. * * @param widget the parent of the options widgets. * @since 4.3 */ void createConfigurationInterface(QWidget *parent); private: QSharedDataPointer d; }; } #endif diff --git a/src/runnermanager.cpp b/src/runnermanager.cpp index 3c206e1..39c40ff 100644 --- a/src/runnermanager.cpp +++ b/src/runnermanager.cpp @@ -1,851 +1,854 @@ /* * Copyright (C) 2006 Aaron Seigo * Copyright (C) 2007, 2009 Ryan P. Bitanga * Copyright (C) 2008 Jordi Polo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 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 "runnermanager.h" #include #include #include #include #include #include "krunner_debug.h" #include #include #include #include #include #include #include #include +#include "dbusrunner_p.h" #include "runnerjobs_p.h" #include "plasma/pluginloader.h" #include #include "querymatch.h" using ThreadWeaver::Queue; using ThreadWeaver::Job; //#define MEASURE_PREPTIME namespace Plasma { /***************************************************** * RunnerManager::Private class * *****************************************************/ class RunnerManagerPrivate { public: RunnerManagerPrivate(RunnerManager *parent) : q(parent), deferredRun(nullptr), currentSingleRunner(nullptr), prepped(false), allRunnersPrepped(false), singleRunnerPrepped(false), teardownRequested(false), singleMode(false), singleRunnerWasLoaded(false) { matchChangeTimer.setSingleShot(true); delayTimer.setSingleShot(true); QObject::connect(&matchChangeTimer, SIGNAL(timeout()), q, SLOT(matchesChanged())); QObject::connect(&context, SIGNAL(matchesChanged()), q, SLOT(scheduleMatchesChanged())); QObject::connect(&delayTimer, SIGNAL(timeout()), q, SLOT(unblockJobs())); } ~RunnerManagerPrivate() { KConfigGroup config = configGroup(); context.save(config); } void scheduleMatchesChanged() { matchChangeTimer.start(100); } void matchesChanged() { emit q->matchesChanged(context.matches()); } void loadConfiguration() { KConfigGroup config = configGroup(); //The number of threads used scales with the number of processors. const int numProcs = qMax(Solid::Device::listFromType(Solid::DeviceInterface::Processor).count(), 1); //This entry allows to define a hard upper limit independent of the number of processors. const int maxThreads = config.readEntry("maxThreads", 16); const int numThreads = qMin(maxThreads, 2 + ((numProcs - 1) * 2)); //qCDebug(KRUNNER) << "setting up" << numThreads << "threads for" << numProcs << "processors"; if (numThreads > Queue::instance()->maximumNumberOfThreads()) { Queue::instance()->setMaximumNumberOfThreads(numThreads); } // Limit the number of instances of a single normal speed runner and all of the slow runners // to half the number of threads const int cap = qMax(2, numThreads/2); DefaultRunnerPolicy::instance().setCap(cap); enabledCategories = config.readEntry("enabledCategories", QStringList()); context.restore(config); } KConfigGroup configGroup() { return conf.isValid() ? conf : KConfigGroup(KSharedConfig::openConfig(), "PlasmaRunnerManager"); } void clearSingleRunner() { if (singleRunnerWasLoaded) { delete currentSingleRunner; } currentSingleRunner = nullptr; } void loadSingleRunner() { if (!singleMode || singleModeRunnerId.isEmpty()) { clearSingleRunner(); return; } if (currentSingleRunner) { if (currentSingleRunner->id() == singleModeRunnerId) { return; } clearSingleRunner(); } AbstractRunner *loadedRunner = q->runner(singleModeRunnerId); if (loadedRunner) { singleRunnerWasLoaded = false; currentSingleRunner = loadedRunner; return; } KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Plasma/Runner"), QStringLiteral("[X-KDE-PluginInfo-Name] == '%1'").arg(singleModeRunnerId)); if (!offers.isEmpty()) { const KService::Ptr &service = offers[0]; currentSingleRunner = loadInstalledRunner(service); if (currentSingleRunner) { emit currentSingleRunner->prepare(); singleRunnerWasLoaded = true; } } } void loadRunners() { KConfigGroup config = configGroup(); KPluginInfo::List offers = RunnerManager::listRunnerInfo(); const bool loadAll = config.readEntry("loadAll", false); const QStringList whiteList = config.readEntry("pluginWhiteList", QStringList()); const bool noWhiteList = whiteList.isEmpty(); KConfigGroup pluginConf; if (conf.isValid()) { pluginConf = KConfigGroup(&conf, "Plugins"); } else { pluginConf = KConfigGroup(KSharedConfig::openConfig(), "Plugins"); } advertiseSingleRunnerIds.clear(); QStringList allCategories; QSet deadRunners; QMutableListIterator it(offers); while (it.hasNext()) { KPluginInfo &description = it.next(); qCDebug(KRUNNER) << "Loading runner: " << description.pluginName(); QString tryExec = description.property(QStringLiteral("TryExec")).toString(); if (!tryExec.isEmpty() && QStandardPaths::findExecutable(tryExec).isEmpty()) { // we don't actually have this application! continue; } const QString runnerName = description.pluginName(); description.load(pluginConf); const bool loaded = runners.contains(runnerName); const bool selected = loadAll || (description.isPluginEnabled() && (noWhiteList || whiteList.contains(runnerName))); const bool singleQueryModeEnabled = description.property(QStringLiteral("X-Plasma-AdvertiseSingleRunnerQueryMode")).toBool(); if (singleQueryModeEnabled) { advertiseSingleRunnerIds.insert(runnerName, description.name()); } if (selected) { AbstractRunner *runner = nullptr; if (!loaded) { runner = loadInstalledRunner(description.service()); } else { runner = runners.value(runnerName); } if (runner) { const QStringList categories = runner->categories(); allCategories << categories; bool allCategoriesDisabled = true; Q_FOREACH (const QString &cat, categories) { if (enabledCategories.contains(cat)) { allCategoriesDisabled = false; break; } } if (enabledCategories.isEmpty() || !allCategoriesDisabled) { qCDebug(KRUNNER) << "Loaded:" << runnerName; runners.insert(runnerName, runner); } else { runners.remove(runnerName); deadRunners.insert(runner); qCDebug(KRUNNER) << "Categories not enabled. Removing runner: " << runnerName; } } } else if (loaded) { //Remove runner deadRunners.insert(runners.take(runnerName)); qCDebug(KRUNNER) << "Plugin disabled. Removing runner: " << runnerName; } } if (enabledCategories.isEmpty()) { enabledCategories = allCategories; } if (!deadRunners.isEmpty()) { QSet > deadJobs; auto it = searchJobs.begin(); while (it != searchJobs.end()) { auto &job = (*it); if (deadRunners.contains(job->runner())) { QObject::disconnect(job->decorator(), SIGNAL(done(ThreadWeaver::JobPointer)), q, SLOT(jobDone(ThreadWeaver::JobPointer))); it = searchJobs.erase(it); deadJobs.insert(job); } else { it++; } } it = oldSearchJobs.begin(); while (it != oldSearchJobs.end()) { auto &job = (*it); if (deadRunners.contains(job->runner())) { it = oldSearchJobs.erase(it); deadJobs.insert(job); } else { it++; } } if (deadJobs.isEmpty()) { qDeleteAll(deadRunners); } else { new DelayedJobCleaner(deadJobs, deadRunners); } } if (!singleRunnerWasLoaded) { // in case we deleted it up above clearSingleRunner(); } #ifndef NDEBUG // qCDebug(KRUNNER) << "All runners loaded, total:" << runners.count(); #endif } AbstractRunner *loadInstalledRunner(const KService::Ptr service) { if (!service) { return nullptr; } AbstractRunner *runner = nullptr; const QString api = service->property(QStringLiteral("X-Plasma-API")).toString(); if (api.isEmpty()) { QVariantList args; args << service->storageId(); if (Plasma::isPluginVersionCompatible(KPluginLoader(*service).pluginVersion())) { QString error; runner = service->createInstance(q, args, &error); if (!runner) { #ifndef NDEBUG // qCDebug(KRUNNER) << "Failed to load runner:" << service->name() << ". error reported:" << error; #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); } if (runner) { #ifndef NDEBUG // qCDebug(KRUNNER) << "================= loading runner:" << service->name() << "================="; #endif QObject::connect(runner, SIGNAL(matchingSuspended(bool)), q, SLOT(runnerMatchingSuspended(bool))); runner->init(); if (prepped) { emit runner->prepare(); } } return runner; } void jobDone(ThreadWeaver::JobPointer job) { auto runJob = job.dynamicCast(); if (!runJob) { return; } if (deferredRun.isEnabled() && runJob->runner() == deferredRun.runner()) { //qCDebug(KRUNNER) << "job actually done, running now **************"; QueryMatch tmpRun = deferredRun; deferredRun = QueryMatch(nullptr); tmpRun.run(context); } searchJobs.remove(runJob); oldSearchJobs.remove(runJob); if (searchJobs.isEmpty() && context.matches().isEmpty()) { // we finished our run, and there are no valid matches, and so no // signal will have been sent out. so we need to emit the signal // ourselves here emit q->matchesChanged(context.matches()); } checkTearDown(); } void checkTearDown() { //qCDebug(KRUNNER) << prepped << teardownRequested << searchJobs.count() << oldSearchJobs.count(); if (!prepped || !teardownRequested) { return; } if (Queue::instance()->isIdle()) { searchJobs.clear(); oldSearchJobs.clear(); } if (searchJobs.isEmpty() && oldSearchJobs.isEmpty()) { if (allRunnersPrepped) { foreach (AbstractRunner *runner, runners) { emit runner->teardown(); } allRunnersPrepped = false; } if (singleRunnerPrepped) { if (currentSingleRunner) { emit currentSingleRunner->teardown(); } singleRunnerPrepped = false; } emit q->queryFinished(); prepped = false; teardownRequested = false; } } void unblockJobs() { if (searchJobs.isEmpty() && Queue::instance()->isIdle()) { oldSearchJobs.clear(); checkTearDown(); return; } Queue::instance()->reschedule(); } void runnerMatchingSuspended(bool suspended) { if (suspended || !prepped || teardownRequested) { return; } AbstractRunner *runner = qobject_cast(q->sender()); if (runner) { startJob(runner); } } void startJob(AbstractRunner *runner) { if ((runner->ignoredTypes() & context.type()) == 0) { QSharedPointer job(new FindMatchesJob(runner, &context, Queue::instance())); QObject::connect(job->decorator(), SIGNAL(done(ThreadWeaver::JobPointer)), q, SLOT(jobDone(ThreadWeaver::JobPointer))); if (runner->speed() == AbstractRunner::SlowSpeed) { job->setDelayTimer(&delayTimer); } Queue::instance()->enqueue(job); searchJobs.insert(job); } } // Delay in ms before slow runners are allowed to run static const int slowRunDelay = 400; RunnerManager *q; QueryMatch deferredRun; RunnerContext context; QTimer matchChangeTimer; QTimer delayTimer; // Timer to control when to run slow runners QHash runners; QHash advertiseSingleRunnerIds; AbstractRunner* currentSingleRunner; QSet > searchJobs; QSet > oldSearchJobs; KConfigGroup conf; QStringList enabledCategories; QString singleModeRunnerId; bool prepped : 1; bool allRunnersPrepped : 1; bool singleRunnerPrepped : 1; bool teardownRequested : 1; bool singleMode : 1; bool singleRunnerWasLoaded : 1; }; /***************************************************** * RunnerManager::Public class * *****************************************************/ RunnerManager::RunnerManager(QObject *parent) : QObject(parent), d(new RunnerManagerPrivate(this)) { d->loadConfiguration(); //ThreadWeaver::setDebugLevel(true, 4); } RunnerManager::RunnerManager(KConfigGroup &c, QObject *parent) : QObject(parent), d(new RunnerManagerPrivate(this)) { // Should this be really needed? Maybe d->loadConfiguration(c) would make // more sense. d->conf = KConfigGroup(&c, "PlasmaRunnerManager"); d->loadConfiguration(); //ThreadWeaver::setDebugLevel(true, 4); } RunnerManager::~RunnerManager() { if (!qApp->closingDown() && (!d->searchJobs.isEmpty() || !d->oldSearchJobs.isEmpty())) { new DelayedJobCleaner(d->searchJobs + d->oldSearchJobs); } delete d; } void RunnerManager::reloadConfiguration() { KSharedConfig::openConfig()->reparseConfiguration(); d->loadConfiguration(); d->loadRunners(); } void RunnerManager::setAllowedRunners(const QStringList &runners) { KConfigGroup config = d->configGroup(); config.writeEntry("pluginWhiteList", runners); if (!d->runners.isEmpty()) { // this has been called with runners already created. so let's do an instant reload d->loadRunners(); } } void RunnerManager::setEnabledCategories(const QStringList& categories) { KConfigGroup config = d->configGroup(); config.writeEntry("enabledCategories", categories); d->enabledCategories = categories; if (!d->runners.isEmpty()) { d->loadRunners(); } } QStringList RunnerManager::allowedRunners() const { KConfigGroup config = d->configGroup(); return config.readEntry("pluginWhiteList", QStringList()); } QStringList RunnerManager::enabledCategories() const { KConfigGroup config = d->configGroup(); QStringList list = config.readEntry("enabledCategories", QStringList()); if (list.isEmpty()) { Q_FOREACH (AbstractRunner* runner, d->runners) { list << runner->categories(); } } return list; } void RunnerManager::loadRunner(const KService::Ptr service) { KPluginInfo description(service); const QString runnerName = description.pluginName(); if (!runnerName.isEmpty() && !d->runners.contains(runnerName)) { AbstractRunner *runner = d->loadInstalledRunner(service); if (runner) { d->runners.insert(runnerName, runner); } } } void RunnerManager::loadRunner(const QString &path) { if (!d->runners.contains(path)) { AbstractRunner *runner = new AbstractRunner(this, path); connect(runner, SIGNAL(matchingSuspended(bool)), this, SLOT(runnerMatchingSuspended(bool))); d->runners.insert(path, runner); } } AbstractRunner* RunnerManager::runner(const QString &name) const { if (d->runners.isEmpty()) { d->loadRunners(); } return d->runners.value(name, 0); } AbstractRunner *RunnerManager::singleModeRunner() const { return d->currentSingleRunner; } void RunnerManager::setSingleModeRunnerId(const QString &id) { d->singleModeRunnerId = id; d->loadSingleRunner(); } QString RunnerManager::singleModeRunnerId() const { return d->singleModeRunnerId; } bool RunnerManager::singleMode() const { return d->singleMode; } void RunnerManager::setSingleMode(bool singleMode) { if (d->singleMode == singleMode) { return; } Plasma::AbstractRunner *prevSingleRunner = d->currentSingleRunner; d->singleMode = singleMode; d->loadSingleRunner(); d->singleMode = d->currentSingleRunner; if (prevSingleRunner != d->currentSingleRunner) { if (d->prepped) { matchSessionComplete(); if (d->singleMode) { setupMatchSession(); } } } } QList RunnerManager::runners() const { return d->runners.values(); } QStringList RunnerManager::singleModeAdvertisedRunnerIds() const { return d->advertiseSingleRunnerIds.keys(); } QString RunnerManager::runnerName(const QString &id) const { if (runner(id)) { return runner(id)->name(); } else { return d->advertiseSingleRunnerIds.value(id, QString()); } } RunnerContext* RunnerManager::searchContext() const { return &d->context; } //Reordering is here so data is not reordered till strictly needed QList RunnerManager::matches() const { return d->context.matches(); } void RunnerManager::run(const QString &matchId) { run(d->context.match(matchId)); } void RunnerManager::run(const QueryMatch &match) { if (!match.isEnabled()) { return; } //TODO: this function is not const as it may be used for learning AbstractRunner *runner = match.runner(); for (auto it = d->searchJobs.constBegin(); it != d->searchJobs.constEnd(); ++it) { if ((*it)->runner() == runner && !(*it)->isFinished()) { #ifndef NDEBUG // qCDebug(KRUNNER) << "deferred run"; #endif d->deferredRun = match; return; } } if (d->deferredRun.isValid()) { d->deferredRun = QueryMatch(nullptr); } d->context.run(match); } QList RunnerManager::actionsForMatch(const QueryMatch &match) { AbstractRunner *runner = match.runner(); if (runner) { return runner->actionsForMatch(match); } return QList(); } QMimeData * RunnerManager::mimeDataForMatch(const QString &id) const { return mimeDataForMatch(d->context.match(id)); } QMimeData * RunnerManager::mimeDataForMatch(const QueryMatch &match) const { AbstractRunner *runner = match.runner(); if (runner) { return runner->mimeDataForMatch(match); } return nullptr; } KPluginInfo::List RunnerManager::listRunnerInfo(const QString &parentApp) { QString constraint; if (parentApp.isEmpty()) { constraint.append("not exist [X-KDE-ParentApp]"); } else { constraint.append("[X-KDE-ParentApp] == '").append(parentApp).append("'"); } KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Plasma/Runner"), constraint); return KPluginInfo::fromServices(offers); } void RunnerManager::setupMatchSession() { d->teardownRequested = false; if (d->prepped) { return; } d->prepped = true; if (d->singleMode) { if (d->currentSingleRunner) { emit d->currentSingleRunner->prepare(); d->singleRunnerPrepped = true; } } else { foreach (AbstractRunner *runner, d->runners) { #ifdef MEASURE_PREPTIME QTime t; t.start(); #endif emit runner->prepare(); #ifdef MEASURE_PREPTIME #ifndef NDEBUG // qCDebug(KRUNNER) << t.elapsed() << runner->name(); #endif #endif } d->allRunnersPrepped = true; } } void RunnerManager::matchSessionComplete() { if (!d->prepped) { return; } d->teardownRequested = true; d->checkTearDown(); } void RunnerManager::launchQuery(const QString &term) { launchQuery(term, QString()); } void RunnerManager::launchQuery(const QString &untrimmedTerm, const QString &runnerName) { setupMatchSession(); QString term = untrimmedTerm.trimmed(); setSingleModeRunnerId(runnerName); setSingleMode(d->currentSingleRunner); if (!runnerName.isEmpty() && !d->currentSingleRunner) { reset(); return; } if (term.isEmpty()) { if (d->singleMode && d->currentSingleRunner->defaultSyntax()) { term = d->currentSingleRunner->defaultSyntax()->exampleQueries().first().remove(QRegExp(QStringLiteral(":q:"))); } else { reset(); return; } } if (d->context.query() == term) { // we already are searching for this! return; } if (!d->singleMode && d->runners.isEmpty()) { d->loadRunners(); } reset(); // qCDebug(KRUNNER) << "runners searching for" << term << "on" << runnerName; d->context.setQuery(term); d->context.setEnabledCategories(d->enabledCategories); QHash runable; //if the name is not empty we will launch only the specified runner if (d->singleMode) { runable.insert(QString(), d->currentSingleRunner); d->context.setSingleRunnerQueryMode(true); } else { runable = d->runners; } foreach (Plasma::AbstractRunner *r, runable) { if (r->isMatchingSuspended()) { continue; } d->startJob(r); } // Start timer to unblock slow runners d->delayTimer.start(RunnerManagerPrivate::slowRunDelay); } QString RunnerManager::query() const { return d->context.query(); } void RunnerManager::reset() { // If ThreadWeaver is idle, it is safe to clear previous jobs if (Queue::instance()->isIdle()) { d->oldSearchJobs.clear(); } else { for (auto it = d->searchJobs.constBegin(); it != d->searchJobs.constEnd(); ++it) { Queue::instance()->dequeue((*it)); } d->oldSearchJobs += d->searchJobs; } d->searchJobs.clear(); if (d->deferredRun.isEnabled()) { //qCDebug(KRUNNER) << "job actually done, running now **************"; QueryMatch tmpRun = d->deferredRun; d->deferredRun = QueryMatch(nullptr); tmpRun.run(d->context); } d->context.reset(); } } // Plasma namespace #include "moc_runnermanager.cpp"