diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,7 @@ find_package(KF5ItemModels CONFIG REQUIRED) find_package(KF5Emoticons CONFIG REQUIRED) find_package(KF5 REQUIRED COMPONENTS SysGuard) +find_package(KDED CONFIG REQUIRED) find_package(KF5Baloo ${KF5_MIN_VERSION}) set_package_properties(KF5Baloo PROPERTIES DESCRIPTION "File Searching" diff --git a/kcms/kded/CMakeLists.txt b/kcms/kded/CMakeLists.txt --- a/kcms/kded/CMakeLists.txt +++ b/kcms/kded/CMakeLists.txt @@ -1,11 +1,29 @@ # KI18N Translation Domain for this library add_definitions(-DTRANSLATION_DOMAIN=\"kcm5_kded\") -add_library(kcm_kded MODULE kcmkded.cpp) -target_link_libraries(kcm_kded KF5::ConfigWidgets KF5::Service KF5::I18n Qt5::DBus) +set(kcm_kded_SRCS + kcmkded.cpp + modulesmodel.cpp + filterproxymodel.cpp +) -install(TARGETS kcm_kded DESTINATION ${KDE_INSTALL_PLUGINDIR} ) +qt5_add_dbus_interface(kcm_kded_SRCS ${KDED_DBUS_INTERFACE} kded_interface) + +ecm_qt_declare_logging_category(kcm_kded_SRCS HEADER debug.h + IDENTIFIER KCM_KDED + CATEGORY_NAME kcm_kded + DEFAULT_SEVERITY Info) + +add_library(kcm_kded MODULE ${kcm_kded_SRCS}) + +kcoreaddons_desktop_to_json(kcm_kded "kcmkded.desktop") + +target_link_libraries(kcm_kded KF5::QuickAddons KF5::Service KF5::I18n Qt5::DBus) + +install(TARGETS kcm_kded DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms ) ########### install files ############### install( FILES kcmkded.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) + +kpackage_install_package(package kcm5_kded kcms) diff --git a/kcms/kded/filterproxymodel.h b/kcms/kded/filterproxymodel.h new file mode 100644 --- /dev/null +++ b/kcms/kded/filterproxymodel.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Kai Uwe Broulik + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#pragma once + +#include + +#include "kcmkded.h" + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(KDEDConfig::ModuleStatus statusFilter WRITE setStatusFilter NOTIFY statusFilterChanged) + +public: + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + QString query() const; + void setQuery(const QString &query); + + KDEDConfig::ModuleStatus statusFilter() const; + void setStatusFilter(KDEDConfig::ModuleStatus statusFilter); + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +Q_SIGNALS: + void queryChanged(); + void statusFilterChanged(); + +private: + QString m_query; + KDEDConfig::ModuleStatus m_statusFilter = KDEDConfig::UnknownStatus; // "all" + +}; diff --git a/kcms/kded/filterproxymodel.cpp b/kcms/kded/filterproxymodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/kded/filterproxymodel.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 Kai Uwe Broulik + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 "filterproxymodel.h" + +#include "modulesmodel.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + +} + +FilterProxyModel::~FilterProxyModel() = default; + +QString FilterProxyModel::query() const +{ + return m_query; +} + +void FilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + m_query = query; + invalidateFilter(); + emit queryChanged(); + } +} + +KDEDConfig::ModuleStatus FilterProxyModel::statusFilter() const +{ + return m_statusFilter; +} + +void FilterProxyModel::setStatusFilter(KDEDConfig::ModuleStatus statusFilter) +{ + if (m_statusFilter != statusFilter) { + m_statusFilter = statusFilter; + invalidateFilter(); + emit statusFilterChanged(); + } +} + +bool FilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + if (!m_query.isEmpty()) { + if (!idx.data(Qt::DisplayRole).toString().contains(m_query, Qt::CaseInsensitive) + && !idx.data(ModulesModel::ModuleNameRole).toString().contains(m_query, Qt::CaseInsensitive)) { + return false; + } + } + + if (m_statusFilter != KDEDConfig::UnknownStatus) { + const auto status = static_cast(idx.data(ModulesModel::StatusRole).toInt()); + if (m_statusFilter != status) { + return false; + } + } + + return true; +} diff --git a/kcms/kded/kcmkded.h b/kcms/kded/kcmkded.h --- a/kcms/kded/kcmkded.h +++ b/kcms/kded/kcmkded.h @@ -1,5 +1,6 @@ /* This file is part of the KDE project Copyright (C) 2002 Daniel Molkentin + Copyright (C) 2020 Kai Uwe Broulik This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public @@ -19,50 +20,84 @@ #ifndef KCMKDED_H #define KCMKDED_H -#include +#include -#include +class QDBusServiceWatcher; -class QPushButton; -class QTreeWidget; -class QTreeWidgetItem; class KConfig; class KPluginMetaData; -Q_DECLARE_LOGGING_CATEGORY(KCM_KDED) +class ModulesModel; +class FilterProxyModel; -class KDEDConfig : public KCModule +class OrgKdeKded5Interface; + +namespace org { + namespace kde { + using kded5 = ::OrgKdeKded5Interface; + } +} + +class KDEDConfig : public KQuickAddons::ConfigModule { -Q_OBJECT + Q_OBJECT + + Q_PROPERTY(ModulesModel *model READ model CONSTANT) + Q_PROPERTY(FilterProxyModel *filteredModel READ filteredModel CONSTANT) + + Q_PROPERTY(bool kdedRunning READ kdedRunning NOTIFY kdedRunningChanged) + public: - explicit KDEDConfig(QWidget* parent, const QVariantList& foo = QVariantList()); + explicit KDEDConfig(QObject* parent, const QVariantList& foo = QVariantList()); ~KDEDConfig() override {} - void load() override; - void save() override; - void defaults() override; + enum ModuleType { + UnknownType = -1, + AutostartType, + OnDemandType + }; + Q_ENUM(ModuleType) + + enum ModuleStatus { + UnknownStatus = -1, + NotRunning, + Running + }; + Q_ENUM(ModuleStatus) + + ModulesModel *model() const; + FilterProxyModel *filteredModel() const; + + bool kdedRunning() const; -protected Q_SLOTS: - void slotReload(); - void slotStartService(); - void slotStopService(); - void slotServiceRunningToggled(); - void slotStartupItemSelected(); - void slotLodItemSelected(); - void slotItemChecked(QTreeWidgetItem *item, int column); - void getServiceStatus(); + Q_INVOKABLE void startModule(const QString &moduleName); + Q_INVOKABLE void stopModule(const QString &moduleName); - bool autoloadEnabled(KConfig *config, const KPluginMetaData &filename); - void setAutoloadEnabled(KConfig *config, const KPluginMetaData &filename, bool b); + void load() override; + void save() override; + void defaults() override; + +signals: + void kdedRunningChanged(); + + void errorMessage(const QString &errorString); + void showSelfDisablingModulesHint(); private: - QTreeWidget *_lvLoD; - QTreeWidget *_lvStartup; - QPushButton *_pbStart; - QPushButton *_pbStop; - - QString RUNNING; - QString NOT_RUNNING; + void getModuleStatus(); + + void startOrStopModule(const QString &moduleName, ModuleStatus status /*better than a bool*/); + + ModulesModel *m_model; + FilterProxyModel *m_filteredModel; + + org::kde::kded5 *m_kdedInterface; + + QDBusServiceWatcher *m_kdedWatcher; + bool m_kdedRunning = false; + + QString m_lastStartedModule; + }; #endif // KCMKDED_H diff --git a/kcms/kded/kcmkded.cpp b/kcms/kded/kcmkded.cpp --- a/kcms/kded/kcmkded.cpp +++ b/kcms/kded/kcmkded.cpp @@ -1,6 +1,7 @@ // vim: noexpandtab shiftwidth=4 tabstop=4 /* This file is part of the KDE project Copyright (C) 2002 Daniel Molkentin + Copyright (C) 2020 Kai Uwe Broulik This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public @@ -20,486 +21,228 @@ #include "kcmkded.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include +#include "debug.h" +#include +#include +#include +#include + +#include +#include #include #include #include #include #include #include -K_PLUGIN_FACTORY(KDEDFactory, - registerPlugin(); - ) -K_EXPORT_PLUGIN(KDEDFactory("kcmkded")) +#include -Q_LOGGING_CATEGORY(KCM_KDED, "kcm_kded") +#include "modulesmodel.h" +#include "filterproxymodel.h" +#include "kded_interface.h" -enum OnDemandColumns -{ - OnDemandService = 0, - OnDemandStatus = 1, - OnDemandDescription = 2 -}; +K_PLUGIN_FACTORY_WITH_JSON(KCMStyleFactory, "kcmkded.json", registerPlugin();) + +static const QString s_kdedServiceName = QStringLiteral("org.kde.kded5"); -enum StartupColumns +KDEDConfig::KDEDConfig(QObject* parent, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, args) + , m_model(new ModulesModel(this)) + , m_filteredModel(new FilterProxyModel(this)) + , m_kdedInterface(new org::kde::kded5(s_kdedServiceName, + QStringLiteral("/kded"), + QDBusConnection::sessionBus())) + , m_kdedWatcher(new QDBusServiceWatcher(s_kdedServiceName, + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForOwnerChange, this)) { - StartupUse = 0, - StartupService = 1, - StartupStatus = 2, - StartupDescription = 3 -}; + qmlRegisterUncreatableType("org.kde.private.kcms.style", 1, 0, "KCM", QStringLiteral("Cannot create instances of KCM")); + // FIXME Qt 5.14 qmlRegisterAnonymousType + qmlRegisterType(); + qmlRegisterType(); + KAboutData *about = new KAboutData(QStringLiteral("kcm5_kded"), i18n("Background Services"), + QStringLiteral("2.0"), QString(), KAboutLicense::GPL, + i18n("(c) 2002 Daniel Molkentin, (c) 2020 Kai Uwe Broulik") + ); + about->addAuthor(i18n("Daniel Molkentin"), QString(),QStringLiteral("molkentin@kde.org")); + about->addAuthor(i18n("Kai Uwe Broulik"), QString(),QStringLiteral("kde@broulik.de")); + setAboutData(about); + setButtons(Apply|Default|Help); + m_filteredModel->setSourceModel(m_model); -static const int LibraryRole = Qt::UserRole + 1; + connect(m_model, &ModulesModel::autoloadedModulesChanged, this, [this] { + setNeedsSave(true); + }); -KDEDConfig::KDEDConfig(QWidget* parent, const QVariantList &) : - KCModule( parent ) -{ - KAboutData *about = - new KAboutData( I18N_NOOP( "kcmkded" ), i18n( "KDE Service Manager" ), - QStringLiteral("1.0"), QString(), KAboutLicense::GPL, - i18n( "(c) 2002 Daniel Molkentin" ) ); - about->addAuthor(i18n("Daniel Molkentin"), QString(),QStringLiteral("molkentin@kde.org")); - setAboutData( about ); - setButtons(Apply|Default|Help); - setQuickHelp( i18n("

Service Manager

This module allows you to have an overview of all plugins of the " - "KDE Daemon, also referred to as KDE Services. Generally, there are two types of service:

" - "
  • Services invoked at startup
  • Services called on demand
" - "

The latter are only listed for convenience. The startup services can be started and stopped. " - "In Administrator mode, you can also define whether services should be loaded at startup.

" - "

Use this with care: some services are vital for Plasma; do not deactivate services if you" - " do not know what you are doing.

")); - - RUNNING = i18n("Running")+' '; - NOT_RUNNING = i18n("Not running")+' '; - - QVBoxLayout *lay = new QVBoxLayout( this ); - lay->setContentsMargins( 0, 0, 0, 0 ); - - QGroupBox *gb = new QGroupBox( i18n("Load-on-Demand Services"), this ); - gb->setWhatsThis( i18n("This is a list of available KDE services which will " - "be started on demand. They are only listed for convenience, as you " - "cannot manipulate these services.")); - lay->addWidget( gb ); - - QVBoxLayout *gblay = new QVBoxLayout( gb ); - - _lvLoD = new QTreeWidget( gb ); - QStringList cols; - cols.append( i18n("Service") ); - cols.append( i18n("Status") ); - cols.append( i18n("Description") ); - _lvLoD->setHeaderLabels( cols ); - _lvLoD->setAllColumnsShowFocus(true); - _lvLoD->setRootIsDecorated( false ); - _lvLoD->setSortingEnabled(true); - _lvLoD->sortByColumn(OnDemandService, Qt::AscendingOrder); - _lvLoD->header()->setStretchLastSection(true); - gblay->addWidget( _lvLoD ); - - gb = new QGroupBox( i18n("Startup Services"), this ); - gb->setWhatsThis( i18n("This shows all KDE services that can be loaded " - "on Plasma startup. Checked services will be invoked on next startup. " - "Be careful with deactivation of unknown services.")); - lay->addWidget( gb ); - - gblay = new QVBoxLayout( gb ); - - _lvStartup = new QTreeWidget( gb ); - cols.clear(); - cols.append( i18n("Use") ); - cols.append( i18n("Service") ); - cols.append( i18n("Status") ); - cols.append( i18n("Description") ); - _lvStartup->setHeaderLabels( cols ); - _lvStartup->setAllColumnsShowFocus(true); - _lvStartup->setRootIsDecorated( false ); - _lvStartup->setSortingEnabled(true); - _lvStartup->sortByColumn(StartupService, Qt::AscendingOrder); - _lvStartup->header()->setStretchLastSection(true); - gblay->addWidget( _lvStartup ); - - QDialogButtonBox *buttonBox = new QDialogButtonBox(Qt::Horizontal, gb); - _pbStart = buttonBox->addButton( i18n("Start") , QDialogButtonBox::ActionRole ); - _pbStop = buttonBox->addButton( i18n("Stop") , QDialogButtonBox::ActionRole ); - gblay->addWidget( buttonBox ); - - _pbStart->setEnabled( false ); - _pbStop->setEnabled( false ); - - connect(_pbStart, &QPushButton::clicked, this, &KDEDConfig::slotStartService); - connect(_pbStop, &QPushButton::clicked, this, &KDEDConfig::slotStopService); - connect(_lvLoD, &QTreeWidget::itemSelectionChanged, this, &KDEDConfig::slotLodItemSelected); - connect(_lvStartup, &QTreeWidget::itemSelectionChanged, this, &KDEDConfig::slotStartupItemSelected); - connect(_lvStartup, &QTreeWidget::itemChanged, this, &KDEDConfig::slotItemChecked); + connect(m_kdedWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, + [this](const QString &service, const QString &oldOwner, const QString &newOwner) { + Q_UNUSED(service) + Q_UNUSED(oldOwner) + const bool running = !newOwner.isEmpty(); + if (m_kdedRunning != running) { + m_kdedRunning = running; + emit kdedRunningChanged(); + } + }); + m_kdedRunning = QDBusConnection::sessionBus().interface()->isServiceRegistered(s_kdedServiceName); } -QString setModuleGroup(const KPluginMetaData &module) +ModulesModel *KDEDConfig::model() const { - return QStringLiteral("Module-%1").arg(module.pluginId()); + return m_model; } -bool KDEDConfig::autoloadEnabled(KConfig *config, const KPluginMetaData &module) +FilterProxyModel *KDEDConfig::filteredModel() const { - KConfigGroup cg(config, setModuleGroup(module)); - return cg.readEntry("autoload", true); + return m_filteredModel; } -void KDEDConfig::setAutoloadEnabled(KConfig *config, const KPluginMetaData &module, bool b) +bool KDEDConfig::kdedRunning() const { - KConfigGroup cg(config, setModuleGroup(module)); - return cg.writeEntry("autoload", b); + return m_kdedRunning; } -// This code was copied from kded.cpp -// TODO: move this KCM to the KDED framework and share the code? -static QVector availableModules() +void KDEDConfig::startModule(const QString &moduleName) { - QVector plugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kded")); - QSet moduleIds; - foreach (const KPluginMetaData &md, plugins) { - moduleIds.insert(md.pluginId()); - } - // also search for old .desktop based kded modules - KPluginInfo::List oldStylePlugins = KPluginInfo::fromServices(KServiceTypeTrader::self()->query(QStringLiteral("KDEDModule"))); - foreach (const KPluginInfo &info, oldStylePlugins) { - if (moduleIds.contains(info.pluginName())) { - qCWarning(KCM_KDED).nospace() << "kded module " << info.pluginName() << " has already been found using " - "JSON metadata, please don't install the now unneeded .desktop file (" << info.entryPath() << ")."; - } else { - qCDebug(KCM_KDED).nospace() << "kded module " << info.pluginName() << " still uses .desktop files (" - << info.entryPath() << "). Please port it to JSON metadata."; - plugins.append(info.toMetaData()); - } - } - return plugins; + startOrStopModule(moduleName, Running); } -// this code was copied from kded.cpp -static bool isModuleLoadedOnDemand(const KPluginMetaData &module) +void KDEDConfig::stopModule(const QString &moduleName) { - bool loadOnDemand = true; - // use toVariant() since it could be string or bool in the json and QJsonObject does not convert - QVariant p = module.rawData().value(QStringLiteral("X-KDE-Kded-load-on-demand")).toVariant(); - if (p.isValid() && p.canConvert() && (p.toBool() == false)) { - loadOnDemand = false; - } - return loadOnDemand; + startOrStopModule(moduleName, NotRunning); } -void KDEDConfig::load() +void KDEDConfig::startOrStopModule(const QString &moduleName, ModuleStatus status) { - KConfig kdedrc(QStringLiteral("kded5rc"), KConfig::NoGlobals); + auto call = (status == NotRunning ? m_kdedInterface->unloadModule(moduleName) + : m_kdedInterface->loadModule(moduleName)); - _lvStartup->clear(); - _lvLoD->clear(); - - QTreeWidgetItem* treeitem = nullptr; - const auto modules = availableModules(); - for (const KPluginMetaData &mod : modules) { - QString servicePath = mod.metaDataFileName(); - - // autoload defaults to false if it is not found - const bool autoload = mod.rawData().value(QStringLiteral("X-KDE-Kded-autoload")).toVariant().toBool(); - // keep estimating dbusModuleName in sync with KDEDModule (kdbusaddons) and kded (kded) - // currently (KF5) the module name in the D-Bus object path is set by the pluginId - const QString dbusModuleName = mod.pluginId(); - qCDebug(KCM_KDED) << "reading kded info from" << servicePath << "autoload =" << autoload << "dbus module name =" << dbusModuleName; - - // The logic has to be identical to Kded::initModules. - // They interpret X-KDE-Kded-autoload as false if not specified - // X-KDE-Kded-load-on-demand as true if not specified - if (autoload) { - treeitem = new QTreeWidgetItem(); - treeitem->setCheckState(StartupUse, autoloadEnabled(&kdedrc, mod) ? Qt::Checked : Qt::Unchecked); - treeitem->setText(StartupService, mod.name()); - treeitem->setText(StartupDescription, mod.description()); - treeitem->setText(StartupStatus, NOT_RUNNING); - treeitem->setData(StartupService, LibraryRole, dbusModuleName); - _lvStartup->addTopLevelItem(treeitem); - } - else if (isModuleLoadedOnDemand(mod)) { - treeitem = new QTreeWidgetItem(); - treeitem->setText(OnDemandService, mod.name() ); - treeitem->setText(OnDemandDescription, mod.description()); - treeitem->setText(OnDemandStatus, NOT_RUNNING); - treeitem->setData(OnDemandService, LibraryRole, dbusModuleName); - _lvLoD->addTopLevelItem(treeitem); - } - else { - qCWarning(KCM_KDED) << "kcmkded: Module " << mod.name() << "from file" << mod.metaDataFileName() << " not loaded on demand or startup! Skipping."; - } - } - - _lvStartup->resizeColumnToContents(StartupUse); - _lvStartup->resizeColumnToContents(StartupService); - _lvStartup->resizeColumnToContents(StartupStatus); - - _lvLoD->resizeColumnToContents(OnDemandService); - _lvLoD->resizeColumnToContents(OnDemandStatus); - - getServiceStatus(); - - emit changed(false); -} + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(call, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, moduleName, status](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + watcher->deleteLater(); -void KDEDConfig::save() -{ - KConfig kdedrc(QStringLiteral("kded5rc"), KConfig::NoGlobals); + if (reply.isError()) { + if (status == NotRunning) { + emit errorMessage(i18n("Failed to stop service: %1", reply.error().message())); + } else { + emit errorMessage(i18n("Failed to start service: %1", reply.error().message())); + } + return; + } - const auto modules = availableModules(); - for (const KPluginMetaData &mod : modules) { - qCDebug(KCM_KDED) << "saving settings for kded module" << mod.pluginId(); - // autoload defaults to false if it is not found - const bool autoload = mod.rawData().value(QStringLiteral("X-KDE-Kded-autoload")).toVariant().toBool(); - if (autoload) { - const QString libraryName = mod.pluginId(); - int count = _lvStartup->topLevelItemCount(); - for(int i = 0; i < count; ++i) { - QTreeWidgetItem *treeitem = _lvStartup->topLevelItem(i); - if ( treeitem->data(StartupService, LibraryRole ).toString() == libraryName) { - // we found a match, now compare and see what changed - setAutoloadEnabled(&kdedrc, mod, treeitem->checkState( StartupUse ) == Qt::Checked); - break; - } - } - } - } - kdedrc.sync(); - - emit changed(false); - - QDBusInterface kdedInterface(QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QStringLiteral("org.kde.kded5")); - kdedInterface.call(QStringLiteral("reconfigure")); - QTimer::singleShot(0, this, &KDEDConfig::slotServiceRunningToggled); -} + if (!reply.value()) { + if (status == NotRunning) { + emit errorMessage(i18n("Failed to stop service.")); + } else { + emit errorMessage(i18n("Failed to start service.")); + } + return; + } + qCDebug(KCM_KDED) << "Successfully" << (status == Running ? "started" : "stopped") << moduleName; + if (status == Running) { + m_lastStartedModule = moduleName; + } else { + m_lastStartedModule.clear(); + } + getModuleStatus(); + }); +} -void KDEDConfig::defaults() +void KDEDConfig::getModuleStatus() { - int count = _lvStartup->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - { - _lvStartup->topLevelItem( i )->setCheckState( StartupUse, Qt::Checked ); - } - - getServiceStatus(); + auto call = m_kdedInterface->loadedModules(); - emit changed(true); -} + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(call, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + watcher->deleteLater(); + if (reply.isError()) { + qCWarning(KCM_KDED) << "Failed to get loaded modules" << reply.error().name() << reply.error().message(); + return; + } -void KDEDConfig::getServiceStatus() -{ - QStringList modules; - QDBusInterface kdedInterface( QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QStringLiteral("org.kde.kded5") ); - QDBusReply reply = kdedInterface.call( QStringLiteral("loadedModules") ); - - if ( reply.isValid() ) { - modules = reply.value(); - } - else { - _lvLoD->setEnabled( false ); - _lvStartup->setEnabled( false ); - KMessageBox::error(this, i18n("Unable to contact KDED.")); - return; - } - qCDebug(KCM_KDED) << "Loaded kded modules:" << modules; - - // Initialize - int count = _lvLoD->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - _lvLoD->topLevelItem( i )->setText( OnDemandStatus, NOT_RUNNING ); - count = _lvStartup->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - _lvStartup->topLevelItem( i )->setText( StartupStatus, NOT_RUNNING ); - - // Fill - foreach( const QString& module, modules ) - { - bool found = false; - - count = _lvLoD->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - { - QTreeWidgetItem *treeitem = _lvLoD->topLevelItem( i ); - if ( treeitem->data( OnDemandService, LibraryRole ).toString() == module ) - { - treeitem->setText( OnDemandStatus, RUNNING ); - found = true; - break; - } - } - - count = _lvStartup->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - { - QTreeWidgetItem *treeitem = _lvStartup->topLevelItem( i ); - if ( treeitem->data( StartupService, LibraryRole ).toString() == module ) - { - treeitem->setText( StartupStatus, RUNNING ); - found = true; - break; - } - } - - if (!found) - { - qCDebug(KCM_KDED) << "Could not relate module " << module; -#ifndef NDEBUG - qCDebug(KCM_KDED) << "Candidates were:"; - count = _lvLoD->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - { - QTreeWidgetItem *treeitem = _lvLoD->topLevelItem( i ); - qCDebug(KCM_KDED) << treeitem->data( OnDemandService, LibraryRole ).toString(); - } - - count = _lvStartup->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - { - QTreeWidgetItem *treeitem = _lvStartup->topLevelItem( i ); - qCDebug(KCM_KDED) << treeitem->data( StartupService, LibraryRole ).toString(); - } -#endif - } - - } + QStringList runningModules = reply.value(); + m_model->setRunningModules(runningModules); + // Check if the user just tried starting a module that then disabled itself again. + // Some kded modules disable themselves on start when they deem themselves unnecessary + // based on some configuration independ of kded or the current environment. + // At least provide some feedback and not leave the user wondering why the service doesn't start. + if (!m_lastStartedModule.isEmpty() && !runningModules.contains(m_lastStartedModule)) { + emit showSelfDisablingModulesHint(); + } + m_lastStartedModule.clear(); + }); } -void KDEDConfig::slotReload() +void KDEDConfig::load() { - QString current; - if ( !_lvStartup->selectedItems().isEmpty() ) - current = _lvStartup->selectedItems().at(0)->data( StartupService, LibraryRole ).toString(); - load(); - if ( !current.isEmpty() ) - { - int count = _lvStartup->topLevelItemCount(); - for( int i = 0; i < count; ++i ) - { - QTreeWidgetItem *treeitem = _lvStartup->topLevelItem( i ); - if ( treeitem->data( StartupService, LibraryRole ).toString() == current ) - { - _lvStartup->setCurrentItem( treeitem, 0, QItemSelectionModel::ClearAndSelect ); - break; - } - } - } -} + m_model->load(); -void KDEDConfig::slotStartupItemSelected() -{ - if ( _lvStartup->selectedItems().isEmpty() ) { - // Disable the buttons - _pbStart->setEnabled( false ); - _pbStop->setEnabled( false ); - return; - } - - // Deselect a currently selected element in the "load on demand" treeview - _lvLoD->setCurrentItem(nullptr, 0, QItemSelectionModel::Clear); - - QTreeWidgetItem *item = _lvStartup->selectedItems().at(0); - if ( item->text(StartupStatus) == RUNNING ) { - _pbStart->setEnabled( false ); - _pbStop->setEnabled( true ); - } - else if ( item->text(StartupStatus) == NOT_RUNNING ) { - _pbStart->setEnabled( true ); - _pbStop->setEnabled( false ); - } - else // Error handling, better do nothing - { - _pbStart->setEnabled( false ); - _pbStop->setEnabled( false ); - } - - getServiceStatus(); + getModuleStatus(); + + setNeedsSave(false); } -void KDEDConfig::slotLodItemSelected() +void KDEDConfig::save() { - if ( _lvLoD->selectedItems().isEmpty() ) - return; + KConfig kdedrc(QStringLiteral("kded5rc"), KConfig::NoGlobals); - // Deselect a currently selected element in the "load on startup" treeview - _lvStartup->setCurrentItem(nullptr, 0, QItemSelectionModel::Clear); -} + for (int i = 0; i < m_model->rowCount(); ++i) { + const QModelIndex idx = m_model->index(i, 0); -void KDEDConfig::slotServiceRunningToggled() -{ - getServiceStatus(); - slotStartupItemSelected(); -} + const auto type = static_cast(idx.data(ModulesModel::TypeRole).toInt()); + if (type != AutostartType) { + continue; + } -void KDEDConfig::slotStartService() -{ - QString service = _lvStartup->selectedItems().at(0)->data( StartupService, LibraryRole ).toString(); - - QDBusInterface kdedInterface( QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"),QStringLiteral("org.kde.kded5") ); - QDBusReply reply = kdedInterface.call( QStringLiteral("loadModule"), service ); - - if ( reply.isValid() ) { - if ( reply.value() ) - slotServiceRunningToggled(); - else - KMessageBox::error(this, "" + i18n("Unable to start server %1.", service) + ""); - } - else { - KMessageBox::error(this, "" + i18n("Unable to start service %1.

Error: %2", - service, reply.error().message()) + "
" ); - } -} + const QString moduleName = idx.data(ModulesModel::ModuleNameRole).toString(); -void KDEDConfig::slotStopService() -{ - QString service = _lvStartup->selectedItems().at(0)->data( StartupService, LibraryRole ).toString(); - qCDebug(KCM_KDED) << "Stopping: " << service; - - QDBusInterface kdedInterface( QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QStringLiteral("org.kde.kded5") ); - QDBusReply reply = kdedInterface.call( QStringLiteral("unloadModule"), service ); - - if ( reply.isValid() ) { - if ( reply.value() ) - slotServiceRunningToggled(); - else - KMessageBox::error(this, "" + i18n("Unable to stop service %1.", service) + ""); - } - else { - KMessageBox::error(this, "" + i18n("Unable to stop service %1.

Error: %2", - service, reply.error().message()) + "
" ); - } + const bool autoloadEnabled = idx.data(ModulesModel::AutoloadEnabledRole).toBool(); + KConfigGroup cg(&kdedrc, QStringLiteral("Module-%1").arg(moduleName)); + cg.writeEntry("autoload", autoloadEnabled); + } + + kdedrc.sync(); + + setNeedsSave(false); + + // Is all of this really necessary? I would also think it to be fire and forget... + // Only if changing autoload for a module may load/unload it, otherwise there's no point. + // Autoload doesn't affect a running session and reloading the running modules is also useless then. + auto call = m_kdedInterface->reconfigure(); + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(call, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + watcher->deleteLater(); + + if (reply.isError()) { + emit errorMessage(i18n("Failed to notify KDE Service Manager (kded5) of saved changed: %1", reply.error().message())); + return; + } + + qCDebug(KCM_KDED) << "Successfully reconfigured kded"; + getModuleStatus(); + }); } -void KDEDConfig::slotItemChecked(QTreeWidgetItem*, int column) +void KDEDConfig::defaults() { - // We only listen to changes the user did. - if (column==StartupUse) { - emit changed(true); - } + for (int i = 0; i < m_model->rowCount(); ++i) { + const QModelIndex idx = m_model->index(i, 0); + if (m_model->setData(idx, true, ModulesModel::AutoloadEnabledRole)) { + setNeedsSave(true); + } + } } #include "kcmkded.moc" diff --git a/kcms/kded/kcmkded.desktop b/kcms/kded/kcmkded.desktop --- a/kcms/kded/kcmkded.desktop +++ b/kcms/kded/kcmkded.desktop @@ -56,7 +56,7 @@ Name[x-test]=xxBackground Servicesxx Name[zh_CN]=后台服务 Name[zh_TW]=背景服務 -Comment=Background Services +Comment=Configure background services Comment[ar]=خدمات الخلفيّة Comment[ast]=Servicios en segundu planu Comment[bs]=Pozadinski servisi diff --git a/kcms/kded/modulesmodel.h b/kcms/kded/modulesmodel.h new file mode 100644 --- /dev/null +++ b/kcms/kded/modulesmodel.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 Kai Uwe Broulik + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#pragma once + +#include +#include +#include + +#include "kcmkded.h" + +struct ModulesModelData +{ + QString display; + QString description; + KDEDConfig::ModuleType type; + bool autoloadEnabled; + KDEDConfig::ModuleStatus status; + QString moduleName; +}; +Q_DECLARE_TYPEINFO(ModulesModelData, Q_MOVABLE_TYPE); + +class ModulesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ModulesModel(QObject *parent); + ~ModulesModel() override; + + enum Roles { + DescriptionRole = Qt::UserRole + 1, + TypeRole, + AutoloadEnabledRole, + StatusRole, + ModuleNameRole + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QHash roleNames() const override; + + void load(); + + QStringList runningModules() const; + void setRunningModules(const QStringList &runningModules); + +signals: + void autoloadedModulesChanged(); + +private: + QVector m_data; + + QStringList m_runningModules; + +}; diff --git a/kcms/kded/modulesmodel.cpp b/kcms/kded/modulesmodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/kded/modulesmodel.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2020 Kai Uwe Broulik + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 "modulesmodel.h" + +#include + +#include +#include +#include +#include +#include + +#include + +#include "debug.h" + +ModulesModel::ModulesModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +ModulesModel::~ModulesModel() = default; + +int ModulesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant ModulesModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index)) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: return item.display; + case DescriptionRole: return item.description; + case TypeRole: return item.type; + case AutoloadEnabledRole: + if (item.type == KDEDConfig::AutostartType) { + return item.autoloadEnabled; + } + return QVariant(); + case StatusRole: return item.status; + case ModuleNameRole: return item.moduleName; + } + + return QVariant(); +} + +bool ModulesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + bool dirty = false; + + if (!checkIndex(index)) { + return dirty; + } + + auto &item = m_data[index.row()]; + + switch (role) { + case AutoloadEnabledRole: + const bool autoloadEnabled = value.toBool(); + if (item.type == KDEDConfig::AutostartType + && item.autoloadEnabled != autoloadEnabled) { + item.autoloadEnabled = value.toBool(); + dirty = true; + + emit autoloadedModulesChanged(); + } + break; + } + + if (dirty) { + emit dataChanged(index, index, {role}); + } + + return dirty; +} + +QHash ModulesModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {TypeRole, QByteArrayLiteral("type")}, + {AutoloadEnabledRole, QByteArrayLiteral("autoloadEnabled")}, + {StatusRole, QByteArrayLiteral("status")}, + {ModuleNameRole, QByteArrayLiteral("moduleName")}, + }; +} + +// This code was copied from kded.cpp +// TODO: move this KCM to the KDED framework and share the code? +static QVector availableModules() +{ + QVector plugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kded")); + QSet moduleIds; + for (const KPluginMetaData &md : qAsConst(plugins)) { + moduleIds.insert(md.pluginId()); + } + // also search for old .desktop based kded modules + const KPluginInfo::List oldStylePlugins = KPluginInfo::fromServices(KServiceTypeTrader::self()->query(QStringLiteral("KDEDModule"))); + for (const KPluginInfo &info : oldStylePlugins) { + if (moduleIds.contains(info.pluginName())) { + qCWarning(KCM_KDED).nospace() << "kded module " << info.pluginName() << " has already been found using " + "JSON metadata, please don't install the now unneeded .desktop file (" << info.entryPath() << ")."; + } else { + qCDebug(KCM_KDED).nospace() << "kded module " << info.pluginName() << " still uses .desktop files (" + << info.entryPath() << "). Please port it to JSON metadata."; + plugins.append(info.toMetaData()); + } + } + return plugins; +} + +// this code was copied from kded.cpp +static bool isModuleLoadedOnDemand(const KPluginMetaData &module) +{ + bool loadOnDemand = true; + // use toVariant() since it could be string or bool in the json and QJsonObject does not convert + QVariant p = module.rawData().value(QStringLiteral("X-KDE-Kded-load-on-demand")).toVariant(); + if (p.isValid() && p.canConvert() && (p.toBool() == false)) { + loadOnDemand = false; + } + return loadOnDemand; +} + +void ModulesModel::load() +{ + beginResetModel(); + + m_data.clear(); + + KConfig kdedrc(QStringLiteral("kded5rc"), KConfig::NoGlobals); + + QVector autostartModules; + QVector onDemandModules; + + const auto modules = availableModules(); + for (const KPluginMetaData &module : modules) { + QString servicePath = module.metaDataFileName(); + + // autoload defaults to false if it is not found + const bool autoload = module.rawData().value(QStringLiteral("X-KDE-Kded-autoload")).toVariant().toBool(); + // keep estimating dbusModuleName in sync with KDEDModule (kdbusaddons) and kded (kded) + // currently (KF5) the module name in the D-Bus object path is set by the pluginId + const QString dbusModuleName = module.pluginId(); + qCDebug(KCM_KDED) << "reading kded info from" << servicePath << "autoload =" << autoload << "dbus module name =" << dbusModuleName; + + KConfigGroup cg(&kdedrc, QStringLiteral("Module-%1").arg(dbusModuleName)); + const bool autoloadEnabled = cg.readEntry("autoload", true); + + ModulesModelData data{ + module.name(), + module.description(), + KDEDConfig::UnknownType, + autoloadEnabled, + KDEDConfig::UnknownStatus, + dbusModuleName + }; + + // The logic has to be identical to Kded::initModules. + // They interpret X-KDE-Kded-autoload as false if not specified + // X-KDE-Kded-load-on-demand as true if not specified + if (autoload) { + data.type = KDEDConfig::AutostartType; + autostartModules << data; + } else if (isModuleLoadedOnDemand(module)) { + data.type = KDEDConfig::OnDemandType; + onDemandModules << data; + } else { + qCWarning(KCM_KDED) << "kcmkded: Module " << module.name() << "from file" << module.metaDataFileName() << " not loaded on demand or startup! Skipping."; + continue; + } + } + + QCollator collator; + // Otherwise "Write" daemon with quotes will be at the top + collator.setIgnorePunctuation(true); + auto sortAlphabetically = [&collator](const ModulesModelData &a, const ModulesModelData &b) { + return collator.compare(a.display, b.display) < 0; + }; + + std::sort(autostartModules.begin(), autostartModules.end(), sortAlphabetically); + std::sort(onDemandModules.begin(), onDemandModules.end(), sortAlphabetically); + + m_data << autostartModules << onDemandModules; + + endResetModel(); +} + +QStringList ModulesModel::runningModules() const +{ + return m_runningModules; +} + +void ModulesModel::setRunningModules(const QStringList &runningModules) +{ + if (m_runningModules == runningModules) { + return; + } + + m_runningModules = runningModules; + + for (auto it = m_data.begin(), end = m_data.end(); it != end; ++it) { + if (runningModules.contains(it->moduleName)) { + it->status = KDEDConfig::Running; + } else { + it->status = KDEDConfig::NotRunning; + } + } + + emit dataChanged(index(0, 0), index(m_data.count() - 1, 0), {StatusRole}); +} diff --git a/kcms/kded/package/contents/ui/main.qml b/kcms/kded/package/contents/ui/main.qml new file mode 100644 --- /dev/null +++ b/kcms/kded/package/contents/ui/main.qml @@ -0,0 +1,290 @@ +/* + * Copyright 2020 Kai Uwe Broulik + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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.6 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls +import org.kde.kirigami 2.10 as Kirigami +import org.kde.kcm 1.2 as KCM +import org.kde.private.kcms.style 1.0 as Private + +KCM.ScrollViewKCM { + id: root + + KCM.ConfigModule.quickHelp: i18n("

