diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ PURPOSE "Used by the HTML-based GUI ksysguard library" ) -find_package(KF5 REQUIRED COMPONENTS CoreAddons Config I18n WindowSystem Completion Auth WidgetsAddons IconThemes ConfigWidgets Service GlobalAccel KIO) +find_package(KF5 REQUIRED COMPONENTS CoreAddons Config I18n WindowSystem Completion Auth WidgetsAddons IconThemes ConfigWidgets Service GlobalAccel KIO Package Declarative) find_package(KF5 OPTIONAL_COMPONENTS Plasma) set_package_properties(KF5Plasma PROPERTIES URL "https://cgit.kde.org/plasma-framework.git/" @@ -110,6 +110,7 @@ add_subdirectory( processcore ) add_subdirectory( processui ) add_subdirectory( sensors ) +add_subdirectory( faces ) if (KF5Plasma_FOUND) add_subdirectory( signalplotter ) endif() diff --git a/faces/CMakeLists.txt b/faces/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/faces/CMakeLists.txt @@ -0,0 +1,67 @@ +set(KSYSGUARD_SENSORS_SOVERSION 1) + +add_subdirectory(import) +add_subdirectory(facepackages) +add_subdirectory(packagestructure) + +add_definitions(-DTRANSLATION_DOMAIN=\"ksysguard_faces\") + +set(faces_LIB_SRCS + SensorFaceController.cpp + SensorFace.cpp +) + +set(faces_LIB_HEADERS + SensorFace_p.h + SensorFaceController.h +) + +ecm_qt_declare_logging_category(faces_LIB_SRCS + HEADER faces_logging.h + IDENTIFIER LIBKSYSGUARD_SENSORS + CATEGORY_NAME org.kde.libksysguard.faces +) + +set_source_files_properties(org.kde.KSysGuardDaemon.xml PROPERTIES INCLUDE SensorInfo_p.h) +qt5_add_resources(RESOURCES resources.qrc) + +add_library(SensorFaces ${faces_LIB_SRCS} ${RESOURCES}) +add_library(KSysGuard::SensorFaces ALIAS SensorFaces) + +target_include_directories(SensorFaces + PUBLIC + "$" + "$" +) + +generate_export_header(SensorFaces) + +target_link_libraries(SensorFaces + PUBLIC + Qt5::Qml + Qt5::Quick + KF5::ConfigCore + KF5::ConfigGui + KSysGuard::Formatter + KSysGuard::Sensors + PRIVATE + Qt5::Core + Qt5::DBus + KF5::I18n + KF5::Package + KF5::Declarative +) + +set_target_properties(SensorFaces PROPERTIES + LIBRARY_OUTPUT_NAME KSysGuardSensorFaces + VERSION ${KSYSGUARD_VERSION_STRING} + SOVERSION ${KSYSGUARD_SENSORS_SOVERSION} +) + +install(TARGETS SensorFaces EXPORT libksysguardLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES + ${faces_LIB_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/sensorfaces_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/ksysguard/faces + COMPONENT Devel +) diff --git a/faces/ConfigAppearance.qml b/faces/ConfigAppearance.qml new file mode 100644 --- /dev/null +++ b/faces/ConfigAppearance.qml @@ -0,0 +1,165 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 + +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kquickcontrols 2.0 +import org.kde.kconfig 1.0 // for KAuthorized +import org.kde.newstuff 1.62 as NewStuff + +import org.kde.quickcharts 1.0 as Charts +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +Kirigami.FormLayout { + id: root + + signal configurationChanged + + function saveConfig() { + controller.title = cfg_title; + controller.faceId = cfg_chartFace; + + var preset = pendingPreset; + pendingPreset = ""; + if (preset != "") { + controller.loadPreset(preset); + root.controller.highPrioritySensorColors = automaticColorSource.colors + } + } + + property Faces.SensorFaceController controller + property alias cfg_title: titleField.text + property string cfg_chartFace + + onCfg_titleChanged: configurationChanged(); + onCfg_chartFaceChanged: configurationChanged(); + + // config keys of the selected preset to be applied on save + property string pendingPreset + + Component.onCompleted: { + cfg_title = controller.title; + cfg_chartFace = controller.faceId; + } + + Charts.ColorGradientSource { + id: automaticColorSource + baseColor: Kirigami.Theme.highlightColor + itemCount: root.controller.highPrioritySensorIds.length + } + + Kirigami.OverlaySheet { + id: presetSheet + parent: root + ListView { + implicitWidth: Kirigami.Units.gridUnit * 15 + model: controller.availablePresetsModel + delegate: Kirigami.SwipeListItem { + contentItem: QQC2.Label { + Layout.fillWidth: true + text: model.display + } + actions: Kirigami.Action { + icon.name: "delete" + visible: model.writable + onTriggered: controller.uninstallPreset(model.pluginId); + } + onClicked: { + cfg_title = model.display; + pendingPreset = model.pluginId; + if (model.config.chartFace) { + cfg_chartFace = model.config.chartFace; + } + + root.configurationChanged(); + presetSheet.close(); + } + } + } + } + RowLayout { + Kirigami.FormData.label: i18n("Presets:") + + QQC2.Button { + icon.name: "document-open" + text: i18n("Load Preset...") + onClicked: presetSheet.open() + } + + NewStuff.Button { + Accessible.name: i18n("Get new presets...") + configFile: "systemmonitor-presets.knsrc" + text: "" + onChangedEntriesChanged: controller.availablePresetsModel.reload(); + QQC2.ToolTip { + text: parent.Accessible.name + } + } + + QQC2.Button { + id: saveButton + icon.name: "document-save" + text: i18n("Save Settings As Preset") + enabled: controller.currentPreset.length == 0 + onClicked: controller.savePreset(); + } + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + + QQC2.TextField { + id: titleField + Kirigami.FormData.label: i18n("Title:") + } + + RowLayout { + Kirigami.FormData.label: i18n("Display Style:") + QQC2.ComboBox { + id: faceCombo + model: controller.availableFacesModel + textRole: "display" + currentIndex: { + // TODO just make an indexOf invokable on the model? + for (var i = 0; i < count; ++i) { + if (model.pluginId(i) === cfg_chartFace) { + return i; + } + } + return -1; + } + onActivated: { + cfg_chartFace = model.pluginId(index); + } + } + + NewStuff.Button { + text: i18n("Get New Display Styles...") + configFile: "systemmonitor-faces.knsrc" + onChangedEntriesChanged: controller.availableFacesModel.reload(); + } + } +} diff --git a/faces/ConfigSensors.qml b/faces/ConfigSensors.qml new file mode 100644 --- /dev/null +++ b/faces/ConfigSensors.qml @@ -0,0 +1,285 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 +import QtQml.Models 2.12 + +import org.kde.kirigami 2.8 as Kirigami +import org.kde.kquickcontrols 2.0 + +import org.kde.kitemmodels 1.0 as KItemModels + +import org.kde.quickcharts 1.0 as Charts +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import "./" as Local + +ColumnLayout { + id: root + + readonly property int implicitHeight: 1 //HACK FIXME to disable external scrollbar + + signal configurationChanged + + property var cfg_totalSensors + property alias cfg_highPrioritySensorIds: usedSensorsView.sensorIds + property alias cfg_sensorColors: usedSensorsView.sensorColors + + property alias cfg_lowPrioritySensorIds: lowPrioritySensorsView.sensorIds + + onCfg_totalSensorsChanged: configurationChanged(); + onCfg_highPrioritySensorIdsChanged: configurationChanged(); + onCfg_sensorColorsChanged: configurationChanged(); + onCfg_lowPrioritySensorIdsChanged: configurationChanged(); + + property Faces.SensorFaceController controller + + Sensors.Sensor { + id: totalSensor + sensorId: cfg_totalSensors.length > 0 ? cfg_totalSensors[0] : "" + } + + function saveConfig() { + controller.totalSensors = cfg_totalSensors; + controller.highPrioritySensorIds = cfg_highPrioritySensorIds; + controller.sensorColors = cfg_sensorColors; + controller.lowPrioritySensorIds = cfg_lowPrioritySensorIds; + } + + function loadConfig() { + cfg_totalSensors = controller.totalSensors; + cfg_highPrioritySensorIds = controller.highPrioritySensorIds; + cfg_sensorColors = controller.sensorColors; + usedSensorsView.load(); + + cfg_lowPrioritySensorIds = controller.lowPrioritySensorIds; + lowPrioritySensorsView.load(); + } + + Component.onCompleted: loadConfig() + + Connections { + target: controller + onTotalSensorsChanged: Qt.callLater(root.loadConfig) + onHighPrioritySensorIdsChanged: Qt.callLater(root.loadConfig) + onSensorColorsChanged: Qt.callLater(root.loadConfig) + onLowPrioritySensorIdsChanged: Qt.callLater(root.loadConfig) + } + + Component { + id: delegateComponent + Kirigami.SwipeListItem { + id: listItem + width: usedSensorsView.width + actions: Kirigami.Action { + icon.name: "list-remove" + text: i18n("Remove") + onTriggered: { + usedSensorsModel.remove(index, 1); + usedSensorsModel.save(); + } + } + contentItem: RowLayout { + Kirigami.ListItemDragHandle { + listItem: listItem + listView: usedSensorsView + onMoveRequested: { + usedSensorsModel.move(oldIndex, newIndex, 1) + usedSensorsModel.save(); + } + } + ColorButton { + id: textColorButton + color: model.color + onColorChanged: { + usedSensorsModel.setProperty(index, "color", color.toString()); + usedSensorsModel.save(); + } + } + QQC2.Label { + Layout.fillWidth: true + text: sensor.name + Sensors.Sensor { + id: sensor + sensorId: model.sensorId + } + } + } + } + } + + RowLayout { + Layout.preferredHeight: sensorListHeader.implicitHeight + visible: controller.supportsTotalSensors + QQC2.Label { + text: i18n("Total Sensor:") + } + QQC2.Label { + Layout.fillWidth: true + text: cfg_totalSensors.length > 0 ? totalSensor.name : i18n("Drop Sensor Here") + elide: Text.ElideRight + DropArea { + anchors.fill: parent + onEntered: { + if (drag.formats.indexOf("application/x-ksysguard") == -1) { + drag.accepted = false; + return; + } + } + onDropped: { + cfg_totalSensors = drop.getDataAsString("application/x-ksysguard") + } + } + } + QQC2.ToolButton { + icon.name: "list-remove" + opacity: cfg_totalSensors.length > 0 + onClicked: cfg_totalSensors = []; + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 0 + Layout.preferredHeight: 0 + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: 0 + Layout.preferredWidth: Kirigami.Units.gridUnit * 14 + + Kirigami.Heading { + Layout.preferredHeight: sensorListHeader.implicitHeight + level: 3 + text: i18n("Chart Sensors") + } + Local.UsedSensorsView { + id: usedSensorsView + showColor: controller.supportsSensorsColors + sensorColors: root.controller.sensorColors + } + Kirigami.Heading { + Layout.preferredHeight: sensorListHeader.implicitHeight + text: i18n("Text Only Sensors") + level: 3 + visible: lowPrioritySensorsView.visible + } + Local.UsedSensorsView { + id: lowPrioritySensorsView + visible: controller.supportsLowPrioritySensors + showColor: false + } + } + + ColumnLayout { + RowLayout { + id: sensorListHeader + Layout.fillWidth: true + QQC2.ToolButton { + icon.name: "go-previous" + enabled: sensorsDelegateModel.rootIndex.valid + onClicked: sensorsDelegateModel.rootIndex = sensorsDelegateModel.parentModelIndex() + } + Kirigami.Heading { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + level: 3 + text: i18n("All Sensors") + } + } + Kirigami.SearchField { + id: searchQuery + Layout.fillWidth: true + } + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: Kirigami.Units.gridUnit * 14 + + ListView { + KItemModels.KSortFilterProxyModel { + id: sensorsSearchableModel + filterCaseSensitivity: Qt.CaseInsensitive + filterString: searchQuery.text + sourceModel: KItemModels.KSortFilterProxyModel { + filterRole: "SensorId" + filterRowCallback: function(row, value) { + return (value && value.length) + } + sourceModel: KItemModels.KDescendantsProxyModel { + model: allSensorsTreeModel + } + } + } + + Sensors.SensorTreeModel { + id: allSensorsTreeModel + } + + model: DelegateModel { + id: sensorsDelegateModel + + model: searchQuery.text.length == 0 ? allSensorsTreeModel : sensorsSearchableModel + + delegate: Kirigami.BasicListItem { + id: sensorTreeDelegate + text: model.display + icon: (model.SensorId.length == 0) ? "folder" : "" + + Drag.active: model.SensorId.length > 0 && sensorTreeDelegate.pressed + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.CopyAction + Drag.hotSpot.x: sensorTreeDelegate.pressX + Drag.hotSpot.y: sensorTreeDelegate.pressY + Drag.mimeData: { + "application/x-ksysguard": model.SensorId + } + //FIXME: better handling of Drag + onPressed: { + onPressed: grabToImage(function(result) { + Drag.imageSource = result.url + }) + } + onClicked: { + if (model.SensorId.length == 0) { + sensorsDelegateModel.rootIndex = sensorsDelegateModel.modelIndex(index); + } + } + onDoubleClicked: { + if (model.SensorId) { + usedSensorsView.appendSensor(model.SensorId); + usedSensorsView.positionViewAtIndex(usedSensorsView.count - 1, ListView.Contain); + } + } + } + } + } + Component.onCompleted: background.visible = true; + QQC2.ScrollBar.horizontal.visible: false + } + } + } +} diff --git a/faces/FaceDetailsConfig.qml b/faces/FaceDetailsConfig.qml new file mode 100644 --- /dev/null +++ b/faces/FaceDetailsConfig.qml @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 + +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kquickcontrols 2.0 + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +Loader { + id: root + + property Faces.SensorFaceController controller + + signal configurationChanged + + function saveConfig() { + if (item.saveConfig) { + item.saveConfig() + } + for (var key in root.controller.faceConfiguration) { + if (item.hasOwnProperty("cfg_" + key)) { + root.controller.faceConfiguration[key] = item["cfg_" + key] + } + } + } + + + onItemChanged: { + if (!item || !root.controller.faceConfiguration) { + return; + } + + for (var key in root.controller.faceConfiguration) { + if (!item.hasOwnProperty("cfg_" + key)) { + continue; + } + + item["cfg_" + key] = root.controller.faceConfiguration[key]; + var changedSignal = item["cfg_" + key + "Changed"]; + if (changedSignal) { + changedSignal.connect(root.configurationChanged); + } + } + } +} diff --git a/faces/SensorFace.cpp b/faces/SensorFace.cpp new file mode 100644 --- /dev/null +++ b/faces/SensorFace.cpp @@ -0,0 +1,104 @@ +/* + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "SensorFace_p.h" +#include "SensorFaceController.h" + +#include + +using namespace KSysGuard; + +class SensorFace::Private { +public: + QPointer contentItem; + SensorFaceController *controller = nullptr; + FormFactor formFactor = Planar; +}; + +SensorFace::SensorFace(QQuickItem *parent) + : QQuickItem(parent), + d(std::make_unique()) +{ + +} + +SensorFace::~SensorFace() +{ +} + +SensorFaceController *SensorFace::controller() const +{ + return d->controller; +} + +// Not writable from QML +void SensorFace::setController(SensorFaceController *controller) +{ + d->controller = controller; +} + +SensorFace::FormFactor SensorFace::formFactor() const +{ + return d->formFactor; +} + +void SensorFace::setFormFactor(SensorFace::FormFactor formFactor) +{ + if (d->formFactor == formFactor) { + return; + } + + d->formFactor = formFactor; + emit formFactorChanged(); +} + +QQuickItem * SensorFace::contentItem() const +{ + return d->contentItem; +} + +void SensorFace::setContentItem(QQuickItem *item) +{ + if (d->contentItem == item) { + return; + } + d->contentItem = item; + + if (d->contentItem) { + d->contentItem->setParentItem(this); + d->contentItem->setX(0); + d->contentItem->setY(0); + d->contentItem->setSize(size()); + } + + emit contentItemChanged(); +} + +void SensorFace::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (d->contentItem) { + d->contentItem->setX(0); + d->contentItem->setY(0); + d->contentItem->setSize(newGeometry.size()); + } + + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +#include "moc_SensorFace_p.cpp" diff --git a/faces/SensorFaceController.h b/faces/SensorFaceController.h new file mode 100644 --- /dev/null +++ b/faces/SensorFaceController.h @@ -0,0 +1,130 @@ +/* + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "sensorfaces_export.h" + +namespace KDeclarative { + class ConfigPropertyMap; +} + +class QQmlEngine; +class KDesktopFile; +class KConfigLoader; + +namespace KSysGuard { + +class SensorFace; + +class SENSORFACES_EXPORT SensorFaceController : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) + Q_PROPERTY(QString faceId READ faceId WRITE setFaceId NOTIFY faceIdChanged) + Q_PROPERTY(QJsonArray totalSensors READ totalSensors WRITE setTotalSensors NOTIFY totalSensorsChanged) + Q_PROPERTY(QJsonArray highPrioritySensorIds READ highPrioritySensorIds WRITE setHighPrioritySensorIds NOTIFY highPrioritySensorIdsChanged) + Q_PROPERTY(QVariantMap sensorColors READ sensorColors WRITE setSensorColors NOTIFY sensorColorsChanged) + Q_PROPERTY(QJsonArray lowPrioritySensorIds READ lowPrioritySensorIds WRITE setLowPrioritySensorIds NOTIFY lowPrioritySensorIdsChanged) + + Q_PROPERTY(QString name READ name NOTIFY faceIdChanged) + Q_PROPERTY(QString icon READ icon NOTIFY faceIdChanged) + Q_PROPERTY(bool supportsSensorsColors READ supportsSensorsColors NOTIFY faceIdChanged) + Q_PROPERTY(bool supportsTotalSensors READ supportsTotalSensors NOTIFY faceIdChanged) + Q_PROPERTY(bool supportsLowPrioritySensors READ supportsLowPrioritySensors NOTIFY faceIdChanged) + Q_PROPERTY(KDeclarative::ConfigPropertyMap *faceConfiguration READ faceConfiguration NOTIFY faceIdChanged) + + Q_PROPERTY(QQuickItem *fullRepresentation READ fullRepresentation NOTIFY faceIdChanged) + Q_PROPERTY(QQuickItem *compactRepresentation READ compactRepresentation NOTIFY faceIdChanged) + Q_PROPERTY(QQuickItem *faceConfigUi READ faceConfigUi NOTIFY faceIdChanged) + Q_PROPERTY(QQuickItem *appearanceConfigUi READ appearanceConfigUi NOTIFY faceIdChanged) + Q_PROPERTY(QQuickItem *sensorsConfigUi READ sensorsConfigUi NOTIFY faceIdChanged) + + Q_PROPERTY(QAbstractItemModel *availableFacesModel READ availableFacesModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *availablePresetsModel READ availablePresetsModel CONSTANT) + +public: + SensorFaceController(KConfigGroup &config, QQmlEngine *engine); + ~SensorFaceController(); + + void setFaceId(const QString &face); + QString faceId() const; + + QQuickItem *fullRepresentation(); + QQuickItem *compactRepresentation(); + QQuickItem *faceConfigUi(); + QQuickItem *appearanceConfigUi(); + QQuickItem *sensorsConfigUi(); + + KDeclarative::ConfigPropertyMap *faceConfiguration() const; + + QString title() const; + void setTitle(const QString &title); + + QJsonArray totalSensors() const; + void setTotalSensors(const QJsonArray &sensor); + + QJsonArray highPrioritySensorIds() const; + void setHighPrioritySensorIds(const QJsonArray &ids); + + QJsonArray sensors() const; + + QJsonArray lowPrioritySensorIds() const; + void setLowPrioritySensorIds(const QJsonArray &ids); + + QVariantMap sensorColors() const; + void setSensorColors(const QVariantMap &colors); + + // from face config, immutable by the user + QString name() const; + const QString icon() const; + + bool supportsSensorsColors() const; + bool supportsTotalSensors() const; + bool supportsLowPrioritySensors() const; + + QAbstractItemModel *availableFacesModel(); + QAbstractItemModel *availablePresetsModel(); + + Q_INVOKABLE void reloadConfig(); + Q_INVOKABLE void loadPreset(const QString &preset); + Q_INVOKABLE void savePreset(); + Q_INVOKABLE void uninstallPreset(const QString &pluginId); + +Q_SIGNALS: + void faceIdChanged(); + void titleChanged(); + void totalSensorsChanged(); + void highPrioritySensorIdsChanged(); + void lowPrioritySensorIdsChanged(); + void sensorsChanged(); + void sensorColorsChanged(); + +private: + class Private; + const std::unique_ptr d; +}; +} diff --git a/faces/SensorFaceController.cpp b/faces/SensorFaceController.cpp new file mode 100644 --- /dev/null +++ b/faces/SensorFaceController.cpp @@ -0,0 +1,706 @@ +/* + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "SensorFaceController.h" +#include "SensorFaceController_p.h" +#include "SensorFace_p.h" +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace KSysGuard; + +FacesModel::FacesModel(QObject *parent) + : QStandardItemModel(parent) +{ + reload(); +} + +void FacesModel::reload() +{ + clear(); + + auto list = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KSysguard/SensorFace")); + // NOTE: This will disable completely the internal in-memory cache + KPackage::Package p; + p.install(QString(), QString()); + + for (auto plugin : list) { + QStandardItem *item = new QStandardItem(plugin.name()); + item->setData(plugin.pluginId(), FacesModel::PluginIdRole); + appendRow(item); + } +} + +QString FacesModel::pluginId(int row) +{ + return data(index(row, 0), PluginIdRole).toString(); +} + +QHash FacesModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + roles[PluginIdRole] = "pluginId"; + return roles; +} + +PresetsModel::PresetsModel(QObject *parent) + : QStandardItemModel(parent) +{ + reload(); +} + +void PresetsModel::reload() +{ + clear(); + QList plugins = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/Applet"), QString(), [](const KPluginMetaData &plugin) { + return plugin.value(QStringLiteral("X-Plasma-RootPath")) == QStringLiteral("org.kde.plasma.systemmonitor"); + }); + + QSet usedNames; + + // We iterate backwards because packages under ~/.local are listed first, while we want them last + auto it = plugins.rbegin(); + for (; it != plugins.rend(); ++it) { + const auto &plugin = *it; + KPackage::Package p = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), plugin.pluginId()); + KDesktopFile df(p.path() + QStringLiteral("metadata.desktop")); + + QString baseName = df.readName(); + QString name = baseName; + int id = 0; + + while (usedNames.contains(name)) { + name = baseName + QStringLiteral(" (") + QString::number(++id) + QStringLiteral(")"); + } + usedNames << name; + + QStandardItem *item = new QStandardItem(baseName); + + // TODO config + QVariantMap config; + + KConfigGroup configGroup(df.group("Config")); + + const QStringList keys = configGroup.keyList(); + for (const QString &key : keys) { + // all strings for now, type conversion happens in QML side when we have the config property map + config.insert(key, configGroup.readEntry(key)); + } + + item->setData(plugin.pluginId(), PresetsModel::PluginIdRole); + item->setData(config, PresetsModel::ConfigRole); + + item->setData(QFileInfo(p.path() + QStringLiteral("metadata.desktop")).isWritable(), PresetsModel::WritableRole); + + appendRow(item); + } +} + +QHash PresetsModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + roles[PluginIdRole] = "pluginId"; + roles[ConfigRole] = "config"; + roles[WritableRole] = "writable"; + return roles; +} + + + +class SensorFaceController::Private +{ +public: + Private(); + SensorFace *createGui(const QString &qmlPath); + QQuickItem *createConfigUi(const QString &file, const QVariantMap &initialProperties); + + SensorFaceController *q; + QString title; + QQmlEngine *engine; + + KConfigGroup faceProperties; + KDeclarative::ConfigPropertyMap *faceConfiguration = nullptr; + KConfigLoader *faceConfigLoader = nullptr; + + bool configNeedsSave = false; + KPackage::Package facePackage; + QString faceId; + KConfigGroup configGroup; + KConfigGroup appearanceGroup; + KConfigGroup sensorsGroup; + KConfigGroup colorsGroup; + QPointer fullRepresentation; + QPointer compactRepresentation; + QPointer faceConfigUi; + QPointer appearanceConfigUi; + QPointer sensorsConfigUi; + + QTimer *syncTimer; + FacesModel *availableFacesModel = nullptr; + PresetsModel *availablePresetsModel = nullptr; +}; + +SensorFaceController::Private::Private() +{} + +SensorFace *SensorFaceController::Private::createGui(const QString &qmlPath) +{ + QQmlComponent *component = new QQmlComponent(engine, qmlPath, nullptr); + // TODO: eventually support async components? (only useful for qml files from http, we probably don't want that) + if (component->status() != QQmlComponent::Ready) { + qCritical() << "Error creating component:"; + for (auto err : component->errors()) { + qWarning() << err.toString(); + } + component->deleteLater(); + return nullptr; + } + + //TODO: add i18n context object + QQmlContext *context = new QQmlContext(engine); + QObject *guiObject = component->beginCreate(context); + SensorFace *gui = qobject_cast(guiObject); + if (!gui) { + qWarning()<<"ERROR: QML gui" << guiObject << "not a SensorFace instance"; + guiObject->deleteLater(); + context->deleteLater(); + return nullptr; + } + // context->setParent(gui); + + gui->setController(q); + + component->completeCreate(); + + component->deleteLater(); + return gui; +} + +QQuickItem *SensorFaceController::Private::createConfigUi(const QString &file, const QVariantMap &initialProperties) +{ + QQmlComponent *component = new QQmlComponent(engine, file, nullptr); + // TODO: eventually support async components? (only useful for qml files from http, we probably don't want that) + if (component->status() != QQmlComponent::Ready) { + qCritical() << "Error creating component:"; + for (auto err : component->errors()) { + qWarning() << err.toString(); + } + component->deleteLater(); + return nullptr; + } + + //TODO: add i18n context object + QQmlContext *context = new QQmlContext(engine); + QObject *guiObject = component->createWithInitialProperties( + initialProperties, context); + QQuickItem *gui = qobject_cast(guiObject); + Q_ASSERT(gui); + + component->deleteLater(); + + return gui; +} + + + + + +SensorFaceController::SensorFaceController(KConfigGroup &config, QQmlEngine *engine) + : QObject(engine), + d(std::make_unique()) +{ + d->q = this; + d->configGroup = config; + d->appearanceGroup = KConfigGroup(&config, "Appearance"); + d->sensorsGroup = KConfigGroup(&config, "Sensors"); + d->colorsGroup = KConfigGroup(&config, "SensorColors"); + d->engine = engine; + d->syncTimer = new QTimer(this); + d->syncTimer->setSingleShot(true); + d->syncTimer->setInterval(5000); + connect(d->syncTimer, &QTimer::timeout, this, [this]() { + d->appearanceGroup.sync(); + d->sensorsGroup.sync(); + }); + + setFaceId(d->appearanceGroup.readEntry("chartFace", QStringLiteral("org.kde.ksysguard.piechart"))); +} + +SensorFaceController::~SensorFaceController() +{ +} + +QString SensorFaceController::title() const +{ + return d->appearanceGroup.readEntry("title", name()); +} + +void SensorFaceController::setTitle(const QString &title) +{ + if (title == SensorFaceController::title()) { + return; + } + + d->appearanceGroup.writeEntry("title", title); + d->syncTimer->start(); + + emit titleChanged(); +} + +QJsonArray SensorFaceController::totalSensors() const +{ + QJsonDocument doc = QJsonDocument::fromJson(d->sensorsGroup.readEntry("totalSensors", QString()).toUtf8()); + return doc.array(); +} + +void SensorFaceController::setTotalSensors(const QJsonArray &totalSensors) +{ + if (totalSensors == SensorFaceController::totalSensors()) { + return; + } + + d->sensorsGroup.writeEntry("totalSensors", QJsonDocument(totalSensors).toJson(QJsonDocument::Compact)); + d->syncTimer->start(); + emit totalSensorsChanged(); +} + +QJsonArray SensorFaceController::highPrioritySensorIds() const +{ + QJsonDocument doc = QJsonDocument::fromJson(d->sensorsGroup.readEntry("highPrioritySensorIds", QString()).toUtf8()); + return doc.array(); +} + + +void SensorFaceController::setHighPrioritySensorIds(const QJsonArray &highPrioritySensorIds) +{ + if (highPrioritySensorIds == SensorFaceController::highPrioritySensorIds()) { + return; + } + + d->sensorsGroup.writeEntry("highPrioritySensorIds", QJsonDocument(highPrioritySensorIds).toJson(QJsonDocument::Compact)); + d->syncTimer->start(); + emit highPrioritySensorIdsChanged(); +} + +QVariantMap SensorFaceController::sensorColors() const +{ + QVariantMap colors; + for (const auto &key : d->colorsGroup.keyList()) { + QColor color = d->colorsGroup.readEntry(key, QColor()); + + if (color.isValid()) { + colors[key] = color; + } + } + return colors; +} + +void SensorFaceController::setSensorColors(const QVariantMap &colors) +{ + d->colorsGroup.deleteGroup(); + d->colorsGroup = KConfigGroup(&d->configGroup, "SensorColors"); + + auto it = colors.constBegin(); + for (; it != colors.constEnd(); ++it) { + d->colorsGroup.writeEntry(it.key(), it.value()); + } + + d->syncTimer->start(); + emit sensorColorsChanged(); +} + +QJsonArray SensorFaceController::lowPrioritySensorIds() const +{ + QJsonDocument doc = QJsonDocument::fromJson(d->sensorsGroup.readEntry("lowPrioritySensorIds", QString()).toUtf8()); + return doc.array(); +} + +void SensorFaceController::setLowPrioritySensorIds(const QJsonArray &lowPrioritySensorIds) +{ + if (lowPrioritySensorIds == SensorFaceController::lowPrioritySensorIds()) { + return; + } + + d->sensorsGroup.writeEntry("lowPrioritySensorIds", QJsonDocument(lowPrioritySensorIds).toJson(QJsonDocument::Compact)); + d->syncTimer->start(); + emit lowPrioritySensorIdsChanged(); +} + +// from face config, immutable by the user +QString SensorFaceController::name() const +{ + return d->facePackage.metadata().name(); +} + +const QString SensorFaceController::icon() const +{ + return d->facePackage.metadata().iconName(); +} + +bool SensorFaceController::supportsSensorsColors() const +{ + return d->faceProperties.readEntry("SupportsSensorsColors", false); +} + +bool SensorFaceController::supportsTotalSensors() const +{ + return d->faceProperties.readEntry("SupportsTotalSensors", false); +} + +bool SensorFaceController::supportsLowPrioritySensors() const +{ + return d->faceProperties.readEntry("SupportsLowPrioritySensors", false); +} + +void SensorFaceController::setFaceId(const QString &face) +{ + if (d->faceId == face) { + return; + } + + if (d->fullRepresentation) { + d->fullRepresentation->deleteLater(); + d->fullRepresentation.clear(); + } + if (d->compactRepresentation) { + d->compactRepresentation->deleteLater(); + d->fullRepresentation.clear(); + } + + d->faceId = face; + + d->facePackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("KSysguard/SensorFace"), face); + + if (d->faceConfiguration) { + d->faceConfiguration->deleteLater(); + d->faceConfiguration = nullptr; + } + if (d->faceConfigLoader) { + d->faceConfigLoader ->deleteLater(); + d->faceConfigLoader = nullptr; + } + + if (!d->facePackage.isValid()) { + emit faceIdChanged(); + return; + } + + //TODO: should be in a different config file rather than metadata + d->faceProperties = KConfigGroup(KSharedConfig::openConfig(d->facePackage.filePath("FaceProperties")), QStringLiteral("Config")); + + const QString xmlPath = d->facePackage.filePath("mainconfigxml"); + + if (!xmlPath.isEmpty()) { + QFile file(xmlPath); + KConfigGroup cg(&d->configGroup, d->faceId); + + d->faceConfigLoader = new KConfigLoader(cg, &file, this); + d->faceConfiguration = new KDeclarative::ConfigPropertyMap(d->faceConfigLoader, this); + } + + d->appearanceGroup.writeEntry("chartFace", face); + d->syncTimer->start(); + emit faceIdChanged(); + return; +} + +QString SensorFaceController::faceId() const +{ + return d->faceId; +} + +KDeclarative::ConfigPropertyMap *SensorFaceController::faceConfiguration() const +{ + return d->faceConfiguration; +} + +QQuickItem *SensorFaceController::compactRepresentation() +{ + if (!d->facePackage.isValid()) { + return nullptr; + } else if (d->compactRepresentation) { + return d->compactRepresentation; + } + + d->fullRepresentation = d->createGui(d->facePackage.filePath("ui", QStringLiteral("CompactRepresentation.qml"))); + return d->compactRepresentation; +} + +QQuickItem *SensorFaceController::fullRepresentation() +{ + if (!d->facePackage.isValid()) { + return nullptr; + } else if (d->fullRepresentation) { + return d->fullRepresentation; + } + + d->fullRepresentation = d->createGui(d->facePackage.filePath("ui", QStringLiteral("FullRepresentation.qml"))); + + return d->fullRepresentation; +} + +QQuickItem *SensorFaceController::faceConfigUi() +{ + if (!d->facePackage.isValid()) { + return nullptr; + } else if (d->faceConfigUi) { + return d->faceConfigUi; + } + + d->faceConfigUi = d->createConfigUi(QStringLiteral(":/FaceDetailsConfig.qml"), + {{QStringLiteral("controller"), QVariant::fromValue(this)}, + {QStringLiteral("source"), d->facePackage.filePath("ui", QStringLiteral("Config.qml"))}}); + + return d->faceConfigUi; +} + +QQuickItem *SensorFaceController::appearanceConfigUi() +{ + if (d->appearanceConfigUi) { + return d->appearanceConfigUi; + } + + d->appearanceConfigUi = d->createConfigUi(QStringLiteral(":/ConfigAppearance.qml"), {{QStringLiteral("controller"), QVariant::fromValue(this)}}); + + return d->appearanceConfigUi; +} + +QQuickItem *SensorFaceController::sensorsConfigUi() +{ + if (d->sensorsConfigUi) { + return d->sensorsConfigUi; + } + + d->sensorsConfigUi = d->createConfigUi(QStringLiteral(":/ConfigSensors.qml"), {{QStringLiteral("controller"), QVariant::fromValue(this)}}); + + return d->sensorsConfigUi; +} + +QAbstractItemModel *SensorFaceController::availableFacesModel() +{ + if (d->availableFacesModel) { + return d->availableFacesModel; + } + + d->availableFacesModel = new FacesModel(this); + return d->availableFacesModel; +} + +QAbstractItemModel *SensorFaceController::availablePresetsModel() +{ + if (d->availablePresetsModel) { + return d->availablePresetsModel; + } + + d->availablePresetsModel = new PresetsModel(this); + + return d->availablePresetsModel; +} + +void SensorFaceController::reloadConfig() +{ + if (d->faceConfigLoader) { + d->faceConfigLoader->load(); + } + + //Force to re-read all the values + setFaceId(d->appearanceGroup.readEntry("chartFace", QStringLiteral("org.kde.ksysguard.textonly"))); + titleChanged(); + totalSensorsChanged(); + highPrioritySensorIdsChanged(); + sensorColorsChanged(); + lowPrioritySensorIdsChanged(); +} + +void SensorFaceController::loadPreset(const QString &preset) +{ + if (preset.isEmpty()) { + return; + } + + auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet")); + + presetPackage.setPath(preset); + + if (!presetPackage.isValid()) { + return; + } + + if (presetPackage.metadata().value(QStringLiteral("X-Plasma-RootPath")) != QStringLiteral("org.kde.plasma.systemmonitor")) { + return; + } + + KDesktopFile df(presetPackage.path() + QStringLiteral("metadata.desktop")); + + auto c = KSharedConfig::openConfig(presetPackage.filePath("config", QStringLiteral("faceproperties"))); + KConfigGroup presetGroup(c, QStringLiteral("Config")); + KConfigGroup colorsGroup(c, QStringLiteral("SensorColors")); + + // Load the title + setTitle(df.readName()); + + //Remove the "custon" value from presets models + if (d->availablePresetsModel && + d->availablePresetsModel->data(d->availablePresetsModel->index(0, 0), PresetsModel::PluginIdRole).toString().isEmpty()) { + d->availablePresetsModel->removeRow(0); + } + + auto loadSensors = [this](const QJsonArray &partialEntries) { + QJsonArray sensors; + + for (const auto &id : partialEntries) { + KSysGuard::SensorQuery query{id.toString()}; + query.execute(); + query.waitForFinished(); + for (const auto &fitleredId : query.sensorIds()) { + sensors.append(QJsonValue(fitleredId)); + } + } + return sensors; + }; + + QJsonDocument doc = QJsonDocument::fromJson(presetGroup.readEntry("totalSensors", QString()).toUtf8()); + setTotalSensors(loadSensors(doc.array())); + + doc = QJsonDocument::fromJson(presetGroup.readEntry("highPrioritySensorIds", QString()).toUtf8()); + setHighPrioritySensorIds(loadSensors(doc.array())); + + doc = QJsonDocument::fromJson(presetGroup.readEntry("lowPrioritySensorIds", QString()).toUtf8()); + setLowPrioritySensorIds(loadSensors(doc.array())); + + setFaceId(presetGroup.readEntry(QStringLiteral("chartFace"), QStringLiteral("org.kde.ksysguard.piechart"))); + + colorsGroup.copyTo(&d->colorsGroup); + emit sensorColorsChanged(); + + if (d->faceConfigLoader) { + KConfigGroup presetGroup(KSharedConfig::openConfig(presetPackage.filePath("FaceProperties")), QStringLiteral("FaceConfig")); + + for (const QString &key : presetGroup.keyList()) { + KConfigSkeletonItem *item = d->faceConfigLoader->findItemByName(key); + if (item) { + if (item->property().type() == QVariant::StringList) { + item->setProperty(presetGroup.readEntry(key, QStringList())); + } else { + item->setProperty(presetGroup.readEntry(key)); + } + d->faceConfigLoader->save(); + d->faceConfigLoader->read(); + } + } + } +} + +void SensorFaceController::savePreset() +{ + QString pluginName = QStringLiteral("org.kde.plasma.systemmonitor.") + title().simplified().replace(QLatin1Char(' '), QChar()).toLower(); + int suffix = 0; + + auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet")); + + presetPackage.setPath(pluginName); + if (presetPackage.isValid()) { + do { + presetPackage.setPath(QString()); + presetPackage.setPath(pluginName + QString::number(++suffix)); + } while (presetPackage.isValid()); + + pluginName += QString::number(suffix); + } + + QTemporaryDir dir; + if (!dir.isValid()) { + return; + } + + KConfig c(dir.path() % QLatin1Literal("/metadata.desktop")); + + KConfigGroup cg(&c, "Desktop Entry"); + cg.writeEntry("Name", title()); + cg.writeEntry("Icon", "ksysguardd"); + cg.writeEntry("X-Plasma-API", "declarativeappletscript"); + cg.writeEntry("X-Plasma-MainScript", "ui/main.qml"); + cg.writeEntry("X-Plasma-Provides", "org.kde.plasma.systemmonitor"); + cg.writeEntry("X-Plasma-RootPath", "org.kde.plasma.systemmonitor"); + cg.writeEntry("X-KDE-PluginInfo-Name", pluginName); + cg.writeEntry("X-KDE-ServiceTypes", "Plasma/Applet"); + cg.writeEntry("X-KDE-PluginInfo-Category", "System Information"); + cg.writeEntry("X-KDE-PluginInfo-License", "LGPL 2.1+"); + cg.writeEntry("X-KDE-PluginInfo-EnabledByDefault", "true"); + cg.writeEntry("X-KDE-PluginInfo-Version", "0.1"); + cg.sync(); + + QDir subDir(dir.path()); + subDir.mkdir(QStringLiteral("contents")); + KConfig faceConfig(subDir.path() % QLatin1Literal("/contents/faceproperties")); + + KConfigGroup configGroup(&faceConfig, "Config"); + configGroup.writeEntry(QStringLiteral("totalSensors"), QJsonDocument(totalSensors()).toJson(QJsonDocument::Compact)); + configGroup.writeEntry(QStringLiteral("highPrioritySensorIds"), QJsonDocument(highPrioritySensorIds()).toJson(QJsonDocument::Compact)); + configGroup.writeEntry(QStringLiteral("lowPrioritySensorIds"), QJsonDocument(lowPrioritySensorIds()).toJson(QJsonDocument::Compact)); + + KConfigGroup colorsGroup(&faceConfig, "SensorColors"); + d->colorsGroup.copyTo(&colorsGroup); + colorsGroup.sync(); + + configGroup = KConfigGroup(&faceConfig, "FaceConfig"); + if (d->faceConfigLoader) { + const auto &items = d->faceConfigLoader->items(); + for (KConfigSkeletonItem *item : items) { + configGroup.writeEntry(item->key(), item->property()); + } + } + configGroup.sync(); + + auto *job = presetPackage.install(dir.path()); + + connect(job, &KJob::finished, this, [this, pluginName] () { + d->availablePresetsModel->reload(); + }); +} + +void SensorFaceController::uninstallPreset(const QString &pluginId) +{ + auto presetPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), pluginId); + + if (presetPackage.metadata().value(QStringLiteral("X-Plasma-RootPath")) != QStringLiteral("org.kde.plasma.systemmonitor")) { + return; + } + + QDir root(presetPackage.path()); + root.cdUp(); + auto *job = presetPackage.uninstall(pluginId, root.path()); + + connect(job, &KJob::finished, this, [this] () { + d->availablePresetsModel->reload(); + }); +} + +#include "moc_SensorFaceController.cpp" diff --git a/faces/SensorFaceController_p.h b/faces/SensorFaceController_p.h new file mode 100644 --- /dev/null +++ b/faces/SensorFaceController_p.h @@ -0,0 +1,67 @@ +/* + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include +#include + +class QQmlEngine; + +namespace KSysGuard { + +class SensorFaceController; + +class FacesModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum AdditionalRoles { + PluginIdRole = Qt::UserRole + 1, + }; + Q_ENUM(AdditionalRoles) + + FacesModel(QObject *parent = nullptr); + ~FacesModel() = default; + + Q_INVOKABLE void reload(); + Q_INVOKABLE QString pluginId(int row); + + QHash roleNames() const override; +}; + +class PresetsModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum AdditionalRoles { + PluginIdRole = Qt::UserRole + 1, + ConfigRole, + WritableRole + }; + Q_ENUM(AdditionalRoles) + + PresetsModel(QObject *parent = nullptr); + ~PresetsModel() = default; + + Q_INVOKABLE void reload(); + + QHash roleNames() const override; +}; +} diff --git a/faces/SensorFace_p.h b/faces/SensorFace_p.h new file mode 100644 --- /dev/null +++ b/faces/SensorFace_p.h @@ -0,0 +1,69 @@ +/* + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include + +#include "sensorfaces_export.h" + +namespace KSysGuard { + +class SensorFaceController; + +class SENSORFACES_EXPORT SensorFace : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(KSysGuard::SensorFaceController *controller READ controller CONSTANT) + Q_PROPERTY(KSysGuard::SensorFace::FormFactor formFactor READ formFactor WRITE setFormFactor NOTIFY formFactorChanged) + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged) + +public: + enum FormFactor { + Planar, + Vertical, + Horizontal + }; + Q_ENUM(FormFactor) + + SensorFace(QQuickItem *parent = nullptr); + ~SensorFace(); + + SensorFaceController *controller() const; + // Not writable from QML + void setController(SensorFaceController *controller); + + SensorFace::FormFactor formFactor() const; + void setFormFactor(SensorFace::FormFactor formFactor); + + QQuickItem * contentItem() const; + void setContentItem(QQuickItem *item); + +Q_SIGNALS: + void formFactorChanged(); + void contentItemChanged(); + +protected: + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private: + class Private; + const std::unique_ptr d; +}; +} diff --git a/faces/UsedSensorsView.qml b/faces/UsedSensorsView.qml new file mode 100644 --- /dev/null +++ b/faces/UsedSensorsView.qml @@ -0,0 +1,184 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 +import QtQml.Models 2.12 + +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kquickcontrols 2.0 + +import org.kde.ksysguard.sensors 1.0 as Sensors + +DropArea { + id: root + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: usedSensorsScroll.implicitWidth + + property alias count: usedSensorsView.count + property alias delegateComponent: delegateComponent + property var sensorIds: [] + property var sensorColors: {} + property bool showColor: true + + function appendSensor(sensorId) { + insertSensor(usedSensorsModel.count, sensorId); + } + + function insertSensor(index, sensorId) { + var color = Kirigami.Theme.highlightColor; + color.hsvHue = Math.random(); + usedSensorsModel.insert(index, {"sensorId": sensorId, "color": color.toString()}) + usedSensorsModel.save() + } + + function positionViewAtIndex(index, mode) { + usedSensorsView.positionViewAtIndex(index, mode); + } + + function load() { + usedSensorsModel.clear(); + for (var i in sensorIds) { + usedSensorsModel.append({"sensorId": sensorIds[i], "color": (sensorColors[sensorIds[i]] || "").toString()}) + } + } + + Component { + id: delegateComponent + Kirigami.SwipeListItem { + id: listItem + actions: Kirigami.Action { + icon.name: "list-remove" + text: i18n("Remove") + onTriggered: { + usedSensorsModel.remove(index, 1); + usedSensorsModel.save(); + } + } + contentItem: RowLayout { + Kirigami.ListItemDragHandle { + listItem: listItem + listView: usedSensorsView + onMoveRequested: { + usedSensorsModel.move(oldIndex, newIndex, 1) + usedSensorsModel.save(); + } + } + ColorButton { + id: textColorButton + color: model.color + visible: root.showColor + showAlphaChannel: true + onAccepted: { + usedSensorsModel.setProperty(index, "color", color.toString()); + usedSensorsModel.save(); + } + } + QQC2.Label { + Layout.fillWidth: true + text: sensor.name + Sensors.Sensor { + id: sensor + sensorId: model.sensorId + } + } + } + } + } + + onEntered: { + if (drag.formats.indexOf("application/x-ksysguard") == -1) { + drag.accepted = false; + return; + } + } + function dropIndex() { + if (!containsDrag) { + return -1; + } else if (usedSensorsView.count == 0) { + return 0; + } + + var itemHeight = usedSensorsView.contentItem.children[0].height; + var contentY = usedSensorsView.contentY + drag.y; + + return Math.max(0, Math.min(usedSensorsView.count, Math.round(contentY / itemHeight))); + } + + onDropped: insertSensor(dropIndex(), drop.getDataAsString("application/x-ksysguard")) + + QQC2.Label { + anchors.centerIn: parent + z: 2 + visible: usedSensorsView.count == 0 + text: i18n("Drop Sensors Here") + } + Rectangle { + anchors { + left: parent.left + right: parent.right + } + height: Kirigami.Units.smallSpacing + color: Kirigami.Theme.highlightColor + visible: parent.containsDrag + y: { + if (usedSensorsView.count == 0) { + return 0; + } + var itemHeight = usedSensorsView.contentItem.children[0].height; + return itemHeight * parent.dropIndex() - usedSensorsView.contentY; + } + opacity: 0.6 + z: 2 + } + QQC2.ScrollView { + id: usedSensorsScroll + anchors.fill: parent + + ListView { + id: usedSensorsView + + model: ListModel { + id: usedSensorsModel + function save() { + var ids = []; + var colors = {}; + for (var i = 0; i < count; ++i) { + ids.push(get(i).sensorId); + colors[get(i).sensorId] = get(i).color; + } + root.sensorIds = ids; + root.sensorColors = colors; + } + } + //NOTE: this row is necessary to make the drag handle work + delegate: Kirigami.DelegateRecycler { + width: usedSensorsView.width + sourceComponent: delegateComponent + } + } + Component.onCompleted: background.visible = true; + QQC2.ScrollBar.horizontal.visible: false + } +} + diff --git a/faces/facepackages/CMakeLists.txt b/faces/facepackages/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/faces/facepackages/CMakeLists.txt @@ -0,0 +1,9 @@ + +function(install_sensorchart_face name) + kpackage_install_package(${name} org.kde.ksysguard.${name} sensorfaces ksysguard) +endfunction() + +install_sensorchart_face(barchart) +install_sensorchart_face(linechart) +install_sensorchart_face(piechart) +install_sensorchart_face(textonly) diff --git a/faces/facepackages/barchart/contents/config/main.xml b/faces/facepackages/barchart/contents/config/main.xml new file mode 100644 --- /dev/null +++ b/faces/facepackages/barchart/contents/config/main.xml @@ -0,0 +1,26 @@ + + + + + + + true + + + false + + + 0 + + + 100 + + + true + + + + diff --git a/faces/facepackages/barchart/contents/faceproperties b/faces/facepackages/barchart/contents/faceproperties new file mode 100644 --- /dev/null +++ b/faces/facepackages/barchart/contents/faceproperties @@ -0,0 +1,5 @@ +[Config] +SupportsSensorsColors=true +SupportsTotalSensors=false +SupportsLowPrioritySensors=true + diff --git a/faces/facepackages/barchart/contents/ui/BarChart.qml b/faces/facepackages/barchart/contents/ui/BarChart.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/barchart/contents/ui/BarChart.qml @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * Copyrigth 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts + +Charts.BarChart { + id: chart + + readonly property int barCount: stacked ? 1 : instantiator.count + + readonly property alias sensorsModel: sensorsModel + + stacked: root.controller.faceConfiguration.barChartStacked + + spacing: Math.round(width / 20) + + yRange { + from: root.controller.faceConfiguration.rangeFrom + to: root.controller.faceConfiguration.rangeTo + automatic: root.controller.faceConfiguration.rangeAuto + } + + Sensors.SensorDataModel { + id: sensorsModel + sensors: root.controller.highPrioritySensorIds + } + + Instantiator { + id: instantiator + model: sensorsModel.sensors + delegate: Charts.ModelSource { + model: sensorsModel + roleName: "Value" + column: index + } + onObjectAdded: { + chart.insertValueSource(index, object) + } + onObjectRemoved: { + chart.removeValueSource(object) + } + } + + colorSource: root.colorSource + nameSource: Charts.ModelSource { + model: sensorsModel + roleName: "ShortName" + indexColumns: true + } +} + diff --git a/sensors/declarative/ExtendedLegend.qml b/faces/facepackages/barchart/contents/ui/CompactRepresentation.qml rename from sensors/declarative/ExtendedLegend.qml rename to faces/facepackages/barchart/contents/ui/CompactRepresentation.qml --- a/sensors/declarative/ExtendedLegend.qml +++ b/faces/facepackages/barchart/contents/ui/CompactRepresentation.qml @@ -21,50 +21,30 @@ */ import QtQuick 2.9 +import QtQuick.Controls 2.2 as QQC2 import QtQuick.Layouts 1.1 import org.kde.kirigami 2.8 as Kirigami -import org.kde.ksysguard.formatter 1.0 -import org.kde.ksysguard.sensors 1.0 +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces import org.kde.quickcharts 1.0 as Charts -import org.kde.quickcharts.controls 1.0 as ChartsControls - -ChartsControls.Legend { - id: legend - - property alias textOnlySensorIds: textOnlySensorsRepeater.model - property var sourceModel - property var colorSource - - flow: GridLayout.TopToBottom - - Layout.maximumHeight: implicitHeight - Layout.maximumWidth: parent.width - - spacing: Kirigami.Units.smallSpacing - - valueVisible: true - valueWidth: units.gridUnit * 2 - formatValue: function(input, index) { - return Formatter.formatValueShowNull(input, sourceModel.data(sourceModel.index(0, index), SensorDataModel.Unit)) - } - - Repeater { - id: textOnlySensorsRepeater - delegate: ChartsControls.LegendDelegate { - name: sensor.shortName - value: sensor.formattedValue || "" - colorVisible: false - - layoutWidth: legend.width - valueWidth: units.gridUnit * 2 - - Sensor { - id: sensor - sensorId: modelData - } +import org.kde.quickcharts.controls 1.0 as ChartControls + +Faces.SensorFace { + id: root + contentItem: ColumnLayout { + BarChart { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignCenter + } + QQC2.Label { + id: label + visible: root.formFactor == PlasmaCore.Types.Planar + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + text: oot.controller.title } } } diff --git a/faces/facepackages/barchart/contents/ui/Config.qml b/faces/facepackages/barchart/contents/ui/Config.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/barchart/contents/ui/Config.qml @@ -0,0 +1,68 @@ +/* + * Copyright 2019 Marco Martin + * Copyrigth 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 as QQC2 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +Kirigami.FormLayout { + id: root + + property alias cfg_showLegend: showSensorsLegendCheckbox.checked + property alias cfg_barChartStacked: stackedCheckbox.checked + property alias cfg_rangeAuto: rangeAutoCheckbox.checked + property alias cfg_rangeFrom: rangeFromSpin.value + property alias cfg_rangeTo: rangeToSpin.value + + QQC2.CheckBox { + id: showSensorsLegendCheckbox + text: i18n("Show Sensors Legend") + } + QQC2.CheckBox { + id: stackedCheckbox + text: i18n("Stacked Bars") + } + QQC2.CheckBox { + id: rangeAutoCheckbox + text: i18n("Automatic Data Range") + } + QQC2.SpinBox { + id: rangeFromSpin + editable: true + from: -99999 + to: 99999 + Kirigami.FormData.label: i18n("From:") + enabled: !rangeAutoCheckbox.checked + } + QQC2.SpinBox { + id: rangeToSpin + editable: true + from: -99999 + to: 99999 + Kirigami.FormData.label: i18n("To:") + enabled: !rangeAutoCheckbox.checked + } +} + diff --git a/faces/facepackages/barchart/contents/ui/FullRepresentation.qml b/faces/facepackages/barchart/contents/ui/FullRepresentation.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/barchart/contents/ui/FullRepresentation.qml @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts +import org.kde.quickcharts.controls 1.0 as ChartControls + +Faces.SensorFace { + id: root + + readonly property bool showLegend: controller.faceConfiguration.showLegend + + // Arbitrary minimumWidth to make easier to align plasmoids in a predictable way + Layout.minimumWidth: Kirigami.Units.gridUnit * 8 + + contentItem: ColumnLayout { + Kirigami.Heading { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + text: root.controller.title + visible: text.length > 0 + level: 2 + } + + BarChart { + id: compactRepresentation + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 5 * Kirigami.Units.gridUnit + Layout.preferredHeight: 8 * Kirigami.Units.gridUnit + Layout.maximumHeight: Math.max(root.width, Layout.minimumHeight) + } + + Faces.ExtendedLegend { + Layout.fillWidth: root.width < implicitWidth * 1.5 + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + visible: root.showLegend + chart: compactRepresentation + sourceModel: root.showLegend ? compactRepresentation.sensorsModel : null + colorSource: globalColorSource + sensorIds: root.showLegend ? root.controller.lowPrioritySensorIds : [] + } + + Item { + Layout.fillHeight: true + } + } +} diff --git a/faces/facepackages/barchart/metadata.desktop b/faces/facepackages/barchart/metadata.desktop new file mode 100644 --- /dev/null +++ b/faces/facepackages/barchart/metadata.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=Bar Chart +Icon=office-chart-bar + +Type=Service +X-KDE-ServiceTypes=KSysguard/SensorFace +X-KDE-ParentApp=org.kde.plasmashell +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kde@broulik.de +X-KDE-PluginInfo-License=LGPLv2+ +X-KDE-PluginInfo-Name=org.kde.ksysguard.barchart +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website=https://kde.org diff --git a/faces/facepackages/linechart/contents/config/main.xml b/faces/facepackages/linechart/contents/config/main.xml new file mode 100644 --- /dev/null +++ b/faces/facepackages/linechart/contents/config/main.xml @@ -0,0 +1,41 @@ + + + + + + + true + + + 10 + + + false + + + true + + + 0 + + + 50 + + + false + + + 0 + + + 100 + + + true + + + + diff --git a/faces/facepackages/linechart/contents/faceproperties b/faces/facepackages/linechart/contents/faceproperties new file mode 100644 --- /dev/null +++ b/faces/facepackages/linechart/contents/faceproperties @@ -0,0 +1,5 @@ +[Config] +SupportsSensorsColors=true +SupportsTotalSensors=false +SupportsLowPrioritySensors=true + diff --git a/sensors/declarative/ExtendedLegend.qml b/faces/facepackages/linechart/contents/ui/CompactRepresentation.qml copy from sensors/declarative/ExtendedLegend.qml copy to faces/facepackages/linechart/contents/ui/CompactRepresentation.qml --- a/sensors/declarative/ExtendedLegend.qml +++ b/faces/facepackages/linechart/contents/ui/CompactRepresentation.qml @@ -21,50 +21,32 @@ */ import QtQuick 2.9 +import QtQuick.Controls 2.2 as QQC2 import QtQuick.Layouts 1.1 import org.kde.kirigami 2.8 as Kirigami -import org.kde.ksysguard.formatter 1.0 -import org.kde.ksysguard.sensors 1.0 - +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces import org.kde.quickcharts 1.0 as Charts -import org.kde.quickcharts.controls 1.0 as ChartsControls - -ChartsControls.Legend { - id: legend - - property alias textOnlySensorIds: textOnlySensorsRepeater.model - property var sourceModel - property var colorSource +import org.kde.quickcharts.controls 1.0 as ChartControls - flow: GridLayout.TopToBottom +import org.kde.plasma.core 2.0 as PlasmaCore - Layout.maximumHeight: implicitHeight - Layout.maximumWidth: parent.width - - spacing: Kirigami.Units.smallSpacing - - valueVisible: true - valueWidth: units.gridUnit * 2 - formatValue: function(input, index) { - return Formatter.formatValueShowNull(input, sourceModel.data(sourceModel.index(0, index), SensorDataModel.Unit)) - } - - Repeater { - id: textOnlySensorsRepeater - delegate: ChartsControls.LegendDelegate { - name: sensor.shortName - value: sensor.formattedValue || "" - colorVisible: false - - layoutWidth: legend.width - valueWidth: units.gridUnit * 2 - - Sensor { - id: sensor - sensorId: modelData - } +Faces.SensorFace { + id: root + contentItem: ColumnLayout { + LineChart { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignCenter + } + QQC2.Label { + id: label + visible: root.formFactor == Faces.SensorFace.Planar + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + text: root.controller.title } } } + diff --git a/faces/facepackages/linechart/contents/ui/Config.qml b/faces/facepackages/linechart/contents/ui/Config.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/linechart/contents/ui/Config.qml @@ -0,0 +1,112 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 as QQC2 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +Kirigami.FormLayout { + id: root + + property alias cfg_showLegend: showSensorsLegendCheckbox.checked + property alias cfg_lineChartStacked: stackedCheckbox.checked + property alias cfg_lineChartFillOpacity: fillOpacitySpin.value + property alias cfg_lineChartSmooth: smoothCheckbox.checked + + property alias cfg_rangeAutoY: rangeAutoYCheckbox.checked + property alias cfg_rangeFromY: rangeFromYSpin.value + property alias cfg_rangeToY: rangeToYSpin.value + property alias cfg_rangeAutoX: rangeAutoXCheckbox.checked + property alias cfg_rangeFromX: rangeFromXSpin.value + property alias cfg_rangeToX: rangeToXSpin.value + + Item { + Kirigami.FormData.label: i18n("Appearance") + Kirigami.FormData.isSection: true + } + QQC2.CheckBox { + id: showSensorsLegendCheckbox + text: i18n("Show Sensors Legend") + } + QQC2.CheckBox { + id: stackedCheckbox + text: i18n("Stacked Charts") + } + QQC2.CheckBox { + id: smoothCheckbox + text: i18n("Smooth Lines") + } + QQC2.SpinBox { + id: fillOpacitySpin + Kirigami.FormData.label: i18n("Fill Opacity:") + editable: true + from: 0 + to: 100 + } + Item { + Kirigami.FormData.label: i18n("Data Ranges") + Kirigami.FormData.isSection: true + } + QQC2.CheckBox { + id: rangeAutoYCheckbox + text: i18n("Automatic Y Data Range") + } + QQC2.SpinBox { + id: rangeFromYSpin + Kirigami.FormData.label: i18n("From (Y):") + enabled: !rangeAutoYCheckbox.checked + editable: true + from: -99999 + to: 99999 + } + QQC2.SpinBox { + id: rangeToYSpin + editable: true + from: -99999 + to: 99999 + Kirigami.FormData.label: i18n("To (Y):") + enabled: !rangeAutoYCheckbox.checked + } + QQC2.CheckBox { + id: rangeAutoXCheckbox + text: i18n("Automatic X Data Range") + } + QQC2.SpinBox { + id: rangeFromXSpin + editable: true + from: -99999 + to: 99999 + Kirigami.FormData.label: i18n("From (X):") + enabled: !rangeAutoXCheckbox.checked + } + QQC2.SpinBox { + id: rangeToXSpin + editable: true + from: -99999 + to: 99999 + Kirigami.FormData.label: i18n("To (X):") + enabled: !rangeAutoXCheckbox.checked + } +} + diff --git a/faces/facepackages/linechart/contents/ui/FullRepresentation.qml b/faces/facepackages/linechart/contents/ui/FullRepresentation.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/linechart/contents/ui/FullRepresentation.qml @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts + +Faces.SensorFace { + id: root + readonly property bool showLegend: controller.faceConfiguration.showLegend + // Arbitrary minimumWidth to make easier to align plasmoids in a predictable way + Layout.minimumWidth: Kirigami.Units.gridUnit * 8 + + contentItem: ColumnLayout { + Kirigami.Heading { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + text: root.controller.title + visible: text.length > 0 + level: 2 + } + + LineChart { + id: compactRepresentation + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 3 * Kirigami.Units.gridUnit + Layout.preferredHeight: 5 * Kirigami.Units.gridUnit + } + + Faces.ExtendedLegend { + Layout.fillWidth: root.width < implicitWidth * 1.5 + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + visible: root.showLegend + chart: compactRepresentation + sourceModel: root.showLegend ? compactRepresentation.sensorsModel : null + colorSource: root.colorSource + sensorIds: root.showLegend ? root.controller.lowPrioritySensorIds : [] + } + } +} diff --git a/faces/facepackages/linechart/contents/ui/LineChart.qml b/faces/facepackages/linechart/contents/ui/LineChart.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/linechart/contents/ui/LineChart.qml @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces +import org.kde.quickcharts 1.0 as Charts + +Charts.LineChart { + id: chart + + //property var sensors: root.controller.highPrioritySensorIds + + readonly property alias sensorsModel: sensorsModel + + Layout.minimumWidth: Kirigami.Units.gridUnit * 8 + + direction: Charts.XYChart.ZeroAtEnd + + fillOpacity: root.controller.faceConfiguration.lineChartFillOpacity / 100 + stacked: root.controller.faceConfiguration.lineChartStacked + smooth: root.controller.faceConfiguration.lineChartSmooth + + //TODO: Have a central heading here too? + //TODO: Have a plasmoid config value for line thickness? + + xRange { + from: root.controller.faceConfiguration.rangeFromX + to: root.controller.faceConfiguration.rangeToX + automatic: root.controller.faceConfiguration.rangeAutoX + } + yRange { + from: root.controller.faceConfiguration.rangeFromY + to: root.controller.faceConfiguration.rangeToY + automatic: root.controller.faceConfiguration.rangeAutoY + } + + Sensors.SensorDataModel { + id: sensorsModel + sensors: root.controller.highPrioritySensorIds + } + + Instantiator { + model: sensorsModel.sensors + delegate: Charts.ModelHistorySource { + model: sensorsModel + column: index + row: 0 + roleName: "Value" + maximumHistory: 50 + } + onObjectAdded: { + chart.insertValueSource(index, object) + } + onObjectRemoved: { + chart.removeValueSource(object) + } + } + + colorSource: root.colorSource + nameSource: Charts.ModelSource { + model: sensorsModel + roleName: "ShortName" + indexColumns: true + } +} + diff --git a/faces/facepackages/linechart/metadata.desktop b/faces/facepackages/linechart/metadata.desktop new file mode 100644 --- /dev/null +++ b/faces/facepackages/linechart/metadata.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=Line Chart +Icon=office-chart-line + +Type=Service +X-KDE-ServiceTypes=KSysguard/SensorFace +X-KDE-ParentApp=org.kde.plasmashell +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=LGPLv2+ +X-KDE-PluginInfo-Name=org.kde.ksysguard.linechart +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website=https://kde.org diff --git a/faces/facepackages/piechart/contents/config/main.xml b/faces/facepackages/piechart/contents/config/main.xml new file mode 100644 --- /dev/null +++ b/faces/facepackages/piechart/contents/config/main.xml @@ -0,0 +1,32 @@ + + + + + + + -180 + + + 360 + + + true + + + true + + + 0 + + + 100 + + + true + + + + diff --git a/faces/facepackages/piechart/contents/faceproperties b/faces/facepackages/piechart/contents/faceproperties new file mode 100644 --- /dev/null +++ b/faces/facepackages/piechart/contents/faceproperties @@ -0,0 +1,5 @@ +[Config] +SupportsSensorsColors=true +SupportsTotalSensors=true +SupportsLowPrioritySensors=true + diff --git a/sensors/declarative/ExtendedLegend.qml b/faces/facepackages/piechart/contents/ui/CompactRepresentation.qml copy from sensors/declarative/ExtendedLegend.qml copy to faces/facepackages/piechart/contents/ui/CompactRepresentation.qml --- a/sensors/declarative/ExtendedLegend.qml +++ b/faces/facepackages/piechart/contents/ui/CompactRepresentation.qml @@ -21,50 +21,30 @@ */ import QtQuick 2.9 +import QtQuick.Controls 2.2 as QQC2 import QtQuick.Layouts 1.1 import org.kde.kirigami 2.8 as Kirigami -import org.kde.ksysguard.formatter 1.0 -import org.kde.ksysguard.sensors 1.0 +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces import org.kde.quickcharts 1.0 as Charts -import org.kde.quickcharts.controls 1.0 as ChartsControls - -ChartsControls.Legend { - id: legend - - property alias textOnlySensorIds: textOnlySensorsRepeater.model - property var sourceModel - property var colorSource - - flow: GridLayout.TopToBottom - - Layout.maximumHeight: implicitHeight - Layout.maximumWidth: parent.width - - spacing: Kirigami.Units.smallSpacing - - valueVisible: true - valueWidth: units.gridUnit * 2 - formatValue: function(input, index) { - return Formatter.formatValueShowNull(input, sourceModel.data(sourceModel.index(0, index), SensorDataModel.Unit)) - } - - Repeater { - id: textOnlySensorsRepeater - delegate: ChartsControls.LegendDelegate { - name: sensor.shortName - value: sensor.formattedValue || "" - colorVisible: false - - layoutWidth: legend.width - valueWidth: units.gridUnit * 2 - - Sensor { - id: sensor - sensorId: modelData - } +import org.kde.quickcharts.controls 1.0 as ChartControls + +Faces.SensorFace { + id: root + contentItem: ColumnLayout { + PieChart { + Layout.fillWidth: true + Layout.fillHeight: !label.visible + Layout.alignment: Qt.AlignCenter + } + QQC2.Label { + id: label + visible: root.formFactor == Faces.SensorFace.Planar + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + text: root.controller.title } } } diff --git a/faces/facepackages/piechart/contents/ui/Config.qml b/faces/facepackages/piechart/contents/ui/Config.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/piechart/contents/ui/Config.qml @@ -0,0 +1,94 @@ +/* + * Copyright 2019 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 as QQC2 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +Kirigami.FormLayout { + id: root + + property alias cfg_showLegend: showSensorsLegendCheckbox.checked + property alias cfg_fromAngle: fromAngleSpin.value + property alias cfg_toAngle: toAngleSpin.value + property alias cfg_smoothEnds: smoothEndsCheckbox.checked + property alias cfg_rangeAuto: rangeAutoCheckbox.checked + property alias cfg_rangeFrom: rangeFromSpin.value + property alias cfg_rangeTo: rangeToSpin.value + + QQC2.CheckBox { + id: showSensorsLegendCheckbox + text: i18n("Show Sensors Legend") + } + QQC2.SpinBox { + id: fromAngleSpin + Kirigami.FormData.label: i18n("Start from Angle") + from: -180 + to: 180 + editable: true + textFromValue: function(value, locale) { + return i18nc("angle degrees", "%1°", value + 180); + } + valueFromText: function(text, locale) { + return Number.fromLocaleString(locale, text.replace(i18nc("angle degrees", "°"), "")) - 180; + } + } + QQC2.SpinBox { + id: toAngleSpin + Kirigami.FormData.label: i18n("Total Pie Angle") + from: 0 + to: 360 + editable: true + textFromValue: function(value, locale) { + return i18nc("angle", "%1°", value); + } + valueFromText: function(text, locale) { + return Number.fromLocaleString(locale, text.replace(i18nc("angle degrees", "°"), "")); + } + } + QQC2.CheckBox { + id: smoothEndsCheckbox + text: i18n("Rounded Lines") + } + + QQC2.CheckBox { + id: rangeAutoCheckbox + text: i18n("Automatic Data Range") + } + QQC2.SpinBox { + id: rangeFromSpin + editable: true + Kirigami.FormData.label: i18n("From:") + enabled: !rangeAutoCheckbox.checked + } + QQC2.SpinBox { + id: rangeToSpin + from: -99999 + to: 99999 + editable: true + Kirigami.FormData.label: i18n("To:") + enabled: !rangeAutoCheckbox.checked + } +} + diff --git a/faces/facepackages/piechart/contents/ui/FullRepresentation.qml b/faces/facepackages/piechart/contents/ui/FullRepresentation.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/piechart/contents/ui/FullRepresentation.qml @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.4 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts +import org.kde.quickcharts.controls 1.0 as ChartsControls + +Faces.SensorFace { + id: root + readonly property bool showLegend: controller.faceConfiguration.showLegend + + contentItem: ColumnLayout { + // Arbitrary minimumWidth to make easier to align plasmoids in a predictable way + Layout.minimumWidth: Kirigami.Units.gridUnit * 8 + Layout.preferredWidth: Kirigami.Units.gridUnit * 8 + + spacing: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + Kirigami.Heading { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + text: root.controller.title + visible: text.length > 0 + level: 2 + } + + PieChart { + id: compactRepresentation + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 5 * Kirigami.Units.gridUnit + Layout.preferredHeight: 8 * Kirigami.Units.gridUnit + Layout.maximumHeight: Math.max(root.width, Layout.minimumHeight) + } + + Faces.ExtendedLegend { + Layout.fillWidth: root.width < implicitWidth * 1.5 + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + visible: root.showLegend + chart: compactRepresentation.chart + sourceModel: root.showLegend ? compactRepresentation.sensorsModel : null + colorSource: root.colorSource + sensorIds: root.showLegend ? root.controller.lowPrioritySensorIds : [] + } + + Item { + Layout.fillHeight: true + } + } +} diff --git a/faces/facepackages/piechart/contents/ui/PieChart.qml b/faces/facepackages/piechart/contents/ui/PieChart.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/piechart/contents/ui/PieChart.qml @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts +import org.kde.quickcharts.controls 1.0 as ChartControls + + +ChartControls.PieChartControl { + id: chart + + property alias headingSensor: sensor.sensorId + property alias sensors: sensorsModel.sensors + property alias sensorsModel: sensorsModel + + Layout.minimumWidth: root.formFactor != Faces.SensorFace.Vertical ? Kirigami.Units.gridUnit * 4 : Kirigami.Units.gridUnit + Layout.minimumHeight: root.formFactor == Faces.SensorFace.Vertical ? width : Kirigami.Units.gridUnit + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + chart.smoothEnds: root.controller.faceConfiguration.smoothEnds + chart.fromAngle: root.controller.faceConfiguration.fromAngle + chart.toAngle: root.controller.faceConfiguration.toAngle + + range { + from: root.controller.faceConfiguration.rangeFrom + to: root.controller.faceConfiguration.rangeTo + automatic: root.controller.faceConfiguration.rangeAuto + } + + chart.backgroundColor: Qt.rgba(0.0, 0.0, 0.0, 0.2) + + text: sensor.formattedValue + valueSources: Charts.ModelSource { + model: Sensors.SensorDataModel { + id: sensorsModel + sensors: root.controller.highPrioritySensorIds + } + roleName: "Value" + indexColumns: true + } + chart.nameSource: Charts.ModelSource { + roleName: "ShortName"; + model: valueSources[0].model; + indexColumns: true + } + chart.colorSource: root.colorSource + + Sensors.Sensor { + id: sensor + sensorId: root.controller.totalSensors.length > 0 ? root.controller.totalSensors[0] : "" + } +} + diff --git a/faces/facepackages/piechart/metadata.desktop b/faces/facepackages/piechart/metadata.desktop new file mode 100644 --- /dev/null +++ b/faces/facepackages/piechart/metadata.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=Pie Chart +Icon=office-chart-pie + +Type=Service +X-KDE-ServiceTypes=KSysguard/SensorFace +X-KDE-ParentApp=org.kde.plasmashell +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=LGPLv2+ +X-KDE-PluginInfo-Name=org.kde.ksysguard.piechart +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website=https://kde.org diff --git a/faces/facepackages/textonly/contents/faceproperties b/faces/facepackages/textonly/contents/faceproperties new file mode 100644 --- /dev/null +++ b/faces/facepackages/textonly/contents/faceproperties @@ -0,0 +1,5 @@ +[Config] +SupportsSensorsColors=true +SupportsTotalSensors=false +SupportsLowPrioritySensors=true + diff --git a/sensors/declarative/ExtendedLegend.qml b/faces/facepackages/textonly/contents/ui/CompactRepresentation.qml copy from sensors/declarative/ExtendedLegend.qml copy to faces/facepackages/textonly/contents/ui/CompactRepresentation.qml --- a/sensors/declarative/ExtendedLegend.qml +++ b/faces/facepackages/textonly/contents/ui/CompactRepresentation.qml @@ -2,7 +2,6 @@ * Copyright 2019 Marco Martin * Copyright 2019 David Edmundson * Copyright 2019 Arjen Hiemstra - * Copyright 2019 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as @@ -25,46 +24,41 @@ import org.kde.kirigami 2.8 as Kirigami -import org.kde.ksysguard.formatter 1.0 -import org.kde.ksysguard.sensors 1.0 +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces import org.kde.quickcharts 1.0 as Charts import org.kde.quickcharts.controls 1.0 as ChartsControls -ChartsControls.Legend { - id: legend - property alias textOnlySensorIds: textOnlySensorsRepeater.model - property var sourceModel - property var colorSource +Faces.SensorFace { + id: root - flow: GridLayout.TopToBottom + Layout.minimumWidth: root.formFactor == Faces.SensorFace.Vertical ? Kirigami.Units.gridUnit : Kirigami.Units.gridUnit * 2 - Layout.maximumHeight: implicitHeight - Layout.maximumWidth: parent.width + contentItem: ColumnLayout { - spacing: Kirigami.Units.smallSpacing + Repeater { + model: root.controller.highPrioritySensorIds - valueVisible: true - valueWidth: units.gridUnit * 2 - formatValue: function(input, index) { - return Formatter.formatValueShowNull(input, sourceModel.data(sourceModel.index(0, index), SensorDataModel.Unit)) - } + ChartsControls.LegendDelegate { + Layout.fillWidth: true - Repeater { - id: textOnlySensorsRepeater - delegate: ChartsControls.LegendDelegate { - name: sensor.shortName - value: sensor.formattedValue || "" - colorVisible: false + name: sensor.shortName + value: sensor.formattedValue + colorVisible: false + colorWidth: 0 - layoutWidth: legend.width - valueWidth: units.gridUnit * 2 + layoutWidth: root.width + valueWidth: Kirigami.Units.gridUnit * 2 - Sensor { - id: sensor - sensorId: modelData + Sensors.Sensor { + id: sensor + sensorId: modelData + } } } + + Item { Layout.fillWidth: true; Layout.fillHeight: true } } } diff --git a/faces/facepackages/textonly/contents/ui/FullRepresentation.qml b/faces/facepackages/textonly/contents/ui/FullRepresentation.qml new file mode 100644 --- /dev/null +++ b/faces/facepackages/textonly/contents/ui/FullRepresentation.qml @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts +import org.kde.quickcharts.controls 1.0 as ChartsControls + +Faces.SensorFace { + id: root + + // Arbitrary minimumWidth to make easier to align plasmoids in a predictable way + Layout.minimumWidth: Kirigami.Units.gridUnit * 8 + + contentItem: ColumnLayout { + + Kirigami.Heading { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + text: root.controller.title + visible: text.length > 0 + level: 2 + } + + Item { Layout.fillWidth: true; Layout.fillHeight: true } + + Repeater { + model: root.controller.highPrioritySensorIds.concat(root.controller.lowPrioritySensorIds) + + ChartsControls.LegendDelegate { + readonly property bool isTextOnly: index >= root.controller.highPrioritySensorIds.length + + Layout.fillWidth: true + Layout.minimumHeight: isTextOnly ? 0 : implicitHeight + + name: sensor.shortName + value: sensor.formattedValue + colorVisible: !isTextOnly + color: !isTextOnly ? globalColorSource.array[index] : "transparent" + + layoutWidth: root.width + valueWidth: Kirigami.Units.gridUnit * 2 + + Sensors.Sensor { + id: sensor + sensorId: modelData + } + } + } + + Item { Layout.fillWidth: true; Layout.fillHeight: true } + } +} diff --git a/faces/facepackages/textonly/metadata.desktop b/faces/facepackages/textonly/metadata.desktop new file mode 100644 --- /dev/null +++ b/faces/facepackages/textonly/metadata.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=Text Only +Icon=view-list-text + +Type=Service +X-KDE-ServiceTypes=KSysguard/SensorFace +X-KDE-ParentApp=org.kde.plasmashell +X-KDE-PluginInfo-Author=Marco Martin +X-KDE-PluginInfo-Email=mart@kde.org +X-KDE-PluginInfo-License=LGPLv2+ +X-KDE-PluginInfo-Name=org.kde.ksysguard.textonly +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Website=https://kde.org diff --git a/faces/import/CMakeLists.txt b/faces/import/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/faces/import/CMakeLists.txt @@ -0,0 +1,8 @@ +include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/..) + +add_library(FacesPlugin SHARED FacesPlugin.cpp) + +target_link_libraries(FacesPlugin Qt5::Qml KSysGuard::Sensors KSysGuard::SensorFaces KF5::ProcessCore KF5::Package KF5::ConfigCore KF5::ConfigGui KF5::Declarative) + +install(TARGETS FacesPlugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/faces) +install(FILES qmldir ExtendedLegend.qml SensorFace.qml DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/faces) diff --git a/sensors/declarative/ExtendedLegend.qml b/faces/import/ExtendedLegend.qml rename from sensors/declarative/ExtendedLegend.qml rename to faces/import/ExtendedLegend.qml --- a/sensors/declarative/ExtendedLegend.qml +++ b/faces/import/ExtendedLegend.qml @@ -34,7 +34,7 @@ ChartsControls.Legend { id: legend - property alias textOnlySensorIds: textOnlySensorsRepeater.model + property alias sensorIds: sensorsRepeater.model property var sourceModel property var colorSource @@ -52,7 +52,7 @@ } Repeater { - id: textOnlySensorsRepeater + id: sensorsRepeater delegate: ChartsControls.LegendDelegate { name: sensor.shortName value: sensor.formattedValue || "" diff --git a/faces/import/FacesPlugin.h b/faces/import/FacesPlugin.h new file mode 100644 --- /dev/null +++ b/faces/import/FacesPlugin.h @@ -0,0 +1,32 @@ +/* + Copyright (C) 2020 Arjen Hiemstra + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include + +class FacesPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/faces/import/FacesPlugin.cpp b/faces/import/FacesPlugin.cpp new file mode 100644 --- /dev/null +++ b/faces/import/FacesPlugin.cpp @@ -0,0 +1,42 @@ +/* + Copyright (C) 2020 Arjen Hiemstra + Copyright (C) 2020 Marco Martin + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "FacesPlugin.h" + +#include "SensorDataModel.h" +#include "SensorTreeModel.h" +#include "Sensor.h" +#include "SensorFace_p.h" +#include "SensorFaceController.h" + +#include + +#include + +using namespace KSysGuard; + +void FacesPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.ksysguard.faces")); + + qmlRegisterType(uri, 1, 0, "AbstractSensorFace"); + qmlRegisterUncreatableType(uri, 1, 0, "SensorFaceController", QStringLiteral("It's not possible to create objects of type SensorFaceController")); + qmlRegisterAnonymousType(uri, 1); +} diff --git a/faces/import/SensorFace.qml b/faces/import/SensorFace.qml new file mode 100644 --- /dev/null +++ b/faces/import/SensorFace.qml @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.4 + +import org.kde.kirigami 2.8 as Kirigami +import org.kde.quickcharts 1.0 as Charts +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +Faces.AbstractSensorFace { + id: root + implicitWidth: contentItem.implicitWidth + implicitHeight: contentItem.implicitHeight + Layout.minimumWidth: contentItem.Layout.minimumWidth + Layout.minimumHeight: contentItem.Layout.minimumHeight + Layout.preferredWidth: contentItem.Layout.preferredWidth + Layout.preferredHeight: contentItem.Layout.preferredHeight + Layout.maximumWidth: contentItem.Layout.maximumWidth + Layout.maximumHeight: contentItem.Layout.maximumHeight + + property alias colorSource: colorSource + + Charts.MapProxySource { + id: colorSource + source: Charts.ArraySource { + array: root.controller.highPrioritySensorIds + } + map: root.controller.sensorColors + } + Charts.ColorGradientSource { + baseColor: Kirigami.Theme.highlightColor + itemCount: root.controller.highPrioritySensorIds.length + + onItemCountChanged: generate() + Component.onCompleted: generate() + + function generate() { + //var colors = colorSource.colors; + var savedColors = root.controller.sensorColors; + for (var i = 0; i < root.controller.highPrioritySensorIds.length; ++i) { + if (savedColors.length <= i) { + savedColors[root.controller.highPrioritySensorIds[i]] = colors[i]; + } else { + // Use the darker trick to make Qt validate the scring as a valid color; + var currentColor = Qt.darker(savedColors[i], 1); + if (!currentColor) { + savedColors[root.controller.highPrioritySensorIds[i]] = colors[i]; + } else { + savedColors[root.controller.highPrioritySensorIds[i]] = currentColor; + } + } + } + root.controller.sensorColors = savedColors; + } + } +} diff --git a/faces/import/qmldir b/faces/import/qmldir new file mode 100644 --- /dev/null +++ b/faces/import/qmldir @@ -0,0 +1,4 @@ +module org.kde.ksysguard.faces +plugin FacesPlugin +ExtendedLegend 1.0 ExtendedLegend.qml +SensorFace 1.0 SensorFace.qml diff --git a/faces/packagestructure/CMakeLists.txt b/faces/packagestructure/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/faces/packagestructure/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library(sensorface_packagestructure MODULE sensorfacepackage.cpp) + +target_link_libraries(sensorface_packagestructure PRIVATE KF5::Package KF5::I18n) + +install(TARGETS sensorface_packagestructure DESTINATION ${KDE_INSTALL_PLUGINDIR}/kpackage/packagestructure) diff --git a/faces/packagestructure/sensorface-packagestructure.json b/faces/packagestructure/sensorface-packagestructure.json new file mode 100644 --- /dev/null +++ b/faces/packagestructure/sensorface-packagestructure.json @@ -0,0 +1,20 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin" + }, + { + "Email": "aseigo@kde.org", + "Name": "Aaron Seigo" + } + ], + "Id": "KSysguard/SensorFace", + "Name": "Face for the KSysguard Sensor Face", + "ServiceTypes": [ + "KPackage/PackageStructure" + ], + "Version": "1" + } +} diff --git a/faces/packagestructure/sensorfacepackage.cpp b/faces/packagestructure/sensorfacepackage.cpp new file mode 100644 --- /dev/null +++ b/faces/packagestructure/sensorfacepackage.cpp @@ -0,0 +1,55 @@ +/****************************************************************************** +* Copyright 2007-2009 by Aaron Seigo * +* Copyright 2020 by Marco Martin * +* * +* This library is free software; you can redistribute it and/or * +* modify it under the terms of the GNU Library General Public * +* License as published by the Free Software Foundation; either * +* version 2 of the License, or (at your option) any later version. * +* * +* This library is distributed in the hope that it will be useful, * +* but WITHOUT ANY WARRANTY; without even the implied warranty of * +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +* Library General Public License for more details. * +* * +* You should have received a copy of the GNU Library General Public License * +* along with this library; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#include +#include +#include + +class SensorFacePackage : public KPackage::PackageStructure +{ + Q_OBJECT +public: + SensorFacePackage(QObject *parent = nullptr, const QVariantList &args = QVariantList()) : KPackage::PackageStructure(parent, args) {} + + void initPackage(KPackage::Package *package) override + { + package->setDefaultPackageRoot(QStringLiteral("ksysguard/sensorfaces")); + + package->addDirectoryDefinition("ui", QStringLiteral("ui"), i18n("User Interface")); + + package->addFileDefinition("CompactRepresentation", QStringLiteral("ui/CompactRepresentation.qml"), i18n("The compact representation of the sensors plasmoid when collapsed, for instance in a panel.")); + package->setRequired("CompactRepresentation", true); + + package->addFileDefinition("FullRepresentation", QStringLiteral("ui/FullRepresentation.qml"), i18n("The representation of the plasmoid when it's fully expanded.")); + package->setRequired("FullRepresentation", true); + + package->addFileDefinition("ConfigUI", QStringLiteral("ui/Config.qml"), i18n("The optional configuration page for this face.")); + + package->addDirectoryDefinition("config", QStringLiteral("config"), i18n("Configuration support")); + package->addFileDefinition("mainconfigxml", QStringLiteral("config/main.xml"), i18n("KConfigXT xml file for face-specific configuration options.")); + + package->addFileDefinition("FaceProperties", QStringLiteral("faceproperties"), i18n("The configuration file that describes face properties and capabilities.")); + package->setRequired("FaceProperties", true); + } +}; + +K_EXPORT_KPACKAGE_PACKAGE_WITH_JSON(SensorFacePackage, "sensorface-packagestructure.json") + +#include "sensorfacepackage.moc" diff --git a/faces/resources.qrc b/faces/resources.qrc new file mode 100644 --- /dev/null +++ b/faces/resources.qrc @@ -0,0 +1,8 @@ + + + ConfigAppearance.qml + ConfigSensors.qml + FaceDetailsConfig.qml + UsedSensorsView.qml + + diff --git a/processcore/extended_process_list.cpp b/processcore/extended_process_list.cpp --- a/processcore/extended_process_list.cpp +++ b/processcore/extended_process_list.cpp @@ -108,8 +108,8 @@ auto userNameSensor = new ProcessSensor( this, QStringLiteral("username"), i18n("Username"), [this](KSysGuard::Process *p) { const K_UID uid = p->uid(); - auto userIt = d->m_userCache.constFind(uid); - if (userIt == d->m_userCache.constEnd()) { + auto userIt = d->m_userCache.find(uid); + if (userIt == d->m_userCache.end()) { userIt = d->m_userCache.insert(uid, KUser(uid)); } return userIt->loginName(); @@ -123,8 +123,8 @@ if (uid == 65534) { // special value meaning nobody return false; } - auto userIt = d->m_userCache.constFind(uid); - if (userIt == d->m_userCache.constEnd()) { + auto userIt = d->m_userCache.find(uid); + if (userIt == d->m_userCache.end()) { userIt = d->m_userCache.insert(uid, KUser(uid)); } diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt --- a/sensors/CMakeLists.txt +++ b/sensors/CMakeLists.txt @@ -43,11 +43,15 @@ target_link_libraries(Sensors PUBLIC Qt5::Qml + Qt5::Quick KSysGuard::Formatter PRIVATE Qt5::Core Qt5::DBus KF5::I18n + KF5::ConfigCore + KF5::ConfigGui + KF5::Declarative ) set_target_properties(Sensors PROPERTIES diff --git a/sensors/SensorQuery.cpp b/sensors/SensorQuery.cpp --- a/sensors/SensorQuery.cpp +++ b/sensors/SensorQuery.cpp @@ -124,7 +124,7 @@ } return; } - auto regexp = QRegularExpression{QRegularExpression::wildcardToRegularExpression(path)}; + auto regexp = QRegularExpression{path}; const auto sensorIds = reply.value().keys(); for (auto id : sensorIds) { diff --git a/sensors/declarative/CMakeLists.txt b/sensors/declarative/CMakeLists.txt --- a/sensors/declarative/CMakeLists.txt +++ b/sensors/declarative/CMakeLists.txt @@ -5,4 +5,4 @@ target_link_libraries(SensorsPlugin Qt5::Qml KSysGuard::Sensors) install(TARGETS SensorsPlugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/sensors) -install(FILES qmldir ExtendedLegend.qml DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/sensors) +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/sensors) diff --git a/sensors/declarative/qmldir b/sensors/declarative/qmldir --- a/sensors/declarative/qmldir +++ b/sensors/declarative/qmldir @@ -1,3 +1,2 @@ module org.kde.ksysguard.sensors plugin SensorsPlugin -ExtendedLegend 1.0 ExtendedLegend.qml