1 /*
2     SPDX-FileCopyrightText: 2020 Marco Martin <mart@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "SensorFaceController.h"
8 #include "SensorFaceController_p.h"
9 #include "SensorFace_p.h"
10 #include <Sensor.h>
11 #include <SensorQuery.h>
12 
13 #include <QtQml>
14 
15 #include <KConfigLoader>
16 #include <KDeclarative/ConfigPropertyMap>
17 #include <KDesktopFile>
18 #include <KLocalizedString>
19 #include <KPackage/PackageLoader>
20 #include <KPluginMetaData>
21 #include <Solid/Block>
22 #include <Solid/Device>
23 #include <Solid/Predicate>
24 #include <Solid/StorageAccess>
25 #include <Solid/StorageVolume>
26 
27 using namespace KSysGuard;
28 
FacesModel(QObject * parent)29 FacesModel::FacesModel(QObject *parent)
30     : QStandardItemModel(parent)
31 {
32     reload();
33 }
34 
reload()35 void FacesModel::reload()
36 {
37     clear();
38 
39     auto list = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KSysguard/SensorFace"));
40     // NOTE: This will disable completely the internal in-memory cache
41     KPackage::Package p;
42     p.install(QString(), QString());
43 
44     for (auto plugin : list) {
45         QStandardItem *item = new QStandardItem(plugin.name());
46         item->setData(plugin.pluginId(), FacesModel::PluginIdRole);
47         appendRow(item);
48     }
49 }
50 
pluginId(int row)51 QString FacesModel::pluginId(int row)
52 {
53     return data(index(row, 0), PluginIdRole).toString();
54 }
55 
roleNames() const56 QHash<int, QByteArray> FacesModel::roleNames() const
57 {
58     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
59 
60     roles[PluginIdRole] = "pluginId";
61     return roles;
62 }
63 
PresetsModel(QObject * parent)64 PresetsModel::PresetsModel(QObject *parent)
65     : QStandardItemModel(parent)
66 {
67     reload();
68 }
69 
reload()70 void PresetsModel::reload()
71 {
72     clear();
73     QList<KPluginMetaData> plugins =
74         KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/Applet"), QString(), [](const KPluginMetaData &plugin) {
75             return plugin.value(QStringLiteral("X-Plasma-RootPath")) == QStringLiteral("org.kde.plasma.systemmonitor");
76         });
77 
78     QSet<QString> usedNames;
79 
80     // We iterate backwards because packages under ~/.local are listed first, while we want them last
81     auto it = plugins.rbegin();
82     for (; it != plugins.rend(); ++it) {
83         const auto &plugin = *it;
84         KPackage::Package p = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), plugin.pluginId());
85         KDesktopFile df(p.path() + QStringLiteral("metadata.desktop"));
86 
87         QString baseName = df.readName();
88         QString name = baseName;
89         int id = 0;
90 
91         while (usedNames.contains(name)) {
92             name = baseName + QStringLiteral(" (") + QString::number(++id) + QStringLiteral(")");
93         }
94         usedNames << name;
95 
96         QStandardItem *item = new QStandardItem(baseName);
97 
98         // TODO config
99         QVariantMap config;
100 
101         KConfigGroup configGroup(KSharedConfig::openConfig(p.filePath("config", QStringLiteral("faceproperties")), KConfig::SimpleConfig),
102                                  QStringLiteral("Config"));
103 
104         const QStringList keys = configGroup.keyList();
105         for (const QString &key : keys) {
106             // all strings for now, type conversion happens in QML side when we have the config property map
107             config.insert(key, configGroup.readEntry(key));
108         }
109 
110         item->setData(plugin.pluginId(), PresetsModel::PluginIdRole);
111         item->setData(config, PresetsModel::ConfigRole);
112 
113         item->setData(QFileInfo(p.path() + QStringLiteral("metadata.desktop")).isWritable(), PresetsModel::WritableRole);
114 
115         appendRow(item);
116     }
117 }
118 
roleNames() const119 QHash<int, QByteArray> PresetsModel::roleNames() const
120 {
121     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
122 
123     roles[PluginIdRole] = "pluginId";
124     roles[ConfigRole] = "config";
125     roles[WritableRole] = "writable";
126     return roles;
127 }
128 
129 QVector<QPair<QRegularExpression, QString>> KSysGuard::SensorFaceControllerPrivate::sensorIdReplacements;
130 QRegularExpression SensorFaceControllerPrivate::oldDiskSensor = QRegularExpression(QStringLiteral("^disk\\/(.+)_\\(\\d+:\\d+\\)"));
131 QRegularExpression SensorFaceControllerPrivate::oldPartitionSensor = QRegularExpression(QStringLiteral("^partitions(\\/.+)\\/"));
132 
SensorFaceControllerPrivate()133 SensorFaceControllerPrivate::SensorFaceControllerPrivate()
134 {
135     if (SensorFaceControllerPrivate::sensorIdReplacements.isEmpty()) {
136         // A list of conversion rules to convert old sensor ids to new ones.
137         // When loading, each regular expression tries to match to the sensor
138         // id. If it matches, it will be be used to replace the sensor id with
139         // the second argument.
140         sensorIdReplacements = {
141             {QRegularExpression(QStringLiteral("network/interfaces/(.*)")), QStringLiteral("network/\\1")},
142             {QRegularExpression(QStringLiteral("network/all/receivedDataRate$")), QStringLiteral("network/all/download")},
143             {QRegularExpression(QStringLiteral("network/all/sentDataRate$")), QStringLiteral("network/all/upload")},
144             {QRegularExpression(QStringLiteral("network/all/totalReceivedData$")), QStringLiteral("network/all/totalDownload")},
145             {QRegularExpression(QStringLiteral("network/all/totalSentData$")), QStringLiteral("network/all/totalUpload")},
146             {QRegularExpression(QStringLiteral("(.*)/receiver/data$")), QStringLiteral("\\1/download")},
147             {QRegularExpression(QStringLiteral("(.*)/transmitter/data$")), QStringLiteral("\\1/upload")},
148             {QRegularExpression(QStringLiteral("(.*)/receiver/dataTotal$")), QStringLiteral("\\1/totalDownload")},
149             {QRegularExpression(QStringLiteral("(.*)/transmitter/dataTotal$")), QStringLiteral("\\1/totalUpload")},
150             {QRegularExpression(QStringLiteral("(.*)/Rate/rio")), QStringLiteral("\\1/read")},
151             {QRegularExpression(QStringLiteral("(.*)/Rate/wio$")), QStringLiteral("\\1/write")},
152             {QRegularExpression(QStringLiteral("(.*)/freespace$")), QStringLiteral("\\1/free")},
153             {QRegularExpression(QStringLiteral("(.*)/filllevel$")), QStringLiteral("\\1/usedPercent")},
154             {QRegularExpression(QStringLiteral("(.*)/usedspace$")), QStringLiteral("\\1/used")},
155             {QRegularExpression(QStringLiteral("cpu/system/(.*)$")), QStringLiteral("cpu/all/\\1")},
156             {QRegularExpression(QStringLiteral("cpu/(.*)/sys$")), QStringLiteral("cpu/\\1/system")},
157             {QRegularExpression(QStringLiteral("cpu/(.*)/TotalLoad$")), QStringLiteral("cpu/\\1/usage")},
158             {QRegularExpression(QStringLiteral("cpu/cpu(\\d+)/clock$")), QStringLiteral("cpu/cpu\\1/frequency")},
159             {QRegularExpression(QStringLiteral("mem/(.*)level")), QStringLiteral("mem/\\1Percent")},
160             {QRegularExpression(QStringLiteral("mem/physical/allocated")), QStringLiteral("memory/physical/used")},
161             {QRegularExpression(QStringLiteral("mem/physical/available")), QStringLiteral("memory/physical/free")},
162             {QRegularExpression(QStringLiteral("mem/physical/buf")), QStringLiteral("memory/physical/buffer")},
163             {QRegularExpression(QStringLiteral("mem/physical/cached")), QStringLiteral("memory/physical/cache")},
164             {QRegularExpression(QStringLiteral("^mem/(.*)")), QStringLiteral("memory/\\1")},
165             {QRegularExpression(QStringLiteral("nvidia/(.*)/temperature$")), QStringLiteral("gpu/\\1/temperature")},
166             {QRegularExpression(QStringLiteral("nvidia/(.*)/memoryClock$")), QStringLiteral("gpu/\\1/memoryFrequency")},
167             {QRegularExpression(QStringLiteral("nvidia/(.*)/processorClock$")), QStringLiteral("gpu/\\1/coreFrequency")},
168             {QRegularExpression(QStringLiteral("nvidia/(.*)/(memory|sharedMemory)$")), QStringLiteral("gpu/\\1/usedVram")},
169             {QRegularExpression(QStringLiteral("nvidia/(.*)/(encoderUsage|decoderUsage)$")), QStringLiteral("gpu/\\1/usage")},
170             {QRegularExpression(QStringLiteral("^(uptime|system/uptime/uptime)$")), QStringLiteral("os/system/uptime")},
171         };
172     }
173 }
174 
replaceDiskId(const QString & entryName) const175 QString SensorFaceControllerPrivate::replaceDiskId(const QString &entryName) const
176 {
177     const auto match = oldDiskSensor.match(entryName);
178     if (!match.hasMatch()) {
179         return entryName;
180     }
181     const QString device = match.captured(1);
182     Solid::Predicate predicate(Solid::DeviceInterface::StorageAccess);
183     predicate &= Solid::Predicate(Solid::DeviceInterface::Block, QStringLiteral("device"), QStringLiteral("/dev/%1").arg(device));
184     const auto devices = Solid::Device::listFromQuery(predicate);
185     if (devices.empty()) {
186         return QString();
187     }
188     QString sensorId = entryName;
189     const auto volume = devices[0].as<Solid::StorageVolume>();
190     const QString id = volume->uuid().isEmpty() ? volume->label() : volume->uuid();
191     return sensorId.replace(match.captured(0), QStringLiteral("disk/") + id);
192 }
193 
replacePartitionId(const QString & entryName) const194 QString SensorFaceControllerPrivate::replacePartitionId(const QString &entryName) const
195 {
196     const auto match = oldPartitionSensor.match(entryName);
197     if (!match.hasMatch()) {
198         return entryName;
199     }
200     QString sensorId = entryName;
201 
202     if (match.captured(1) == QLatin1String("/all")) {
203         return sensorId.replace(match.captured(0), QStringLiteral("disk/all/"));
204     }
205 
206     const QString filePath = match.captured(1) == QLatin1String("/__root__") ? QStringLiteral("/") : match.captured(1);
207     const Solid::Predicate predicate(Solid::DeviceInterface::StorageAccess, QStringLiteral("filePath"), filePath);
208     const auto devices = Solid::Device::listFromQuery(predicate);
209     if (devices.empty()) {
210         return entryName;
211     }
212     const auto volume = devices[0].as<Solid::StorageVolume>();
213     const QString id = volume->uuid().isEmpty() ? volume->label() : volume->uuid();
214     return sensorId.replace(match.captured(0), QStringLiteral("disk/%1/").arg(id));
215 }
216 
readSensors(const KConfigGroup & read,const QString & entryName)217 QJsonArray SensorFaceControllerPrivate::readSensors(const KConfigGroup &read, const QString &entryName)
218 {
219     auto original = QJsonDocument::fromJson(read.readEntry(entryName, QString()).toUtf8()).array();
220     QJsonArray newSensors;
221     for (auto entry : original) {
222         QString sensorId = entry.toString();
223         for (auto replacement : qAsConst(sensorIdReplacements)) {
224             auto match = replacement.first.match(sensorId);
225             if (match.hasMatch()) {
226                 sensorId.replace(replacement.first, replacement.second);
227             }
228         }
229         sensorId = replaceDiskId(sensorId);
230         sensorId = replacePartitionId(sensorId);
231         newSensors.append(sensorId);
232     }
233 
234     return newSensors;
235 }
236 
readAndUpdateSensors(KConfigGroup & config,const QString & entryName)237 QJsonArray SensorFaceControllerPrivate::readAndUpdateSensors(KConfigGroup &config, const QString &entryName)
238 {
239     auto original = QJsonDocument::fromJson(config.readEntry(entryName, QString()).toUtf8()).array();
240 
241     const KConfigGroup &group = config;
242     auto newSensors = readSensors(group, entryName);
243 
244     if (newSensors != original) {
245         config.writeEntry(entryName, QJsonDocument(newSensors).toJson(QJsonDocument::Compact));
246     }
247 
248     return newSensors;
249 }
250 
resolveSensors(const QJsonArray & partialEntries,std::function<void (const QJsonArray &)> callback)251 void SensorFaceControllerPrivate::resolveSensors(const QJsonArray &partialEntries, std::function<void(const QJsonArray &)> callback)
252 {
253     if (partialEntries.isEmpty()) {
254         callback(partialEntries);
255         return;
256     }
257 
258     auto sensors = std::make_shared<QJsonArray>();
259 
260     for (const auto &id : partialEntries) {
261         auto query = new KSysGuard::SensorQuery{id.toString()};
262         query->connect(query, &KSysGuard::SensorQuery::finished, q, [this, query, sensors, callback] {
263             query->sortByName();
264             const auto ids = query->sensorIds();
265             delete query;
266             std::transform(ids.begin(), ids.end(), std::back_inserter(*sensors), [](const QString &id) {
267                 return QJsonValue(id);
268             });
269             if (sensors.use_count() == 1) {
270                 // We are the last query
271                 callback(*sensors);
272             }
273         });
274         query->execute();
275     }
276 }
277 
createGui(const QString & qmlPath)278 SensorFace *SensorFaceControllerPrivate::createGui(const QString &qmlPath)
279 {
280     QQmlComponent *component = new QQmlComponent(engine, qmlPath, nullptr);
281     // TODO: eventually support async  components? (only useful for qml files from http, we probably don't want that)
282     if (component->status() != QQmlComponent::Ready) {
283         qCritical() << "Error creating component:";
284         for (auto err : component->errors()) {
285             qWarning() << err.toString();
286         }
287         component->deleteLater();
288         return nullptr;
289     }
290 
291     QQmlContext *context = new QQmlContext(engine);
292     context->setContextObject(contextObj);
293     QObject *guiObject = component->beginCreate(context);
294     SensorFace *gui = qobject_cast<SensorFace *>(guiObject);
295     if (!gui) {
296         qWarning() << "ERROR: QML gui" << guiObject << "not a SensorFace instance";
297         guiObject->deleteLater();
298         context->deleteLater();
299         return nullptr;
300     }
301     context->setParent(gui);
302 
303     gui->setController(q);
304     gui->setParent(q);
305 
306     component->completeCreate();
307 
308     component->deleteLater();
309     return gui;
310 }
311 
createConfigUi(const QString & file,const QVariantMap & initialProperties)312 QQuickItem *SensorFaceControllerPrivate::createConfigUi(const QString &file, const QVariantMap &initialProperties)
313 {
314     QQmlComponent *component = new QQmlComponent(engine, file, nullptr);
315     // TODO: eventually support async  components? (only useful for qml files from http, we probably don't want that)
316     if (component->status() != QQmlComponent::Ready) {
317         qCritical() << "Error creating component:";
318         for (auto err : component->errors()) {
319             qWarning() << err.toString();
320         }
321         component->deleteLater();
322         return nullptr;
323     }
324 
325     QQmlContext *context = new QQmlContext(engine);
326     context->setContextObject(contextObj);
327     QObject *guiObject = component->createWithInitialProperties(initialProperties, context);
328     QQuickItem *gui = qobject_cast<QQuickItem *>(guiObject);
329     Q_ASSERT(gui);
330     context->setParent(gui);
331     gui->setParent(q);
332 
333     component->deleteLater();
334 
335     return gui;
336 }
337 
SensorFaceController(KConfigGroup & config,QQmlEngine * engine)338 SensorFaceController::SensorFaceController(KConfigGroup &config, QQmlEngine *engine)
339     : QObject(engine)
340     , d(std::make_unique<SensorFaceControllerPrivate>())
341 {
342     d->q = this;
343     d->configGroup = config;
344     d->appearanceGroup = KConfigGroup(&config, "Appearance");
345     d->sensorsGroup = KConfigGroup(&config, "Sensors");
346     d->colorsGroup = KConfigGroup(&config, "SensorColors");
347     d->labelsGroup = KConfigGroup(&config, "SensorLabels");
348     d->engine = engine;
349     d->syncTimer = new QTimer(this);
350     d->syncTimer->setSingleShot(true);
351     d->syncTimer->setInterval(5000);
352     connect(d->syncTimer, &QTimer::timeout, this, [this]() {
353         if (!d->shouldSync) {
354             return;
355         }
356         d->appearanceGroup.sync();
357         d->sensorsGroup.sync();
358     });
359 
360     d->contextObj = new KLocalizedContext(this);
361 
362     d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("totalSensors")), [this](const QJsonArray &resolvedSensors) {
363         d->totalSensors = resolvedSensors;
364         Q_EMIT totalSensorsChanged();
365     });
366     d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("lowPrioritySensorIds")), [this](const QJsonArray &resolvedSensors) {
367         d->lowPrioritySensorIds = resolvedSensors;
368         Q_EMIT lowPrioritySensorIdsChanged();
369     });
370     d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("highPrioritySensorIds")), [this](const QJsonArray &resolvedSensors) {
371         d->highPrioritySensorIds = resolvedSensors;
372         Q_EMIT highPrioritySensorIdsChanged();
373     });
374 
375     setFaceId(d->appearanceGroup.readEntry("chartFace", QStringLiteral("org.kde.ksysguard.piechart")));
376 }
377 
~SensorFaceController()378 SensorFaceController::~SensorFaceController()
379 {
380     auto forceSave = d->faceProperties.readEntry(QStringLiteral("ForceSaveOnDestroy"), false);
381     if (!forceSave) {
382         if (!d->shouldSync) {
383             // If we should not sync automatically, clear all changes before we
384             // destroy the config objects, otherwise they will be written during
385             // destruction.
386             d->appearanceGroup.markAsClean();
387             d->colorsGroup.markAsClean();
388             d->labelsGroup.markAsClean();
389             if (d->faceConfigLoader && d->faceConfigLoader->isSaveNeeded()) {
390                 d->faceConfigLoader->load();
391             }
392         }
393     } else {
394         d->faceConfigLoader->save();
395     }
396 }
397 
configGroup() const398 KConfigGroup KSysGuard::SensorFaceController::configGroup() const
399 {
400     return d->configGroup;
401 }
402 
title() const403 QString SensorFaceController::title() const
404 {
405     // both Title and title can exist to allow i18n of Title
406     if (d->appearanceGroup.hasKey("title")) {
407         return d->appearanceGroup.readEntry("title");
408     } else {
409         // if neither exist fall back to name
410         return d->appearanceGroup.readEntry("Title", i18n("System Monitor Sensor"));
411     }
412 }
413 
setTitle(const QString & title)414 void SensorFaceController::setTitle(const QString &title)
415 {
416     if (title == SensorFaceController::title()) {
417         return;
418     }
419 
420     d->appearanceGroup.writeEntry("title", title);
421     d->syncTimer->start();
422 
423     emit titleChanged();
424 }
425 
showTitle() const426 bool SensorFaceController::showTitle() const
427 {
428     return d->appearanceGroup.readEntry("showTitle", true);
429 }
430 
setShowTitle(bool show)431 void SensorFaceController::setShowTitle(bool show)
432 {
433     if (show == showTitle()) {
434         return;
435     }
436 
437     d->appearanceGroup.writeEntry("showTitle", show);
438     d->syncTimer->start();
439 
440     emit showTitleChanged();
441 }
442 
totalSensors() const443 QJsonArray SensorFaceController::totalSensors() const
444 {
445     return d->totalSensors;
446 }
447 
setTotalSensors(const QJsonArray & totalSensors)448 void SensorFaceController::setTotalSensors(const QJsonArray &totalSensors)
449 {
450     if (totalSensors == d->totalSensors) {
451         return;
452     }
453     const auto currentEntry = QJsonDocument::fromJson(d->sensorsGroup.readEntry("totalSensors").toUtf8()).array();
454     if (totalSensors == currentEntry) {
455         return;
456     }
457     d->sensorsGroup.writeEntry("totalSensors", QJsonDocument(totalSensors).toJson(QJsonDocument::Compact));
458     // Until we have resolved
459     d->totalSensors = totalSensors;
460     d->syncTimer->start();
461     Q_EMIT totalSensorsChanged();
462     d->resolveSensors(totalSensors, [this](const QJsonArray &resolvedSensors) {
463         if (resolvedSensors == d->totalSensors) {
464             return;
465         }
466         d->totalSensors = resolvedSensors;
467         Q_EMIT totalSensorsChanged();
468     });
469 }
470 
highPrioritySensorIds() const471 QJsonArray SensorFaceController::highPrioritySensorIds() const
472 {
473     return d->highPrioritySensorIds;
474 }
475 
setHighPrioritySensorIds(const QJsonArray & highPrioritySensorIds)476 void SensorFaceController::setHighPrioritySensorIds(const QJsonArray &highPrioritySensorIds)
477 {
478     if (highPrioritySensorIds == d->highPrioritySensorIds) {
479         return;
480     }
481     const auto currentEntry = QJsonDocument::fromJson(d->sensorsGroup.readEntry("highPrioritySensorIds").toUtf8()).array();
482     if (highPrioritySensorIds == currentEntry) {
483         return;
484     }
485     d->sensorsGroup.writeEntry("highPrioritySensorIds", QJsonDocument(highPrioritySensorIds).toJson(QJsonDocument::Compact));
486     // Until we have resolved
487     d->syncTimer->start();
488     d->highPrioritySensorIds = highPrioritySensorIds;
489     Q_EMIT highPrioritySensorIdsChanged();
490     d->resolveSensors(highPrioritySensorIds, [this](const QJsonArray &resolvedSensors) {
491         if (resolvedSensors == d->highPrioritySensorIds) {
492             return;
493         }
494         d->highPrioritySensorIds = resolvedSensors;
495         Q_EMIT highPrioritySensorIdsChanged();
496     });
497 }
498 
sensorColors() const499 QVariantMap SensorFaceController::sensorColors() const
500 {
501     QVariantMap colors;
502     for (const auto &key : d->colorsGroup.keyList()) {
503         QColor color = d->colorsGroup.readEntry(key, QColor());
504 
505         if (color.isValid()) {
506             colors[key] = color;
507         }
508     }
509     return colors;
510 }
511 
setSensorColors(const QVariantMap & colors)512 void SensorFaceController::setSensorColors(const QVariantMap &colors)
513 {
514     if (colors == this->sensorColors()) {
515         return;
516     }
517 
518     d->colorsGroup.deleteGroup();
519     d->colorsGroup = KConfigGroup(&d->configGroup, "SensorColors");
520 
521     auto it = colors.constBegin();
522     for (; it != colors.constEnd(); ++it) {
523         d->colorsGroup.writeEntry(it.key(), it.value());
524     }
525 
526     d->syncTimer->start();
527     emit sensorColorsChanged();
528 }
529 
sensorLabels() const530 QVariantMap SensorFaceController::sensorLabels() const
531 {
532     QVariantMap labels;
533     for (const auto &key : d->labelsGroup.keyList()) {
534         labels[key] = d->labelsGroup.readEntry(key);
535     }
536     return labels;
537 }
538 
setSensorLabels(const QVariantMap & labels)539 void SensorFaceController::setSensorLabels(const QVariantMap &labels)
540 {
541     if (labels == this->sensorLabels()) {
542         return;
543     }
544 
545     d->labelsGroup.deleteGroup();
546     d->labelsGroup = KConfigGroup(&d->configGroup, "SensorLabels");
547 
548     for (auto it = labels.cbegin(); it != labels.cend(); ++it) {
549         const auto label = it.value().toString();
550         if (!label.isEmpty()) {
551             d->labelsGroup.writeEntry(it.key(), label);
552         }
553     }
554 
555     d->syncTimer->start();
556     emit sensorLabelsChanged();
557 }
558 
lowPrioritySensorIds() const559 QJsonArray SensorFaceController::lowPrioritySensorIds() const
560 {
561     return d->lowPrioritySensorIds;
562 }
563 
setLowPrioritySensorIds(const QJsonArray & lowPrioritySensorIds)564 void SensorFaceController::setLowPrioritySensorIds(const QJsonArray &lowPrioritySensorIds)
565 {
566     if (lowPrioritySensorIds == d->lowPrioritySensorIds) {
567         return;
568     }
569     const auto currentEntry = QJsonDocument::fromJson(d->sensorsGroup.readEntry("lowPrioritySensorIds").toUtf8()).array();
570     if (lowPrioritySensorIds == currentEntry) {
571         return;
572     }
573     d->sensorsGroup.writeEntry("lowPrioritySensorIds", QJsonDocument(lowPrioritySensorIds).toJson(QJsonDocument::Compact));
574     // Until we have resolved
575     d->lowPrioritySensorIds = lowPrioritySensorIds;
576     d->syncTimer->start();
577     Q_EMIT lowPrioritySensorIdsChanged();
578     d->resolveSensors(lowPrioritySensorIds, [this](const QJsonArray &resolvedSensors) {
579         if (resolvedSensors == d->lowPrioritySensorIds) {
580             return;
581         }
582         d->lowPrioritySensorIds = resolvedSensors;
583         Q_EMIT lowPrioritySensorIdsChanged();
584     });
585 }
586 
updateRateLimit() const587 int SensorFaceController::updateRateLimit() const
588 {
589     return d->appearanceGroup.readEntry<int>(QStringLiteral("updateRateLimit"), 0);
590 }
591 
setUpdateRateLimit(int limit)592 void SensorFaceController::setUpdateRateLimit(int limit)
593 {
594     if (limit == updateRateLimit()) {
595         return;
596     }
597 
598     d->appearanceGroup.writeEntry("updateRateLimit", limit);
599     d->syncTimer->start();
600 
601     Q_EMIT updateRateLimitChanged();
602 }
603 
604 // from face config, immutable by the user
name() const605 QString SensorFaceController::name() const
606 {
607     return d->facePackage.metadata().name();
608 }
609 
icon() const610 const QString SensorFaceController::icon() const
611 {
612     return d->facePackage.metadata().iconName();
613 }
614 
supportsSensorsColors() const615 bool SensorFaceController::supportsSensorsColors() const
616 {
617     return d->faceProperties.readEntry("SupportsSensorsColors", false);
618 }
619 
supportsTotalSensors() const620 bool SensorFaceController::supportsTotalSensors() const
621 {
622     return d->faceProperties.readEntry("SupportsTotalSensors", false);
623 }
624 
supportsLowPrioritySensors() const625 bool SensorFaceController::supportsLowPrioritySensors() const
626 {
627     return d->faceProperties.readEntry("SupportsLowPrioritySensors", false);
628 }
629 
maxTotalSensors() const630 int SensorFaceController::maxTotalSensors() const
631 {
632     return d->faceProperties.readEntry("MaxTotalSensors", 1);
633 }
634 
setFaceId(const QString & face)635 void SensorFaceController::setFaceId(const QString &face)
636 {
637     if (d->faceId == face) {
638         return;
639     }
640 
641     if (d->fullRepresentation) {
642         d->fullRepresentation->deleteLater();
643         d->fullRepresentation.clear();
644     }
645     if (d->compactRepresentation) {
646         d->compactRepresentation->deleteLater();
647         d->compactRepresentation.clear();
648     }
649     if (d->faceConfigUi) {
650         d->faceConfigUi->deleteLater();
651         d->faceConfigUi.clear();
652     }
653 
654     d->faceId = face;
655 
656     d->facePackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("KSysguard/SensorFace"), face);
657 
658     if (d->faceConfiguration) {
659         d->faceConfiguration->deleteLater();
660         d->faceConfiguration = nullptr;
661     }
662     if (d->faceConfigLoader) {
663         d->faceConfigLoader->deleteLater();
664         d->faceConfigLoader = nullptr;
665     }
666 
667     if (!d->facePackage.isValid()) {
668         emit faceIdChanged();
669         return;
670     }
671 
672     d->contextObj->setTranslationDomain(QLatin1String("ksysguard_face_") + face);
673 
674     d->faceProperties = KConfigGroup(KSharedConfig::openConfig(d->facePackage.filePath("FaceProperties"), KConfig::SimpleConfig), QStringLiteral("Config"));
675 
676     reloadFaceConfiguration();
677 
678     d->appearanceGroup.writeEntry("chartFace", face);
679     d->syncTimer->start();
680     emit faceIdChanged();
681     return;
682 }
683 
faceId() const684 QString SensorFaceController::faceId() const
685 {
686     return d->faceId;
687 }
688 
faceConfiguration() const689 KDeclarative::ConfigPropertyMap *SensorFaceController::faceConfiguration() const
690 {
691     return d->faceConfiguration;
692 }
693 
compactRepresentation()694 QQuickItem *SensorFaceController::compactRepresentation()
695 {
696     if (!d->facePackage.isValid()) {
697         return nullptr;
698     } else if (d->compactRepresentation) {
699         return d->compactRepresentation;
700     }
701 
702     d->compactRepresentation = d->createGui(d->facePackage.filePath("ui", QStringLiteral("CompactRepresentation.qml")));
703     return d->compactRepresentation;
704 }
705 
fullRepresentation()706 QQuickItem *SensorFaceController::fullRepresentation()
707 {
708     if (!d->facePackage.isValid()) {
709         return nullptr;
710     } else if (d->fullRepresentation) {
711         return d->fullRepresentation;
712     }
713 
714     d->fullRepresentation = d->createGui(d->facePackage.filePath("ui", QStringLiteral("FullRepresentation.qml")));
715     return d->fullRepresentation;
716 }
717 
faceConfigUi()718 QQuickItem *SensorFaceController::faceConfigUi()
719 {
720     if (!d->facePackage.isValid()) {
721         return nullptr;
722     } else if (d->faceConfigUi) {
723         return d->faceConfigUi;
724     }
725 
726     const QString filePath = d->facePackage.filePath("ui", QStringLiteral("Config.qml"));
727 
728     if (filePath.isEmpty()) {
729         return nullptr;
730     }
731 
732     d->faceConfigUi = d->createConfigUi(QStringLiteral(":/FaceDetailsConfig.qml"),
733                                         {{QStringLiteral("controller"), QVariant::fromValue(this)}, {QStringLiteral("source"), filePath}});
734 
735     if (d->faceConfigUi && !d->faceConfigUi->property("item").value<QQuickItem *>()) {
736         d->faceConfigUi->deleteLater();
737         d->faceConfigUi.clear();
738     }
739     return d->faceConfigUi;
740 }
741 
appearanceConfigUi()742 QQuickItem *SensorFaceController::appearanceConfigUi()
743 {
744     if (d->appearanceConfigUi) {
745         return d->appearanceConfigUi;
746     }
747 
748     d->appearanceConfigUi = d->createConfigUi(QStringLiteral(":/ConfigAppearance.qml"), {{QStringLiteral("controller"), QVariant::fromValue(this)}});
749 
750     return d->appearanceConfigUi;
751 }
752 
sensorsConfigUi()753 QQuickItem *SensorFaceController::sensorsConfigUi()
754 {
755     if (d->sensorsConfigUi) {
756         return d->sensorsConfigUi;
757     }
758 
759     if (d->faceProperties.readEntry("SupportsSensors", true)) {
760         d->sensorsConfigUi = d->createConfigUi(QStringLiteral(":/ConfigSensors.qml"), {{QStringLiteral("controller"), QVariant::fromValue(this)}});
761     } else {
762         d->sensorsConfigUi = new QQuickItem;
763     }
764     return d->sensorsConfigUi;
765 }
766 
availableFacesModel()767 QAbstractItemModel *SensorFaceController::availableFacesModel()
768 {
769     if (d->availableFacesModel) {
770         return d->availableFacesModel;
771     }
772 
773     d->availableFacesModel = new FacesModel(this);
774     return d->availableFacesModel;
775 }
776 
availablePresetsModel()777 QAbstractItemModel *SensorFaceController::availablePresetsModel()
778 {
779     if (d->availablePresetsModel) {
780         return d->availablePresetsModel;
781     }
782 
783     d->availablePresetsModel = new PresetsModel(this);
784 
785     return d->availablePresetsModel;
786 }
787 
reloadConfig()788 void SensorFaceController::reloadConfig()
789 {
790     if (d->faceConfigLoader) {
791         d->faceConfigLoader->load();
792     }
793 
794     d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("totalSensors")), [this](const QJsonArray &resolvedSensors) {
795         d->totalSensors = resolvedSensors;
796         Q_EMIT totalSensorsChanged();
797     });
798     d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("lowPrioritySensorIds")), [this](const QJsonArray &resolvedSensors) {
799         d->lowPrioritySensorIds = resolvedSensors;
800         Q_EMIT lowPrioritySensorIdsChanged();
801     });
802     d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("highPrioritySensorIds")), [this](const QJsonArray &resolvedSensors) {
803         d->highPrioritySensorIds = resolvedSensors;
804         Q_EMIT highPrioritySensorIdsChanged();
805     });
806 
807     // Force to re-read all the values
808     setFaceId(d->appearanceGroup.readEntry("chartFace", QStringLiteral("org.kde.ksysguard.textonly")));
809     Q_EMIT titleChanged();
810     Q_EMIT sensorColorsChanged();
811     Q_EMIT sensorLabelsChanged();
812     Q_EMIT showTitleChanged();
813     Q_EMIT updateRateLimitChanged();
814 }
815 
loadPreset(const QString & preset)816 void SensorFaceController::loadPreset(const QString &preset)
817 {
818     if (preset.isEmpty()) {
819         return;
820     }
821 
822     auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
823 
824     presetPackage.setPath(preset);
825 
826     if (!presetPackage.isValid()) {
827         return;
828     }
829 
830     if (presetPackage.metadata().value(QStringLiteral("X-Plasma-RootPath")) != QStringLiteral("org.kde.plasma.systemmonitor")) {
831         return;
832     }
833 
834     KDesktopFile df(presetPackage.path() + QStringLiteral("metadata.desktop"));
835 
836     auto c = KSharedConfig::openConfig(presetPackage.filePath("config", QStringLiteral("faceproperties")), KConfig::SimpleConfig);
837     const KConfigGroup presetGroup(c, QStringLiteral("Config"));
838     const KConfigGroup colorsGroup(c, QStringLiteral("SensorColors"));
839 
840     // Load the title
841     setTitle(df.readName());
842 
843     // Remove the "custon" value from presets models
844     if (d->availablePresetsModel && d->availablePresetsModel->data(d->availablePresetsModel->index(0, 0), PresetsModel::PluginIdRole).toString().isEmpty()) {
845         d->availablePresetsModel->removeRow(0);
846     }
847 
848     setTotalSensors(d->readSensors(presetGroup, QStringLiteral("totalSensors")));
849     setHighPrioritySensorIds(d->readSensors(presetGroup, QStringLiteral("highPrioritySensorIds")));
850     setLowPrioritySensorIds(d->readSensors(presetGroup, QStringLiteral("lowPrioritySensorIds")));
851 
852     setFaceId(presetGroup.readEntry(QStringLiteral("chartFace"), QStringLiteral("org.kde.ksysguard.piechart")));
853 
854     colorsGroup.copyTo(&d->colorsGroup);
855     emit sensorColorsChanged();
856 
857     if (d->faceConfigLoader) {
858         KConfigGroup presetGroup(KSharedConfig::openConfig(presetPackage.filePath("FaceProperties"), KConfig::SimpleConfig), QStringLiteral("FaceConfig"));
859 
860         for (const QString &key : presetGroup.keyList()) {
861             KConfigSkeletonItem *item = d->faceConfigLoader->findItemByName(key);
862             if (item) {
863                 if (item->property().type() == QVariant::StringList) {
864                     item->setProperty(presetGroup.readEntry(key, QStringList()));
865                 } else {
866                     item->setProperty(presetGroup.readEntry(key));
867                 }
868                 d->faceConfigLoader->save();
869                 d->faceConfigLoader->read();
870             }
871         }
872     }
873 }
874 
savePreset()875 void SensorFaceController::savePreset()
876 {
877     QString pluginName = QStringLiteral("org.kde.plasma.systemmonitor.") + title().simplified().replace(QLatin1Char(' '), QStringLiteral("")).toLower();
878     int suffix = 0;
879 
880     auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
881 
882     presetPackage.setPath(pluginName);
883     if (presetPackage.isValid()) {
884         do {
885             presetPackage.setPath(QString());
886             presetPackage.setPath(pluginName + QString::number(++suffix));
887         } while (presetPackage.isValid());
888 
889         pluginName += QString::number(suffix);
890     }
891 
892     QTemporaryDir dir;
893     if (!dir.isValid()) {
894         return;
895     }
896 
897     KConfig c(dir.path() % QStringLiteral("/metadata.desktop"));
898 
899     KConfigGroup cg(&c, "Desktop Entry");
900     cg.writeEntry("Name", title());
901     cg.writeEntry("Icon", "ksysguardd");
902     cg.writeEntry("X-Plasma-API", "declarativeappletscript");
903     cg.writeEntry("X-Plasma-MainScript", "ui/main.qml");
904     cg.writeEntry("X-Plasma-Provides", "org.kde.plasma.systemmonitor");
905     cg.writeEntry("X-Plasma-RootPath", "org.kde.plasma.systemmonitor");
906     cg.writeEntry("X-KDE-PluginInfo-Name", pluginName);
907     cg.writeEntry("X-KDE-ServiceTypes", "Plasma/Applet");
908     cg.writeEntry("X-KDE-PluginInfo-Category", "System Information");
909     cg.writeEntry("X-KDE-PluginInfo-License", "LGPL 2.1+");
910     cg.writeEntry("X-KDE-PluginInfo-EnabledByDefault", "true");
911     cg.writeEntry("X-KDE-PluginInfo-Version", "0.1");
912     cg.sync();
913 
914     QDir subDir(dir.path());
915     subDir.mkdir(QStringLiteral("contents"));
916     KConfig faceConfig(subDir.path() % QStringLiteral("/contents/faceproperties"));
917 
918     KConfigGroup configGroup(&faceConfig, "Config");
919     configGroup.writeEntry(QStringLiteral("totalSensors"), QJsonDocument(totalSensors()).toJson(QJsonDocument::Compact));
920     configGroup.writeEntry(QStringLiteral("highPrioritySensorIds"), QJsonDocument(highPrioritySensorIds()).toJson(QJsonDocument::Compact));
921     configGroup.writeEntry(QStringLiteral("lowPrioritySensorIds"), QJsonDocument(lowPrioritySensorIds()).toJson(QJsonDocument::Compact));
922 
923     KConfigGroup colorsGroup(&faceConfig, "SensorColors");
924     d->colorsGroup.copyTo(&colorsGroup);
925     colorsGroup.sync();
926 
927     configGroup = KConfigGroup(&faceConfig, "FaceConfig");
928     if (d->faceConfigLoader) {
929         const auto &items = d->faceConfigLoader->items();
930         for (KConfigSkeletonItem *item : items) {
931             configGroup.writeEntry(item->key(), item->property());
932         }
933     }
934     configGroup.sync();
935 
936     auto *job = presetPackage.install(dir.path());
937 
938     connect(job, &KJob::finished, this, [this, pluginName]() {
939         d->availablePresetsModel->reload();
940     });
941 }
942 
uninstallPreset(const QString & pluginId)943 void SensorFaceController::uninstallPreset(const QString &pluginId)
944 {
945     auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), pluginId);
946 
947     if (presetPackage.metadata().value(QStringLiteral("X-Plasma-RootPath")) != QStringLiteral("org.kde.plasma.systemmonitor")) {
948         return;
949     }
950 
951     QDir root(presetPackage.path());
952     root.cdUp();
953     auto *job = presetPackage.uninstall(pluginId, root.path());
954 
955     connect(job, &KJob::finished, this, [this]() {
956         d->availablePresetsModel->reload();
957     });
958 }
959 
shouldSync() const960 bool SensorFaceController::shouldSync() const
961 {
962     return d->shouldSync;
963 }
964 
setShouldSync(bool sync)965 void SensorFaceController::setShouldSync(bool sync)
966 {
967     d->shouldSync = sync;
968     if (d->faceConfiguration) {
969         d->faceConfiguration->setAutosave(sync);
970     }
971     if (!d->shouldSync && d->syncTimer->isActive()) {
972         d->syncTimer->stop();
973     }
974 }
975 
reloadFaceConfiguration()976 void SensorFaceController::reloadFaceConfiguration()
977 {
978     const QString xmlPath = d->facePackage.filePath("mainconfigxml");
979 
980     if (!xmlPath.isEmpty()) {
981         QFile file(xmlPath);
982         KConfigGroup cg(&d->configGroup, d->faceId);
983 
984         if (d->faceConfigLoader) {
985             delete d->faceConfigLoader;
986         }
987 
988         if (d->faceConfiguration) {
989             delete d->faceConfiguration;
990         }
991 
992         d->faceConfigLoader = new KConfigLoader(cg, &file, this);
993         d->faceConfiguration = new KDeclarative::ConfigPropertyMap(d->faceConfigLoader, this);
994         d->faceConfiguration->setAutosave(d->shouldSync);
995         connect(d->faceConfiguration, &KDeclarative::ConfigPropertyMap::valueChanged, this, [this](const QString &key) {
996             auto item = d->faceConfigLoader->findItemByName(key);
997             if (item) {
998                 item->writeConfig(d->faceConfigLoader->config());
999             }
1000         });
1001 
1002         Q_EMIT faceConfigurationChanged();
1003     }
1004 }
1005 
1006 #include "moc_SensorFaceController.cpp"
1007