This module allows you to have an overview of all plugins of the +KDE Daemon, also referred to as KDE Services. Generally, there are two types of service:

+
  • Services invoked at startup
  • Services called on demand
+

The latter are only listed for convenience. The startup services can be started and stopped. +You can also define whether services should be loaded at startup.

+

Use this with care: some services are vital for Plasma; do not deactivate services if you + do not know what you are doing.

") + + // TODO immutable somehow? + + Binding { + target: kcm.filteredModel + property: "query" + value: searchField.text + } + + Binding { + target: kcm.filteredModel + property: "statusFilter" + value: filterCombo.model[filterCombo.currentIndex].statusFilter + } + + header: ColumnLayout { + Kirigami.InlineMessage { + Layout.fillWidth: true + text: i18n("The KDE Service Manager (kded5) is currently not running. Make sure it is installed correctly."); + type: Kirigami.MessageType.Error + showCloseButton: false + visible: !kcm.kdedRunning + } + + Kirigami.InlineMessage { + id: selfDisablingModulesHint + Layout.fillWidth: true + text: i18n("Some services disable themselves again when manually started if they are not useful in the current environment.") + type: Kirigami.MessageType.Information + showCloseButton: true + visible: false + } + + Kirigami.InlineMessage { + id: errorMessage + Layout.fillWidth: true + + type: Kirigami.MessageType.Error + showCloseButton: true + visible: false + + Connections { + target: kcm + onErrorMessage: { + errorMessage.text = errorString; + errorMessage.visible = true; + } + onShowSelfDisablingModulesHint: { + selfDisablingModulesHint.visible = true; + } + } + } + + RowLayout { + Layout.fillWidth: true + + Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + } + + QtControls.ComboBox { + id: filterCombo + textRole: "text" + model: [ + {text: i18n("All Services"), statusFilter: Private.KCM.UnknownStatus}, + {text: i18nc("List running services", "Running"), statusFilter: Private.KCM.Running}, + {text: i18nc("List not running services", "Not Running"), statusFilter: Private.KCM.NotRunning} + ] + + // HACK QQC2 doesn't support icons, so we just tamper with the desktop style ComboBox's background + // and inject a nice little filter icon. + Component.onCompleted: { + if (!background || !background.hasOwnProperty("properties")) { + // not a KQuickStyleItem + return; + } + + var props = background.properties || {}; + + background.properties = Qt.binding(function() { + var newProps = props; + newProps.currentIcon = "view-filter"; + newProps.iconColor = Kirigami.Theme.textColor; + return newProps; + }); + } + } + } + } + + view: ListView { + id: list + clip: true + activeFocusOnTab: true + + model: kcm.filteredModel + + section.property: "type" + section.delegate: Kirigami.ListSectionHeader { + width: list.width + label: { + switch (Number(section)) { + case Private.KCM.AutostartType: return i18n("Startup Services"); + case Private.KCM.OnDemandType: return i18n("Load-on-Demand Services"); + } + } + } + + Component { + id: listDelegateComponent + + Kirigami.AbstractListItem { + id: delegate + // FIXME why does the padding logic to dodge the ScrollBars not work here? + text: model.display + checkable: model.type !== Private.KCM.OnDemandType + checked: model.autoloadEnabled === true + hoverEnabled: checkable + focusPolicy: Qt.ClickFocus + Accessible.description: i18n("Toggle automatically loading this service on startup") + onClicked: { + if (checkable) { + model.autoloadEnabled = !model.autoloadEnabled; + } + } + + Component.onCompleted: { + // Checkable Kirigami.ListItem has blue background which we don't want + // as we have a dedicated CheckBox. Still using those properties for accessibility. + background.color = Qt.binding(function() { + return delegate.highlighted || (delegate.supportsMouseEvents && delegate.pressed) + ? delegate.activeBackgroundColor + : delegate.backgroundColor + }); + } + + contentItem: RowLayout { + QtControls.CheckBox { + id: autoloadCheck + // Keep focus on the delegate + focusPolicy: Qt.NoFocus + checked: delegate.checked + visible: delegate.checkable + onToggled: model.autoloadEnabled = !model.autoloadEnabled + + QtControls.ToolTip { + text: delegate.Accessible.description + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + QtControls.Label { + id: displayLabel + Layout.fillWidth: true + text: delegate.text + elide: Text.ElideRight + textFormat: Text.PlainText + color: (delegate.highlighted || (delegate.pressed && delegate.supportsMouseEvents)) ? delegate.activeTextColor : delegate.textColor + } + + QtControls.Label { + Layout.fillWidth: true + text: model.description + // FIXME do we have a descriptive label component? + opacity: delegate.hovered ? 0.8 : 0.6 + wrapMode: Text.WordWrap + textFormat: Text.PlainText + color: displayLabel.color + } + } + + QtControls.Label { + id: statusLabel + transformOrigin: Item.Right + horizontalAlignment: Text.AlignRight + opacity: model.status === Private.KCM.Running ? 1 : delegate.hovered ? 0.8 : 0.6 + color: displayLabel.color + text: { + switch (model.status) { + case Private.KCM.NotRunning: return i18n("Not running"); + case Private.KCM.Running: return i18n("Running"); + } + return i18n("Unknown"); + } + + readonly property int status: model.status + onStatusChanged: { + if (Kirigami.Units.longDuration > 1) { + statusChangedAnimation.restart(); + } + } + + // FIXME use DelegateRecycler.onReused and stop animation when delegate gets reused + // Otherwise label blinks as you scroll up and down the list :) + SequentialAnimation { + id: statusChangedAnimation + PropertyAction { + target: statusLabel + property: "color" + value: model.status === Private.KCM.Running ? Kirigami.Theme.positiveTextColor + : Kirigami.Theme.negativeTextColor + } + NumberAnimation { + target: statusLabel + property: "scale" + from: 1.5 + to: 1 + duration: Kirigami.Units.longDuration + easing.type: Easing.InOutQuad + } + ColorAnimation { + target: statusLabel + property: "color" + to: displayLabel.color + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + + QtControls.Button { + icon.name: model.status === Private.KCM.Running ? "media-playback-pause" : "media-playback-start" + visible: model.type !== Private.KCM.OnDemandType + onClicked: { + selfDisablingModulesHint.visible = false; + errorMessage.visible = false; + + if (model.status === Private.KCM.Running) { + kcm.stopModule(model.moduleName); + } else { + kcm.startModule(model.moduleName); + } + } + Accessible.name: model.status === Private.KCM.Running ? i18n("Stop Service") : i18n("Start Service") + + QtControls.ToolTip { + text: parent.Accessible.name + } + } + } + } + } + + delegate: Kirigami.DelegateRecycler { + width: list.width + sourceComponent: listDelegateComponent + } + } +} diff --git a/kcms/kded/package/metadata.desktop b/kcms/kded/package/metadata.desktop new file mode 100644 --- /dev/null +++ b/kcms/kded/package/metadata.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Name=Background Services +Comment=Configure background services + +Icon=preferences-system-session-services +Type=Service +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kdebroulik.de +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kcm_style +X-KDE-PluginInfo-Version=2.0 +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript + +X-Plasma-MainScript=ui/main.qml