Commit aa2119a8 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

[KDED KCM] Rewrite as KDeclarative ScrollViewKCM

This rewrites the "Background services" KCM in QML using ScrollViewKCM.
The two separate list views are merged into a single one with the configurable services at the top,
and the ones that are loaded on-demand and "only for your convenience" at the bottom.
A search field is added searching through name and plugin ID. Since the sortable table headers are gone,
a filter combo is provided instead to filter for all, running, or non-running services.
As an extra Schmankerl when starting a service that immediately disables itself again (which technically isn't an error
that would be indicated as such) a hint is shown you're not left wondering why it doesn't start.
A hint is also displayed when services got automatically started/stopped when applying settings as this reloads kded5.
Furthermore, the code is cleaned up a lot (quite eerie, adding a 2020 Copyright to an existing 2002 one :),
ported to json plugin data, and a proper QAbstractListModel added.

Differential Revision: https://phabricator.kde.org/D26506
parent edc2ead6
......@@ -81,6 +81,7 @@ find_package(KSMServerDBusInterface CONFIG REQUIRED)
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"
......
# 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)
/*
* Copyright (C) 2020 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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<KDEDConfig::ModuleStatus>(idx.data(ModulesModel::StatusRole).toInt());
if (m_statusFilter != status) {
return false;
}
}
return true;
}
/*
* Copyright (c) 2020 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QSortFilterProxyModel>
#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"
};
This diff is collapsed.
......@@ -56,7 +56,7 @@ Name[uk]=Фонові служби
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
......
/* This file is part of the KDE project
Copyright (C) 2002 Daniel Molkentin <molkentin@kde.org>
Copyright (C) 2020 Kai Uwe Broulik <kde@broulik.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
......@@ -19,50 +20,87 @@
#ifndef KCMKDED_H
#define KCMKDED_H
#include <QLoggingCategory>
#include <KQuickAddons/ConfigModule>
#include <KCModule>
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();
void showRunningModulesChangedAfterSaveHint();
private:
QTreeWidget *_lvLoD;
QTreeWidget *_lvStartup;
QPushButton *_pbStart;
QPushButton *_pbStop;
QString RUNNING;
QString NOT_RUNNING;
void setKdedRunning(bool kdedRunning);
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;
QStringList m_runningModulesBeforeReconfigure;
};
#endif // KCMKDED_H
......
/*
* Copyright (C) 2020 Kai Uwe Broulik <kde@broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "modulesmodel.h"
#include <QCollator>
#include <KConfig>
#include <KConfigGroup>
#include <KPluginInfo>
#include <KPluginLoader>
#include <KServiceTypeTrader>
#include <algorithm>
#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: {
if (!m_runningModulesKnown) {
return KDEDConfig::UnknownStatus;
}
if (m_runningModules.contains(item.moduleName)) {
return KDEDConfig::Running;
}
return KDEDConfig::NotRunning;
}
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<int, QByteArray> 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<KPluginMetaData> availableModules()
{
QVector<KPluginMetaData> plugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kded"));
QSet<QString> 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<bool>() && (p.toBool() == false)) {
loadOnDemand = false;
}
return loadOnDemand;
}
void ModulesModel::load()
{
beginResetModel();
m_data.clear();
KConfig kdedrc(QStringLiteral("kded5rc"), KConfig::NoGlobals);
QStringList knownModules;
QVector<ModulesModelData> autostartModules;
QVector<ModulesModelData> 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;
if (knownModules.contains(dbusModuleName)) {
continue;
}
knownModules.append(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,
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();
}
bool ModulesModel::runningModulesKnown() const
{
return m_runningModulesKnown;
}
void ModulesModel::setRunningModulesKnown(bool known)
{
if (m_runningModulesKnown != known) {
m_runningModulesKnown = known;
emit dataChanged(index(0, 0), index(m_data.count() - 1, 0), {StatusRole});
}
}
QStringList ModulesModel::runningModules() const
{
return m_runningModules;
}
void ModulesModel::setRunningModules(const QStringList &runningModules)
{
if (m_runningModules == runningModules) {
return;
}
m_runningModules = runningModules;
if (m_runningModulesKnown) {
emit dataChanged(index(0, 0), index(m_data.count() - 1, 0), {StatusRole});
}
}
/*
* Copyright (C) 2020 Kai Uwe Broulik <kde@pbroulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QAbstractListModel>
#include <QString>
#include <QVector>
#include "kcmkded.h"
struct ModulesModelData
{
QString display;
QString description;
KDEDConfig::ModuleType type;
bool autoloadEnabled;
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<int, QByteArray> roleNames() const override;
void load();
bool runningModulesKnown() const;
void setRunningModulesKnown(bool known);
QStringList runningModules() const;
void setRunningModules(const QStringList &runningModules);
signals:
void autoloadedModulesChanged();
private:
QVector<ModulesModelData> m_data;
bool m_runningModulesKnown = false;
QStringList m_runningModules;
};
This diff is collapsed.
[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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment