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

KDE's Doxygen guidelines are available online.