Libksysguard

SensorFaceController.cpp
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 <QFileInfo>
14#include <QJsonDocument>
15#include <QQmlContext>
16#include <QQmlEngine>
17#include <QTemporaryDir>
18#include <QTimer>
19
20#include <KConfigLoader>
21#include <KConfigPropertyMap>
22#include <KDesktopFile>
23#include <KLocalizedString>
24#include <KPackage/PackageJob>
25#include <KPackage/PackageLoader>
26#include <KPluginMetaData>
27#include <Solid/Block>
28#include <Solid/Device>
29#include <Solid/Predicate>
30#include <Solid/StorageAccess>
31#include <Solid/StorageVolume>
32
33using namespace KSysGuard;
34
35FacesModel::FacesModel(QObject *parent)
36 : QStandardItemModel(parent)
37{
38 reload();
39}
40
41void FacesModel::reload()
42{
43 clear();
44
45 const auto list = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KSysguard/SensorFace"));
46 for (auto plugin : list) {
47 QStandardItem *item = new QStandardItem(plugin.name());
48 item->setData(plugin.pluginId(), FacesModel::PluginIdRole);
50 }
51}
52
53QString FacesModel::pluginId(int row)
54{
55 return data(index(row, 0), PluginIdRole).toString();
56}
57
58QHash<int, QByteArray> FacesModel::roleNames() const
59{
61
62 roles[PluginIdRole] = "pluginId";
63 return roles;
64}
65
66PresetsModel::PresetsModel(QObject *parent)
67 : QStandardItemModel(parent)
68{
69 reload();
70}
71
72void PresetsModel::reload()
73{
74 clear();
76 KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/Applet"), QString(), [](const KPluginMetaData &plugin) {
77 return plugin.value(QStringLiteral("X-Plasma-RootPath")) == QStringLiteral("org.kde.plasma.systemmonitor");
78 });
79
80 QSet<QString> usedNames;
81
82 // We iterate backwards because packages under ~/.local are listed first, while we want them last
83 auto it = plugins.rbegin();
84 for (; it != plugins.rend(); ++it) {
85 const auto &plugin = *it;
86 KPackage::Package p = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), plugin.pluginId());
87 auto metadata = p.metadata();
88
89 QString baseName = metadata.name();
90 QString name = baseName;
91 int id = 0;
92
93 while (usedNames.contains(name)) {
94 name = baseName + QStringLiteral(" (") + QString::number(++id) + QStringLiteral(")");
95 }
96 usedNames << name;
97
98 QStandardItem *item = new QStandardItem(baseName);
99
100 // TODO config
101 QVariantMap config;
102
103 KConfigGroup configGroup(KSharedConfig::openConfig(p.filePath("config", QStringLiteral("faceproperties")), KConfig::SimpleConfig),
104 QStringLiteral("Config"));
105
106 const QStringList keys = configGroup.keyList();
107 for (const QString &key : keys) {
108 // all strings for now, type conversion happens in QML side when we have the config property map
109 config.insert(key, configGroup.readEntry(key));
110 }
111
112 item->setData(plugin.pluginId(), PresetsModel::PluginIdRole);
113 item->setData(config, PresetsModel::ConfigRole);
114
115 item->setData(QFileInfo(p.metadata().fileName()).isWritable(), PresetsModel::WritableRole);
116
118 }
119}
120
121QHash<int, QByteArray> PresetsModel::roleNames() const
122{
124
125 roles[PluginIdRole] = "pluginId";
126 roles[ConfigRole] = "config";
127 roles[WritableRole] = "writable";
128 return roles;
129}
130
131SensorResolver::SensorResolver(SensorFaceController *_controller, const QJsonArray &_expected)
132 : controller(_controller)
133 , expected(_expected)
134{
135}
136
137void SensorResolver::execute()
138{
139 std::transform(expected.begin(), expected.end(), std::back_inserter(queries), [this](const QJsonValue &entry) {
140 auto query = new SensorQuery{entry.toString()};
141 query->connect(query, &KSysGuard::SensorQuery::finished, controller, [this](SensorQuery *query) {
142 query->sortByName();
143 query->deleteLater();
144
145 const auto ids = query->sensorIds();
146 if (ids.isEmpty()) {
147 missing.append(query->path());
148 } else {
149 std::transform(ids.begin(), ids.end(), std::back_inserter(found), [](const QString &id) {
150 return id;
151 });
152 }
153
154 queries.removeOne(query);
155 if (queries.isEmpty()) {
156 callback(this);
157 }
158 });
159 query->execute();
160 return query;
161 });
162}
163
164QList<QPair<QRegularExpression, QString>> KSysGuard::SensorFaceControllerPrivate::sensorIdReplacements;
165QRegularExpression SensorFaceControllerPrivate::oldDiskSensor = QRegularExpression(QStringLiteral("^disk\\/(.+)_\\(\\d+:\\d+\\)"));
166QRegularExpression SensorFaceControllerPrivate::oldPartitionSensor = QRegularExpression(QStringLiteral("^partitions(\\/.+)\\/"));
167
168SensorFaceControllerPrivate::SensorFaceControllerPrivate()
169{
170 if (SensorFaceControllerPrivate::sensorIdReplacements.isEmpty()) {
171 // A list of conversion rules to convert old sensor ids to new ones.
172 // When loading, each regular expression tries to match to the sensor
173 // id. If it matches, it will be be used to replace the sensor id with
174 // the second argument.
175 sensorIdReplacements = {
176 {QRegularExpression(QStringLiteral("network/interfaces/(.*)")), QStringLiteral("network/\\1")},
177 {QRegularExpression(QStringLiteral("network/all/receivedDataRate$")), QStringLiteral("network/all/download")},
178 {QRegularExpression(QStringLiteral("network/all/sentDataRate$")), QStringLiteral("network/all/upload")},
179 {QRegularExpression(QStringLiteral("network/all/totalReceivedData$")), QStringLiteral("network/all/totalDownload")},
180 {QRegularExpression(QStringLiteral("network/all/totalSentData$")), QStringLiteral("network/all/totalUpload")},
181 {QRegularExpression(QStringLiteral("(.*)/receiver/data$")), QStringLiteral("\\1/download")},
182 {QRegularExpression(QStringLiteral("(.*)/transmitter/data$")), QStringLiteral("\\1/upload")},
183 {QRegularExpression(QStringLiteral("(.*)/receiver/dataTotal$")), QStringLiteral("\\1/totalDownload")},
184 {QRegularExpression(QStringLiteral("(.*)/transmitter/dataTotal$")), QStringLiteral("\\1/totalUpload")},
185 {QRegularExpression(QStringLiteral("(.*)/Rate/rio")), QStringLiteral("\\1/read")},
186 {QRegularExpression(QStringLiteral("(.*)/Rate/wio$")), QStringLiteral("\\1/write")},
187 {QRegularExpression(QStringLiteral("(.*)/freespace$")), QStringLiteral("\\1/free")},
188 {QRegularExpression(QStringLiteral("(.*)/filllevel$")), QStringLiteral("\\1/usedPercent")},
189 {QRegularExpression(QStringLiteral("(.*)/usedspace$")), QStringLiteral("\\1/used")},
190 {QRegularExpression(QStringLiteral("cpu/system/(.*)$")), QStringLiteral("cpu/all/\\1")},
191 {QRegularExpression(QStringLiteral("cpu/(.*)/sys$")), QStringLiteral("cpu/\\1/system")},
192 {QRegularExpression(QStringLiteral("cpu/(.*)/TotalLoad$")), QStringLiteral("cpu/\\1/usage")},
193 {QRegularExpression(QStringLiteral("cpu/cpu(\\d+)/clock$")), QStringLiteral("cpu/cpu\\1/frequency")},
194 {QRegularExpression(QStringLiteral("mem/(.*)level")), QStringLiteral("mem/\\1Percent")},
195 {QRegularExpression(QStringLiteral("mem/physical/allocated")), QStringLiteral("memory/physical/used")},
196 {QRegularExpression(QStringLiteral("mem/physical/available")), QStringLiteral("memory/physical/free")},
197 {QRegularExpression(QStringLiteral("mem/physical/buf")), QStringLiteral("memory/physical/buffer")},
198 {QRegularExpression(QStringLiteral("mem/physical/cached")), QStringLiteral("memory/physical/cache")},
199 {QRegularExpression(QStringLiteral("^mem/(.*)")), QStringLiteral("memory/\\1")},
200 {QRegularExpression(QStringLiteral("nvidia/(.*)/temperature$")), QStringLiteral("gpu/\\1/temperature")},
201 {QRegularExpression(QStringLiteral("nvidia/(.*)/memoryClock$")), QStringLiteral("gpu/\\1/memoryFrequency")},
202 {QRegularExpression(QStringLiteral("nvidia/(.*)/processorClock$")), QStringLiteral("gpu/\\1/coreFrequency")},
203 {QRegularExpression(QStringLiteral("nvidia/(.*)/(memory|sharedMemory)$")), QStringLiteral("gpu/\\1/usedVram")},
204 {QRegularExpression(QStringLiteral("nvidia/(.*)/(encoderUsage|decoderUsage)$")), QStringLiteral("gpu/\\1/usage")},
205 {QRegularExpression(QStringLiteral("^(uptime|system/uptime/uptime)$")), QStringLiteral("os/system/uptime")},
206 };
207 }
208}
209
210QString SensorFaceControllerPrivate::replaceDiskId(const QString &entryName) const
211{
212 const auto match = oldDiskSensor.match(entryName);
213 if (!match.hasMatch()) {
214 return entryName;
215 }
216 const QString device = match.captured(1);
217 Solid::Predicate predicate(Solid::DeviceInterface::StorageAccess);
218 predicate &= Solid::Predicate(Solid::DeviceInterface::Block, QStringLiteral("device"), QStringLiteral("/dev/%1").arg(device));
219 const auto devices = Solid::Device::listFromQuery(predicate);
220 if (devices.empty()) {
221 return QString();
222 }
223 QString sensorId = entryName;
224 const auto volume = devices[0].as<Solid::StorageVolume>();
225 const QString id = volume->uuid().isEmpty() ? volume->label() : volume->uuid();
226 return sensorId.replace(match.captured(0), QStringLiteral("disk/") + id);
227}
228
229QString SensorFaceControllerPrivate::replacePartitionId(const QString &entryName) const
230{
231 const auto match = oldPartitionSensor.match(entryName);
232 if (!match.hasMatch()) {
233 return entryName;
234 }
235 QString sensorId = entryName;
236
237 if (match.captured(1) == QLatin1String("/all")) {
238 return sensorId.replace(match.captured(0), QStringLiteral("disk/all/"));
239 }
240
241 const QString filePath = match.captured(1) == QLatin1String("/__root__") ? QStringLiteral("/") : match.captured(1);
242 const Solid::Predicate predicate(Solid::DeviceInterface::StorageAccess, QStringLiteral("filePath"), filePath);
243 const auto devices = Solid::Device::listFromQuery(predicate);
244 if (devices.empty()) {
245 return entryName;
246 }
247 const auto volume = devices[0].as<Solid::StorageVolume>();
248 const QString id = volume->uuid().isEmpty() ? volume->label() : volume->uuid();
249 return sensorId.replace(match.captured(0), QStringLiteral("disk/%1/").arg(id));
250}
251
252QJsonArray SensorFaceControllerPrivate::readSensors(const KConfigGroup &read, const QString &entryName)
253{
254 auto original = QJsonDocument::fromJson(read.readEntry(entryName, QString()).toUtf8()).array();
255 QJsonArray newSensors;
256 for (auto entry : original) {
257 QString sensorId = entry.toString();
258 for (auto replacement : std::as_const(sensorIdReplacements)) {
259 auto match = replacement.first.match(sensorId);
260 if (match.hasMatch()) {
261 sensorId.replace(replacement.first, replacement.second);
262 }
263 }
264 sensorId = replaceDiskId(sensorId);
265 sensorId = replacePartitionId(sensorId);
266 newSensors.append(sensorId);
267 }
268
269 return newSensors;
270}
271
272QJsonArray SensorFaceControllerPrivate::readAndUpdateSensors(KConfigGroup &config, const QString &entryName)
273{
274 auto original = QJsonDocument::fromJson(config.readEntry(entryName, QString()).toUtf8()).array();
275
276 const KConfigGroup &group = config;
277 auto newSensors = readSensors(group, entryName);
278
279 if (newSensors != original) {
280 config.writeEntry(entryName, QJsonDocument(newSensors).toJson(QJsonDocument::Compact));
281 }
282
283 return newSensors;
284}
285
286void SensorFaceControllerPrivate::resolveSensors(const QJsonArray &partialEntries, std::function<void(SensorResolver *)> callback)
287{
288 auto resolver = new SensorResolver{q, partialEntries};
289 resolver->callback = [this, callback](SensorResolver *resolver) {
290 callback(resolver);
291
292 if (!resolver->missing.isEmpty()) {
293 for (const auto &entry : std::as_const(resolver->missing)) {
294 missingSensors.append(entry);
295 }
296 Q_EMIT q->missingSensorsChanged();
297 }
298
299 delete resolver;
300 };
301 resolver->execute();
302}
303
304SensorFace *SensorFaceControllerPrivate::createGui(const QString &qmlPath)
305{
306 QQmlComponent *component = new QQmlComponent(engine, qmlPath, nullptr);
307 // TODO: eventually support async components? (only useful for qml files from http, we probably don't want that)
308 if (component->status() != QQmlComponent::Ready) {
309 qCritical() << "Error creating component:";
310 for (auto err : component->errors()) {
311 qWarning() << err.toString();
312 }
313 component->deleteLater();
314 return nullptr;
315 }
316
317 QQmlContext *context = new QQmlContext(engine);
318 context->setContextObject(contextObj);
319 QObject *guiObject = component->beginCreate(context);
320 SensorFace *gui = qobject_cast<SensorFace *>(guiObject);
321 if (!gui) {
322 qWarning() << "ERROR: QML gui" << guiObject << "not a SensorFace instance";
323 guiObject->deleteLater();
324 context->deleteLater();
325 return nullptr;
326 }
327 context->setParent(gui);
328
329 gui->setController(q);
330 gui->setParent(q);
331
332 component->completeCreate();
333
334 component->deleteLater();
335 return gui;
336}
337
338QQuickItem *SensorFaceControllerPrivate::createConfigUi(const QString &file, const QVariantMap &initialProperties)
339{
340 QQmlComponent *component = new QQmlComponent(engine, file, nullptr);
341 // TODO: eventually support async components? (only useful for qml files from http, we probably don't want that)
342 if (component->status() != QQmlComponent::Ready) {
343 qCritical() << "Error creating component:";
344 for (auto err : component->errors()) {
345 qWarning() << err.toString();
346 }
347 component->deleteLater();
348 return nullptr;
349 }
350
351 QQmlContext *context = new QQmlContext(engine);
352 context->setContextObject(contextObj);
353 QObject *guiObject = component->createWithInitialProperties(initialProperties, context);
354 QQuickItem *gui = qobject_cast<QQuickItem *>(guiObject);
355 Q_ASSERT(gui);
356 context->setParent(gui);
357 gui->setParent(q);
358
359 component->deleteLater();
360
361 return gui;
362}
363
365 : QObject(engine)
366 , d(std::make_unique<SensorFaceControllerPrivate>())
367{
368 d->q = this;
369 d->configGroup = config;
370 d->appearanceGroup = KConfigGroup(&config, "Appearance");
371 d->sensorsGroup = KConfigGroup(&config, "Sensors");
372 d->colorsGroup = KConfigGroup(&config, "SensorColors");
373 d->labelsGroup = KConfigGroup(&config, "SensorLabels");
374 d->engine = engine;
375 d->syncTimer = new QTimer(this);
376 d->syncTimer->setSingleShot(true);
377 d->syncTimer->setInterval(5000);
378 connect(d->syncTimer, &QTimer::timeout, this, [this]() {
379 if (!d->shouldSync) {
380 return;
381 }
382 d->appearanceGroup.sync();
383 d->sensorsGroup.sync();
384 });
385
386 d->contextObj = new KLocalizedContext(this);
387
388 d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("totalSensors")), [this](SensorResolver *resolver) {
389 d->totalSensors = resolver->found;
390 Q_EMIT totalSensorsChanged();
391 });
392 d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("lowPrioritySensorIds")), [this](SensorResolver *resolver) {
393 d->lowPrioritySensorIds = resolver->found;
394 Q_EMIT lowPrioritySensorIdsChanged();
395 });
396 d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("highPrioritySensorIds")), [this](SensorResolver *resolver) {
397 d->highPrioritySensorIds = resolver->found;
398 Q_EMIT highPrioritySensorIdsChanged();
399 });
400
401 setFaceId(d->appearanceGroup.readEntry("chartFace", QStringLiteral("org.kde.ksysguard.piechart")));
402}
403
404SensorFaceController::~SensorFaceController()
405{
406 if (!d->faceProperties.isValid()) {
407 return;
408 }
409
410 auto forceSave = d->faceProperties.readEntry(QStringLiteral("ForceSaveOnDestroy"), false);
411 if (!forceSave) {
412 if (!d->shouldSync) {
413 // If we should not sync automatically, clear all changes before we
414 // destroy the config objects, otherwise they will be written during
415 // destruction.
416 d->appearanceGroup.markAsClean();
417 d->colorsGroup.markAsClean();
418 d->labelsGroup.markAsClean();
419 if (d->faceConfigLoader && d->faceConfigLoader->isSaveNeeded()) {
420 d->faceConfigLoader->load();
421 }
422 }
423 } else {
424 d->faceConfigLoader->save();
425 }
426}
427
429{
430 return d->configGroup;
431}
432
434{
435 // both Title and title can exist to allow i18n of Title
436 if (d->appearanceGroup.hasKey("title")) {
437 return d->appearanceGroup.readEntry("title");
438 } else {
439 // if neither exist fall back to name
440 return d->appearanceGroup.readEntry("Title", i18n("System Monitor Sensor"));
441 }
442}
443
444void SensorFaceController::setTitle(const QString &title)
445{
447 return;
448 }
449
450 d->appearanceGroup.writeEntry("title", title);
451 d->syncTimer->start();
452
453 Q_EMIT titleChanged();
454}
455
457{
458 return d->appearanceGroup.readEntry("showTitle", true);
459}
460
461void SensorFaceController::setShowTitle(bool show)
462{
463 if (show == showTitle()) {
464 return;
465 }
466
467 d->appearanceGroup.writeEntry("showTitle", show);
468 d->syncTimer->start();
469
470 Q_EMIT showTitleChanged();
471}
472
474{
475 return d->totalSensors;
476}
477
478void SensorFaceController::setTotalSensors(const QJsonArray &totalSensors)
479{
480 if (totalSensors == d->totalSensors) {
481 return;
482 }
483 const auto currentEntry = QJsonDocument::fromJson(d->sensorsGroup.readEntry("totalSensors").toUtf8()).array();
484 if (totalSensors == currentEntry) {
485 return;
486 }
487 d->sensorsGroup.writeEntry("totalSensors", QJsonDocument(totalSensors).toJson(QJsonDocument::Compact));
488 // Until we have resolved
489 d->totalSensors = totalSensors;
490 d->syncTimer->start();
491 Q_EMIT totalSensorsChanged();
492 d->resolveSensors(totalSensors, [this](SensorResolver *resolver) {
493 if (resolver->found == d->totalSensors) {
494 return;
495 }
496 d->totalSensors = resolver->found;
497 Q_EMIT totalSensorsChanged();
498 });
499}
500
502{
503 return d->highPrioritySensorIds;
504}
505
506void SensorFaceController::setHighPrioritySensorIds(const QJsonArray &highPrioritySensorIds)
507{
508 if (highPrioritySensorIds == d->highPrioritySensorIds) {
509 return;
510 }
511 const auto currentEntry = QJsonDocument::fromJson(d->sensorsGroup.readEntry("highPrioritySensorIds").toUtf8()).array();
512 if (highPrioritySensorIds == currentEntry) {
513 return;
514 }
515 d->sensorsGroup.writeEntry("highPrioritySensorIds", QJsonDocument(highPrioritySensorIds).toJson(QJsonDocument::Compact));
516 // Until we have resolved
517 d->syncTimer->start();
518 d->highPrioritySensorIds = highPrioritySensorIds;
519 Q_EMIT highPrioritySensorIdsChanged();
520 d->resolveSensors(highPrioritySensorIds, [this](SensorResolver *resolver) {
521 if (resolver->found == d->highPrioritySensorIds) {
522 return;
523 }
524 d->highPrioritySensorIds = resolver->found;
525 Q_EMIT highPrioritySensorIdsChanged();
526 });
527}
528
530{
531 return d->missingSensors;
532}
533
534QVariantMap SensorFaceController::sensorColors() const
535{
536 QVariantMap colors;
537 for (const auto &key : d->colorsGroup.keyList()) {
538 QColor color = d->colorsGroup.readEntry(key, QColor());
539
540 if (color.isValid()) {
541 colors[key] = color;
542 }
543 }
544 return colors;
545}
546
547void SensorFaceController::setSensorColors(const QVariantMap &colors)
548{
549 if (colors == this->sensorColors()) {
550 return;
551 }
552
553 d->colorsGroup.deleteGroup();
554 d->colorsGroup = KConfigGroup(&d->configGroup, "SensorColors");
555
556 auto it = colors.constBegin();
557 for (; it != colors.constEnd(); ++it) {
558 d->colorsGroup.writeEntry(it.key(), it.value());
559 }
560
561 d->syncTimer->start();
562 Q_EMIT sensorColorsChanged();
563}
564
565QVariantMap SensorFaceController::sensorLabels() const
566{
567 QVariantMap labels;
568 for (const auto &key : d->labelsGroup.keyList()) {
569 labels[key] = d->labelsGroup.readEntry(key);
570 }
571 return labels;
572}
573
574void SensorFaceController::setSensorLabels(const QVariantMap &labels)
575{
576 if (labels == this->sensorLabels()) {
577 return;
578 }
579
580 d->labelsGroup.deleteGroup();
581 d->labelsGroup = KConfigGroup(&d->configGroup, "SensorLabels");
582
583 for (auto it = labels.cbegin(); it != labels.cend(); ++it) {
584 const auto label = it.value().toString();
585 if (!label.isEmpty()) {
586 d->labelsGroup.writeEntry(it.key(), label);
587 }
588 }
589
590 d->syncTimer->start();
591 Q_EMIT sensorLabelsChanged();
592}
593
595{
596 return d->lowPrioritySensorIds;
597}
598
599void SensorFaceController::setLowPrioritySensorIds(const QJsonArray &lowPrioritySensorIds)
600{
601 if (lowPrioritySensorIds == d->lowPrioritySensorIds) {
602 return;
603 }
604 const auto currentEntry = QJsonDocument::fromJson(d->sensorsGroup.readEntry("lowPrioritySensorIds").toUtf8()).array();
605 if (lowPrioritySensorIds == currentEntry) {
606 return;
607 }
608 d->sensorsGroup.writeEntry("lowPrioritySensorIds", QJsonDocument(lowPrioritySensorIds).toJson(QJsonDocument::Compact));
609 // Until we have resolved
610 d->lowPrioritySensorIds = lowPrioritySensorIds;
611 d->syncTimer->start();
612 Q_EMIT lowPrioritySensorIdsChanged();
613 d->resolveSensors(lowPrioritySensorIds, [this](SensorResolver *resolver) {
614 if (resolver->found == d->lowPrioritySensorIds) {
615 return;
616 }
617 d->lowPrioritySensorIds = resolver->found;
618 Q_EMIT lowPrioritySensorIdsChanged();
619 });
620}
621
623{
624 return d->appearanceGroup.readEntry<int>(QStringLiteral("updateRateLimit"), 0);
625}
626
627void SensorFaceController::setUpdateRateLimit(int limit)
628{
629 if (limit == updateRateLimit()) {
630 return;
631 }
632
633 d->appearanceGroup.writeEntry("updateRateLimit", limit);
634 d->syncTimer->start();
635
636 Q_EMIT updateRateLimitChanged();
637}
638
639// from face config, immutable by the user
641{
642 return d->facePackage.metadata().name();
643}
644
646{
647 return d->facePackage.metadata().iconName();
648}
649
651{
652 if (!d->faceProperties.isValid()) {
653 return false;
654 }
655
656 return d->faceProperties.readEntry("SupportsSensorsColors", false);
657}
658
660{
661 if (!d->faceProperties.isValid()) {
662 return false;
663 }
664
665 return d->faceProperties.readEntry("SupportsTotalSensors", false);
666}
667
669{
670 if (!d->faceProperties.isValid()) {
671 return false;
672 }
673
674 return d->faceProperties.readEntry("SupportsLowPrioritySensors", false);
675}
676
678{
679 if (!d->faceProperties.isValid()) {
680 return 1;
681 }
682
683 return d->faceProperties.readEntry("MaxTotalSensors", 1);
684}
685
686void SensorFaceController::setFaceId(const QString &face)
687{
688 if (d->faceId == face) {
689 return;
690 }
691
692 if (d->fullRepresentation) {
693 d->fullRepresentation->deleteLater();
694 d->fullRepresentation.clear();
695 }
696 if (d->compactRepresentation) {
697 d->compactRepresentation->deleteLater();
698 d->compactRepresentation.clear();
699 }
700 if (d->faceConfigUi) {
701 d->faceConfigUi->deleteLater();
702 d->faceConfigUi.clear();
703 }
704
705 d->faceId = face;
706
707 d->facePackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("KSysguard/SensorFace"), face);
708
709 if (d->faceConfiguration) {
710 d->faceConfiguration->deleteLater();
711 d->faceConfiguration = nullptr;
712 }
713 if (d->faceConfigLoader) {
714 d->faceConfigLoader->deleteLater();
715 d->faceConfigLoader = nullptr;
716 }
717
718 if (!d->facePackage.isValid()) {
719 Q_EMIT faceIdChanged();
720 return;
721 }
722
723 d->contextObj->setTranslationDomain(QLatin1String("ksysguard_face_") + face);
724
725 d->faceProperties = KConfigGroup(KSharedConfig::openConfig(d->facePackage.filePath("FaceProperties"), KConfig::SimpleConfig), QStringLiteral("Config"));
726
727 if (!d->faceProperties.isValid()) {
728 Q_EMIT faceIdChanged();
729 return;
730 }
731
733
734 d->appearanceGroup.writeEntry("chartFace", face);
735 d->syncTimer->start();
736 Q_EMIT faceIdChanged();
737 return;
738}
739
741{
742 return d->faceId;
743}
744
746{
747 return d->faceConfiguration;
748}
749
751{
752 if (!d->facePackage.isValid()) {
753 return nullptr;
754 } else if (d->compactRepresentation) {
755 return d->compactRepresentation;
756 }
757
758 d->compactRepresentation = d->createGui(d->facePackage.filePath("ui", QStringLiteral("CompactRepresentation.qml")));
759 return d->compactRepresentation;
760}
761
763{
764 if (!d->facePackage.isValid()) {
765 return nullptr;
766 } else if (d->fullRepresentation) {
767 return d->fullRepresentation;
768 }
769
770 d->fullRepresentation = d->createGui(d->facePackage.filePath("ui", QStringLiteral("FullRepresentation.qml")));
771 return d->fullRepresentation;
772}
773
775{
776 if (!d->facePackage.isValid()) {
777 return nullptr;
778 } else if (d->faceConfigUi) {
779 return d->faceConfigUi;
780 }
781
782 const QString filePath = d->facePackage.filePath("ui", QStringLiteral("Config.qml"));
783
784 if (filePath.isEmpty()) {
785 return nullptr;
786 }
787
788 d->faceConfigUi = d->createConfigUi(QStringLiteral(":/FaceDetailsConfig.qml"),
789 {{QStringLiteral("controller"), QVariant::fromValue(this)}, {QStringLiteral("source"), QUrl::fromLocalFile(filePath)}});
790
791 if (d->faceConfigUi && !d->faceConfigUi->property("item").value<QQuickItem *>()) {
792 d->faceConfigUi->deleteLater();
793 d->faceConfigUi.clear();
794 }
795 return d->faceConfigUi;
796}
797
799{
800 if (d->appearanceConfigUi) {
801 return d->appearanceConfigUi;
802 }
803
804 d->appearanceConfigUi = d->createConfigUi(QStringLiteral(":/ConfigAppearance.qml"), {{QStringLiteral("controller"), QVariant::fromValue(this)}});
805
806 return d->appearanceConfigUi;
807}
808
810{
811 if (d->sensorsConfigUi) {
812 return d->sensorsConfigUi;
813 }
814
815 if (d->faceProperties.isValid() && d->faceProperties.readEntry("SupportsSensors", true)) {
816 d->sensorsConfigUi = d->createConfigUi(QStringLiteral(":/ConfigSensors.qml"), {{QStringLiteral("controller"), QVariant::fromValue(this)}});
817 } else {
818 d->sensorsConfigUi = new QQuickItem;
819 }
820 return d->sensorsConfigUi;
821}
822
824{
825 if (d->availableFacesModel) {
826 return d->availableFacesModel;
827 }
828
829 d->availableFacesModel = new FacesModel(this);
830 return d->availableFacesModel;
831}
832
834{
835 if (d->availablePresetsModel) {
836 return d->availablePresetsModel;
837 }
838
839 d->availablePresetsModel = new PresetsModel(this);
840
841 return d->availablePresetsModel;
842}
843
845{
846 if (d->faceConfigLoader) {
847 d->faceConfigLoader->load();
848 }
849
850 d->missingSensors = QJsonArray{};
851 Q_EMIT missingSensorsChanged();
852
853 d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("totalSensors")), [this](SensorResolver *resolver) {
854 d->totalSensors = resolver->found;
855 Q_EMIT totalSensorsChanged();
856 });
857 d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("lowPrioritySensorIds")), [this](SensorResolver *resolver) {
858 d->lowPrioritySensorIds = resolver->found;
859 Q_EMIT lowPrioritySensorIdsChanged();
860 });
861 d->resolveSensors(d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("highPrioritySensorIds")), [this](SensorResolver *resolver) {
862 d->highPrioritySensorIds = resolver->found;
863 Q_EMIT highPrioritySensorIdsChanged();
864 });
865
866 // Force to re-read all the values
867 setFaceId(d->appearanceGroup.readEntry("chartFace", QStringLiteral("org.kde.ksysguard.textonly")));
868 Q_EMIT titleChanged();
869 Q_EMIT sensorColorsChanged();
870 Q_EMIT sensorLabelsChanged();
871 Q_EMIT showTitleChanged();
872 Q_EMIT updateRateLimitChanged();
873}
874
876{
877 if (preset.isEmpty()) {
878 return;
879 }
880
881 auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
882
883 presetPackage.setPath(preset);
884
885 if (!presetPackage.isValid()) {
886 return;
887 }
888
889 if (presetPackage.metadata().value(QStringLiteral("X-Plasma-RootPath")) != QStringLiteral("org.kde.plasma.systemmonitor")) {
890 return;
891 }
892
893 auto c = KSharedConfig::openConfig(presetPackage.filePath("config", QStringLiteral("faceproperties")), KConfig::SimpleConfig);
894 const KConfigGroup presetGroup(c, QStringLiteral("Config"));
895 const KConfigGroup colorsGroup(c, QStringLiteral("SensorColors"));
896
897 // Load the title
898 setTitle(presetPackage.metadata().name());
899
900 // Remove the "custom" value from presets models
901 if (d->availablePresetsModel && d->availablePresetsModel->data(d->availablePresetsModel->index(0, 0), PresetsModel::PluginIdRole).toString().isEmpty()) {
902 d->availablePresetsModel->removeRow(0);
903 }
904
905 setTotalSensors(d->readSensors(presetGroup, QStringLiteral("totalSensors")));
906 setHighPrioritySensorIds(d->readSensors(presetGroup, QStringLiteral("highPrioritySensorIds")));
907 setLowPrioritySensorIds(d->readSensors(presetGroup, QStringLiteral("lowPrioritySensorIds")));
908
909 setFaceId(presetGroup.readEntry(QStringLiteral("chartFace"), QStringLiteral("org.kde.ksysguard.piechart")));
910
911 colorsGroup.copyTo(&d->colorsGroup);
912 Q_EMIT sensorColorsChanged();
913
914 if (d->faceConfigLoader) {
915 KConfigGroup presetGroup(KSharedConfig::openConfig(presetPackage.filePath("FaceProperties"), KConfig::SimpleConfig), QStringLiteral("FaceConfig"));
916
917 for (const QString &key : presetGroup.keyList()) {
918 KConfigSkeletonItem *item = d->faceConfigLoader->findItemByName(key);
919 if (item) {
920 if (item->property().type() == QVariant::StringList) {
921 item->setProperty(presetGroup.readEntry(key, QStringList()));
922 } else {
923 item->setProperty(presetGroup.readEntry(key));
924 }
925 d->faceConfigLoader->save();
926 d->faceConfigLoader->read();
927 }
928 }
929 }
930}
931
933{
934 QString pluginName = QStringLiteral("org.kde.plasma.systemmonitor.") + title().simplified().replace(QLatin1Char(' '), QStringLiteral("")).toLower();
935 int suffix = 0;
936
937 auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"));
938
939 presetPackage.setPath(pluginName);
940 if (presetPackage.isValid()) {
941 do {
942 presetPackage.setPath(QString());
943 presetPackage.setPath(pluginName + QString::number(++suffix));
944 } while (presetPackage.isValid());
945
946 pluginName += QString::number(suffix);
947 }
948
949 QTemporaryDir dir;
950 if (!dir.isValid()) {
951 return;
952 }
953
954 // First write "new style" plugin JSON file.
955 QJsonDocument json;
956 json.setObject({
957 {"KPlugin",
959 {"Id", pluginName},
960 {"Name", title()},
961 {"Icon", "ksysguardd"},
962 {"Category", "System Information"},
963 {"License", "LGPL 2.1+"},
964 {"EnabledByDefault", true},
965 {"Version", "0.1"},
966 }},
967 {"X-Plasma-API", "declarativeappletscript"},
968 {"X-Plasma-MainScript", "ui/main.qml"},
969 {"X-Plasma-Provides", "org.kde.plasma.systemmonitor"},
970 {"X-Plasma-RootPath", "org.kde.plasma.systemmonitor"},
971 {"KPackageStructure", "Plasma/Applet"},
972 });
973
974 if (QFile file{dir.path() % QStringLiteral("/metadata.json")}; file.open(QIODevice::WriteOnly)) {
975 file.write(json.toJson());
976 } else {
977 qWarning() << "Could not write metadata.json file for preset" << title();
978 }
979
980 QDir subDir(dir.path());
981 subDir.mkpath(QStringLiteral("contents/config"));
982 KConfig faceConfig(subDir.path() % QStringLiteral("/contents/config/faceproperties"));
983
984 KConfigGroup configGroup(&faceConfig, "Config");
985
986 auto sensors = d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("totalSensors"));
987 configGroup.writeEntry(QStringLiteral("totalSensors"), QJsonDocument(sensors).toJson(QJsonDocument::Compact));
988 sensors = d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("highPrioritySensorIds"));
989 configGroup.writeEntry(QStringLiteral("highPrioritySensorIds"), QJsonDocument(sensors).toJson(QJsonDocument::Compact));
990 sensors = d->readAndUpdateSensors(d->sensorsGroup, QStringLiteral("lowPrioritySensorIds"));
991 configGroup.writeEntry(QStringLiteral("lowPrioritySensorIds"), QJsonDocument(sensors).toJson(QJsonDocument::Compact));
992 configGroup.writeEntry(QStringLiteral("chartFace"), faceId());
993
994 KConfigGroup colorsGroup(&faceConfig, "SensorColors");
995 d->colorsGroup.copyTo(&colorsGroup);
996 colorsGroup.sync();
997
998 configGroup = KConfigGroup(&faceConfig, "FaceConfig");
999 if (d->faceConfigLoader) {
1000 const auto &items = d->faceConfigLoader->items();
1001 for (KConfigSkeletonItem *item : items) {
1002 configGroup.writeEntry(item->key(), item->property());
1003 }
1004 }
1005 configGroup.sync();
1006
1007 auto *job = KPackage::PackageJob::install(QStringLiteral("Plasma/Applet"), dir.path());
1008 connect(job, &KJob::finished, this, [this]() {
1009 d->availablePresetsModel->reload();
1010 });
1011}
1012
1014{
1015 auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), pluginId);
1016
1017 if (presetPackage.metadata().value(QStringLiteral("X-Plasma-RootPath")) != QStringLiteral("org.kde.plasma.systemmonitor")) {
1018 return;
1019 }
1020
1021 QDir root(presetPackage.path());
1022 root.cdUp();
1023 auto *job = KPackage::PackageJob::uninstall(QStringLiteral("Plasma/Applet"), pluginId, root.path());
1024
1025 connect(job, &KJob::finished, this, [this]() {
1026 d->availablePresetsModel->reload();
1027 });
1028}
1029
1031{
1032 return d->shouldSync;
1033}
1034
1036{
1037 d->shouldSync = sync;
1038 if (!d->shouldSync && d->syncTimer->isActive()) {
1039 d->syncTimer->stop();
1040 }
1041}
1042
1044{
1045 const QString xmlPath = d->facePackage.filePath("mainconfigxml");
1046
1047 if (!xmlPath.isEmpty()) {
1048 QFile file(xmlPath);
1049 KConfigGroup cg(&d->configGroup, d->faceId);
1050
1051 if (d->faceConfigLoader) {
1052 delete d->faceConfigLoader;
1053 }
1054
1055 if (d->faceConfiguration) {
1056 delete d->faceConfiguration;
1057 }
1058
1059 d->faceConfigLoader = new KConfigLoader(cg, &file, this);
1060 d->faceConfiguration = new KConfigPropertyMap(d->faceConfigLoader, this);
1061 connect(d->faceConfiguration, &KConfigPropertyMap::valueChanged, this, [this](const QString &key) {
1062 auto item = d->faceConfigLoader->findItemByName(key);
1063 if (item) {
1064 item->writeConfig(d->faceConfigLoader->config());
1065 }
1066 });
1067
1068 Q_EMIT faceConfigurationChanged();
1069 }
1070}
1071
1073{
1074 auto replaceSensors = [this, from, to](const QString &configEntry) {
1075 auto array = QJsonDocument::fromJson(d->sensorsGroup.readEntry(configEntry, QString()).toUtf8()).array();
1076 for (auto itr = array.begin(); itr != array.end(); ++itr) {
1077 if (itr->toString() == from) {
1078 *itr = QJsonValue(to);
1079 }
1080 }
1082 };
1083
1084 d->sensorsGroup.writeEntry("totalSensors", replaceSensors(QStringLiteral("totalSensors")));
1085 d->sensorsGroup.writeEntry("highPrioritySensorIds", replaceSensors(QStringLiteral("highPrioritySensorIds")));
1086 d->sensorsGroup.writeEntry("lowPrioritySensorIds", replaceSensors(QStringLiteral("lowPrioritySensorIds")));
1087
1088 if (d->shouldSync) {
1089 d->sensorsGroup.sync();
1090 }
1091}
1092
1093#include "moc_SensorFaceController.cpp"
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
bool sync() override
QStringList keyList() const
void copyTo(KConfigBase *other, WriteConfigFlags pFlags=Normal) const
virtual void setProperty(const QVariant &p)=0
virtual QVariant property() const=0
void finished(KJob *job)
static PackageJob * uninstall(const QString &packageFormat, const QString &pluginId, const QString &packageRoot=QString())
static PackageJob * install(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot=QString())
QList< KPluginMetaData > findPackages(const QString &packageFormat, const QString &packageRoot=QString(), std::function< bool(const KPluginMetaData &)> filter=std::function< bool(const KPluginMetaData &)>())
Package loadPackage(const QString &packageFormat, const QString &packagePath=QString())
static PackageLoader * self()
QList< KPluginMetaData > listPackages(const QString &packageFormat, const QString &packageRoot=QString())
void setPath(const QString &path)
QString filePath(const QByteArray &key, const QString &filename=QString()) const
KPluginMetaData metadata() const
QString pluginId() const
bool value(const QString &key, bool defaultValue) const
QString fileName() const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
The SensorFaceController links sensor faces and applications in which these faces are shown.
int updateRateLimit
The minimum time that needs to elapse, in milliseconds, between updates of the face.
KConfigPropertyMap * faceConfiguration
A map of config options and values that are specific to the current face as defined by the main....
QAbstractItemModel * availablePresetsModel
A list of available face presets.
QVariantMap sensorLabels
Maps sensorIds to user configurable labels than should be displayed instead of the name of the sensor...
QJsonArray totalSensors
Sensors that are typically used to display a total in some way or form.
QString name
The name of the current face.
Q_INVOKABLE void replaceSensors(const QString &from, const QString &to)
Replace one sensor with another.
bool shouldSync() const
Whether the controller should sync configuration changes.
SensorFaceController(KConfigGroup &config, QQmlEngine *engine)
Creates a new SensorFaceController.
QJsonArray missingSensors
Contains the paths of missing sensors, if there are any.
QJsonArray highPrioritySensorIds
Sensors that should always be shown in the face.
QString icon
The icon of the current face.
QQuickItem * sensorsConfigUi
A user interface for configuring which sensors are displayed in a face Emits configurationChanged if ...
QString faceId
The id of the current face.
Q_INVOKABLE void loadPreset(const QString &preset)
Loads a specific preset.
QJsonArray lowPrioritySensorIds
Secondary list of sensors.
void setShouldSync(bool sync)
Specifies if the controller should automatically sync configuration changes.
QQuickItem * faceConfigUi
A user interface that is suited for configuring the face specific options.
Q_INVOKABLE void savePreset()
Save the current configuration as a preset.
KConfigGroup configGroup() const
Retrieve the KConfigGroup this controller is using to store configuration.
QQuickItem * appearanceConfigUi
A user interface for configuring the general appearance of a face like the title and the used face.
QQuickItem * compactRepresentation
The compact representation of the current face.
int maxTotalSensors
The amount of total sensors the current face supports.
bool showTitle
Whether the title should be displayed or if it should be hidden instead.
bool supportsLowPrioritySensors
Whether the current face can display low priority sensors.
bool supportsTotalSensors
Whether the current face can display total sensors.
QString title
A title for the face.
Q_INVOKABLE void uninstallPreset(const QString &pluginId)
Uninstall a specific preset.
QAbstractItemModel * availableFacesModel
A list of all available faces.
QQuickItem * fullRepresentation
The full representation of the current face.
bool supportsSensorsColors
Whether the current face supports sensor colors.
Q_INVOKABLE void reloadConfig()
Reload the configuration.
Q_INVOKABLE void reloadFaceConfiguration()
Reload only the face configuration.
QVariantMap sensorColors
Maps sensorIds to colors that can be used when a color for something relating to a specific sensor is...
Base for sensor faces.
An object to query the daemon for a list of sensors and their metadata.
Definition SensorQuery.h:26
static QList< Device > listFromQuery(const Predicate &predicate, const QString &parentUdi=QString())
QString i18n(const char *text, const TYPE &arg...)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QVariant read(const QByteArray &data, int versionOverride=0)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & reload()
QString label(StandardShortcut id)
QString name(StandardShortcut id)
virtual QHash< int, QByteArray > roleNames() const const
bool isValid() const const
bool cdUp()
bool mkpath(const QString &dirPath) const const
QString path() const const
bool isWritable() const const
void append(const QJsonValue &value)
iterator begin()
iterator end()
QJsonArray array() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
void setObject(const QJsonObject &object)
QByteArray toJson(JsonFormat format) const const
reverse_iterator rbegin()
reverse_iterator rend()
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
void setParent(QObject *parent)
virtual QObject * beginCreate(QQmlContext *context)
virtual void completeCreate()
QObject * createWithInitialProperties(const QVariantMap &initialProperties, QQmlContext *context)
void setContextObject(QObject *object)
void valueChanged(const QString &key, const QVariant &value)
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
bool contains(const QSet< T > &other) const const
virtual void setData(const QVariant &value, int role)
void appendRow(QStandardItem *item)
virtual QVariant data(const QModelIndex &index, int role) const const override
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
QStandardItem * item(int row, int column) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString simplified() const const
QString toLower() const const
void timeout()
QUrl fromLocalFile(const QString &localFile)
Type type() const const
QVariant fromValue(T &&value)
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:21:23 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.