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