Browse Source

Massive GUI overhaul. Ported everything to Kirigami2. Also fixed profiles.

Malte Veerman 6 years ago
parent
commit
0554d6c785

+ 2 - 2
CMakeLists.txt

@@ -44,13 +44,13 @@ include(FindPkgConfig)
 
 #Find Qt5
 find_package(Qt5Core REQUIRED)
-find_package(Qt5 5.10 COMPONENTS Quick QuickControls2)
+find_package(Qt5 5.8 COMPONENTS Quick QuickControls2)
 set_package_properties(Qt5Quick PROPERTIES TYPE RUNTIME PURPOSE "Needed by the QML parts")
 set_package_properties(Qt5QuickControls2 PROPERTIES TYPE RUNTIME PURPOSE "Needed by the QML parts")
 
 #Find KF5
 find_package(KF5 COMPONENTS I18n REQUIRED)
-find_package(KF5 COMPONENTS Kirigami2)
+find_package(KF5 5.37 COMPONENTS Kirigami2)
 set_package_properties(KF5Kirigami2 PROPERTIES TYPE RUNTIME PURPOSE "Needed by the QML parts")
 
 if(${KF5_VERSION} VERSION_GREATER_EQUAL 5.33.0)

+ 2 - 2
README.md

@@ -14,11 +14,11 @@ To compile the additional KCM set the cmake option -DBUILD_KCM=on.
 The KCM is only build, if the -DNO_SYSTEMD option is unset or set to false.
 
 # Requirements
-* Qt5: Base/Core, Widgets, GUI, QML, Quick
+* Qt5: Base/Core, Widgets, GUI, QML, Quick, QuickControls2
 * KF5: I18n, Auth, Config, Package, Declarative, CoreAddons, DBusAddons, KCMUtils, Extra-Cmake-Modules, Kirigami2, Notifications
 
 ## Debian/Ubuntu command to install the build requirements:
-`sudo apt-get install libkf5config-dev libkf5auth-dev libkf5package-dev libkf5declarative-dev libkf5coreaddons-dev libkf5dbusaddons-dev libkf5kcmutils-dev libkf5i18n-dev libqt5core5a libqt5widgets5 libqt5gui5 libqt5qml5 extra-cmake-modules qtbase5-dev kirigami2-dev libkf5kirigami2-5 libkf5notifications-dev qml-module-org-kde-kirigami2 qml-module-qtquick-dialogs qml-module-qtquick-controls cmake build-essential`
+`sudo apt-get install libkf5config-dev libkf5auth-dev libkf5package-dev libkf5declarative-dev libkf5coreaddons-dev libkf5dbusaddons-dev libkf5kcmutils-dev libkf5i18n-dev libqt5core5a libqt5widgets5 libqt5gui5 libqt5qml5 extra-cmake-modules qtbase5-dev kirigami2-dev libkf5kirigami2-5 libkf5notifications-dev qml-module-org-kde-kirigami2 qml-module-qtquick-dialogs qml-module-qtquick-controls2 cmake build-essential`
 
 **Note:** This was tested on KDE Neon User Edition 5.13, which is based on Ubuntu 18.04 LTS (Debian 9 Stretch/Sid).
 

+ 76 - 105
fancontrol-gui/package/contents/ui/Application.qml

@@ -18,10 +18,10 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.10
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Gui 1.0 as Gui
 import Fancontrol.Qml 1.0 as Fancontrol
 
@@ -29,6 +29,9 @@ import Fancontrol.Qml 1.0 as Fancontrol
 Kirigami.ApplicationWindow {
     id: window
 
+    property string leftPage
+    property QtObject fan: Fancontrol.Base.pwmFanModel.fans.length > 0 ? Fancontrol.Base.pwmFanModel.fans[0] : null
+
     function showWindow() {
         window.show()
         window.raise()
@@ -36,9 +39,16 @@ Kirigami.ApplicationWindow {
     }
 
     title: i18n("Fancontrol-GUI")
-    width: 1024
-    height: 768
-    color: palette.window
+    reachableModeEnabled: false
+    pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 30
+
+    onLeftPageChanged: {
+        window.pageStack.clear();
+        if (leftPage)
+            window.pageStack.push(Qt.resolvedUrl(leftPage));
+    }
+
+    onWideScreenChanged: drawer.drawerOpen = wideScreen
 
     onClosing: {
         windowConfig.save(window);
@@ -53,82 +63,81 @@ Kirigami.ApplicationWindow {
         Fancontrol.Base.load();
         windowConfig.restore(window);
         window.visible = !Fancontrol.Base.startMinimized;
+        leftPage = "SensorsTab.qml";
     }
 
-    header: ToolBar {
-        RowLayout {
-            anchors.fill: parent
+    globalDrawer: Kirigami.GlobalDrawer {
+        id: drawer
 
-            ToolButton {
-                action: applyAction
-                display: AbstractButton.IconOnly
-            }
-            ToolButton {
-                action: resetAction
-                display: AbstractButton.IconOnly
-            }
-            Loader {
-                active: !!Fancontrol.Base.systemdCom
-
-                sourceComponent: ToolButton {
-                    action: startAction
-                    display: AbstractButton.IconOnly
-                }
-            }
-            Loader {
-                active: !!Fancontrol.Base.systemdCom
+        width: Kirigami.Units.gridUnit * 10
+        modal: !window.wideScreen
+        handleVisible: !window.wideScreen
+        resetMenuOnTriggered: false
 
-                sourceComponent: ToolButton {
-                    action: stopAction
-                    display: AbstractButton.IconOnly
-                }
+        function populateFans(fans) {
+            for (var i=fansAction.children.length-1; i>=0; i--) {
+                fansAction.children[i].destroy();
             }
-            Item {
-                Layout.fillWidth: true
+            var actions = [];
+            for (var i=0; i<fans.length; i++) {
+                var action = fanActionComponent.createObject(fansAction, { "fan": fans[i] });
+                actions.push(action);
             }
+            fansAction.children = actions;
         }
-    }
 
-    ColumnLayout {
-        anchors.fill: parent
-        spacing: 5
+        Component {
+            id: fanActionComponent
 
-        TabBar {
-            id: tabBar
+            Kirigami.Action {
+                property QtObject fan
 
-            Layout.fillWidth: true
+                text: fan.name
+                checked: window.fan === fan
 
-            TabButton {
-                text: i18n("Sensors")
-                width: implicitWidth
-            }
-            TabButton {
-                text: i18n("PwmFans")
-                width: implicitWidth
-            }
-            TabButton {
-                text: i18n("Configfile")
-                width: implicitWidth
-            }
-            TabButton {
-                text: i18n("Settings")
-                width: implicitWidth
+                onTriggered: window.fan = fan
             }
         }
 
-        StackLayout {
-            Layout.fillWidth: true
-            Layout.fillHeight: true
-            currentIndex: tabBar.currentIndex
-            anchors.margins: Kirigami.Units.smallSpacing
+        Component.onCompleted: populateFans(Fancontrol.Base.pwmFanModel.fans)
 
-            SensorsTab {}
-            PwmFansTab {}
-            ConfigfileTab {}
-            SettingsTab {}
+        Connections {
+            target: Fancontrol.Base.pwmFanModel
+            onFansChanged: populateFans(Fancontrol.Base.pwmFanModel.fans)
         }
+
+        actions: [
+            Kirigami.Action {
+                text: i18n("Sensors")
+                checked: window.leftPage === "SensorsTab.qml"
+
+                onTriggered: window.leftPage = "SensorsTab.qml"
+            },
+            Kirigami.Action {
+                id: fansAction
+
+                text: i18n("Fans")
+                checked: window.leftPage === "PwmFansTab.qml"
+
+                onTriggered: window.leftPage = "PwmFansTab.qml"
+            },
+            Kirigami.Action {
+                text: i18n("Configfile")
+                checked: window.leftPage === "ConfigfileTab.qml"
+
+                onTriggered: window.leftPage = "ConfigfileTab.qml"
+            },
+            Kirigami.Action {
+                text: i18n("Settings")
+                checked: window.leftPage === "SettingsTab.qml"
+
+                onTriggered: window.leftPage = "SettingsTab.qml"
+            }
+        ]
     }
 
+    contextDrawer: Kirigami.ContextDrawer {}
+
     Loader {
         id: trayLoader
 
@@ -153,6 +162,7 @@ Kirigami.ApplicationWindow {
         id: errorDialog
 
         visible: false
+        anchors.centerIn: window.contentItem
     }
 
     Dialog {
@@ -164,6 +174,7 @@ Kirigami.ApplicationWindow {
         modal: true
         title: i18n("Unsaved changes")
         standardButtons: Dialog.Cancel | Dialog.Discard | Dialog.Apply
+        anchors.centerIn: window.contentItem
 
         onRejected: close()
         onDiscarded: {
@@ -184,44 +195,4 @@ Kirigami.ApplicationWindow {
             text: i18n("There are unsaved changes.\nDo you want to apply these changes?")
         }
     }
-
-    Action {
-        id: applyAction
-//         text: i18n("Apply")
-        enabled: Fancontrol.Base.needsApply
-        onTriggered: Fancontrol.Base.apply()
-        icon.name: "dialog-ok-apply"
-//         tooltip: i18n("Apply changes")
-        shortcut: StandardKey.Apply
-    }
-    Action {
-        id: resetAction
-//         text: i18n("Reset")
-        enabled: Fancontrol.Base.needsApply
-        onTriggered: Fancontrol.Base.reset()
-        icon.name: "edit-undo"
-//         tooltip: i18n("Revert changes")
-    }
-    Action {
-        id: startAction
-//         text: i18n("Start")
-        enabled: !!Fancontrol.Base.systemdCom && !Fancontrol.Base.systemdCom.serviceActive
-        icon.name: "media-playback-start"
-//         tooltip: i18n("Enable manual control")
-
-        onTriggered: Fancontrol.Base.systemdCom.serviceActive = true
-    }
-    Action {
-        id: stopAction
-//         text: i18n("Stop")
-        enabled: !!Fancontrol.Base.systemdCom && Fancontrol.Base.systemdCom.serviceActive
-        icon.name: "media-playback-stop"
-//         tooltip: i18n("Disable manual control")
-
-        onTriggered: Fancontrol.Base.systemdCom.serviceActive = false
-    }
-
-    SystemPalette {
-        id: palette
-    }
 }

+ 14 - 30
fancontrol-gui/package/contents/ui/ConfigfileTab.qml

@@ -18,43 +18,27 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.10
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 
 
-ColumnLayout {
+Kirigami.ScrollablePage {
+    id: root
+
     property QtObject loader: Fancontrol.Base.loader
 
-    Label {
-        Layout.alignment: Qt.AlignTop
+    header: Label {
         text: !!loader && loader.configEqualToLoadedFile ? loader.configPath : i18n("New config")
     }
 
-    Rectangle {
-        Layout.fillHeight: true
-        Layout.fillWidth: true
-        color: palette.light
-        border.width: 1
-        radius: Kirigami.Units.smallSpacing
-
-        ScrollView {
-            id: scrollView
-
-            anchors.fill: parent
-            anchors.margins: Kirigami.Units.smallSpacing
-
-            TextEdit {
-                text: !!loader ? loader.config : ""
-                readOnly: true
-                color: palette.text
-            }
-        }
-    }
-
-    SystemPalette {
-        id: palette
+    TextEdit {
+        width: root.width - root.leftPadding - root.rightPadding
+        text: !!loader ? loader.config : ""
+        readOnly: true
+        color: Kirigami.Theme.textColor
+        wrapMode: TextEdit.Wrap
     }
 }

+ 81 - 107
fancontrol-gui/package/contents/ui/PwmFansTab.qml

@@ -18,143 +18,117 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.10
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 
 
-Item {
+Kirigami.Page {
     property QtObject loader: Fancontrol.Base.loader
     property QtObject systemdCom: Fancontrol.Base.hasSystemdCommunicator() ? Fancontrol.Base.systemdCom : null
     property QtObject pwmFanModel: Fancontrol.Base.pwmFanModel
     property QtObject tempModel: Fancontrol.Base.tempModel
     property QtObject profileModel: Fancontrol.Base.profileModel
     property var pwmFans: pwmFanModel.fans
+    property QtObject fan: applicationWindow().fan
 
     id: root
 
-    RowLayout {
-        id: profileRow
-
-        anchors.top: parent.top
-        width: parent.width
-        height: childrenRect.height
-
-        Label {
-            text: i18n("Profile:")
-            Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-            renderType: Text.NativeRendering
-        }
-        ComboBox {
-            id: profileComboBox
-
-            property string saveText: editText.length > 0 ? editText : currentText
-
-            editable: true
-            model: profileModel
-            textRole: "display"
-            Layout.fillWidth: true
-            Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+    header: Fancontrol.FanHeader {
+        fan: root.fan
+    }
 
-            onActivated: Fancontrol.Base.applyProfile(index)
+    contextualActions: [
+        Kirigami.Action {
+            text: i18n("Manage profiles")
+            onTriggered: profilesDialog.open()
+        },
+        Kirigami.Action {
+            text: i18n("Service")
+            visible: Fancontrol.Base.hasSystemdCommunicator
+            tooltip: i18n("Control the systemd service")
+
+            Kirigami.Action {
+                text: !!systemdCom && systemdCom.serviceActive ? i18n("Stop service") : i18n("Start service")
+                icon.name: !!systemdCom && systemdCom.serviceActive ? "media-playback-stop" : "media-playback-start"
+
+                onTriggered: systemdCom.serviceActive = !systemdCom.serviceActive
+            }
+            Kirigami.Action {
+                text: !!systemdCom && systemdCom.serviceEnabled ? i18n("Disable service") : i18n("Enable service")
+                tooltip: !!systemdCom && systemdCom.serviceEnabled ? i18n("Disable service autostart at boot") : i18n("Enable service autostart at boot")
 
-            Connections {
-                target: Fancontrol.Base
-                onProfileChanged: profileComboBox.currentIndex = profile
+                onTriggered: systemdCom.serviceEnabled = !systemdCom.serviceEnabled
+            }
+        },
+        Kirigami.Action {
+            text: loader.sensorsDetected ? i18n("Detect fans again") : i18n("Detect fans")
+            icon.name: "dialog-password"
+            onTriggered: loader.detectSensors()
+        },
+        Kirigami.Action {
+            visible: !!systemdCom && !!fan
+            text: !!fan ? fan.testing ? i18n("Abort test") : i18n("Test start and stop values") : ""
+            icon.name: "dialog-password"
+            onTriggered: {
+                if (fan.testing) {
+                    fan.abortTest();
+                } else {
+                    fan.test();
+                }
             }
         }
-        Button {
-            Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-            action: saveProfileAction
-        }
-        Button {
-            Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-            action: deleteProfileAction
-        }
-    }
+    ]
 
-    RowLayout {
-        id: fanRow
-
-        anchors.top: profileRow.bottom
-        width: parent.width
-        height: childrenRect.height
-        visible: pwmFans.length > 0
-
-        Label {
-            text: i18n("Fan:")
-            Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-            renderType: Text.NativeRendering
-        }
-        ComboBox {
-            id: fanComboBox
+    mainAction: Kirigami.Action {
+        text: i18n("Apply")
+        enabled: Fancontrol.Base.needsApply
+        icon.name: "dialog-ok-apply"
+        tooltip: i18n("Apply changes")
+        shortcut: StandardKey.Apply
 
-            model: pwmFanModel
-            textRole: "display"
-            Layout.fillWidth: true
-            Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-        }
-        Button {
-            Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-            action: detectFansAction
-        }
+        onTriggered: Fancontrol.Base.apply()
     }
+    rightAction: Kirigami.Action {
+        text: i18n("Reset")
+        enabled: Fancontrol.Base.needsApply
+        icon.name: "edit-undo"
+        tooltip: i18n("Revert changes")
 
-    Loader {
-        width: parent.width
-        anchors.top: fanRow.bottom
-        anchors.bottom: parent.bottom
-        anchors.topMargin: parent.anchors.margins
-        active: pwmFans.length > fanComboBox.currentIndex && fanComboBox.currentIndex >= 0
-
-        sourceComponent: Fancontrol.FanItem {
-            fan: pwmFans[fanComboBox.currentIndex]
-            tempModel: root.tempModel
-            systemdCom: root.systemdCom
-        }
+        onTriggered: Fancontrol.Base.reset()
     }
 
     ColumnLayout {
-        id: noFansInfo
+        anchors.fill: parent
 
-        anchors.centerIn: parent
-        spacing: Kirigami.Units.smallSpacing * 2
-        visible: pwmFans.length === 0
-
-        Label {
-            Layout.alignment: Qt.AlignCenter
-            text: i18n("There are no pwm capable fans in your system.")
-            font.pointSize: 14
-            font.bold: true
-        }
+        Loader {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            active: !!root.fan
 
-        Button {
-            Layout.alignment: Qt.AlignCenter
-            action: detectFansAction
+            sourceComponent: Fancontrol.FanItem {
+                fan: root.fan
+                tempModel: root.tempModel
+                systemdCom: root.systemdCom
+            }
         }
     }
 
-    Action {
-        id: saveProfileAction
-
-        text: i18n("Save profile")
-        icon.name: "document-save"
-        onTriggered: Fancontrol.Base.saveProfile(profileComboBox.saveText)
+    Label {
+        anchors.centerIn: parent
+        visible: pwmFans.length === 0
+        text: i18n("There are no pwm capable fans in your system.")
+        font.pointSize: 14
+        font.bold: true
     }
-    Action {
-        id: deleteProfileAction
 
-        text: i18n("Delete profile")
-        icon.name: "edit-delete"
-        onTriggered: Fancontrol.Base.deleteProfile(profileComboBox.currentIndex)
-    }
-    Action {
-        id: detectFansAction
+    Fancontrol.ProfilesDialog {
+        id: profilesDialog
 
-        text: loader.sensorsDetected ? i18n("Detect fans again") : i18n("Detect fans")
-        icon.name: "dialog-password"
-        onTriggered: loader.detectSensors()
+        visible: false
+        modal: true
+        anchors.centerIn: parent
     }
 }

+ 29 - 29
fancontrol-gui/package/contents/ui/SensorsTab.qml

@@ -18,40 +18,44 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
+import QtQuick 2.6
+import QtQuick.Controls 2.1
 import QtQuick.Layouts 1.2
-import org.kde.kirigami 2.0 as Kirigami
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 
 
-RowLayout {
+Kirigami.ScrollablePage {
     id: root
 
-    property int padding: 10
     property QtObject loader: Fancontrol.Base.loader
 
-    anchors.fill: parent
+    spacing: Kirigami.Units.smallSpacing
+
+    ListView {
+        id: listView
+
+        width: root.width
+        topMargin: spacing
+        spacing: Kirigami.Units.largeSpacing * 2
+        headerPositioning: ListView.OverlayHeader
 
-    Repeater {
         model: loader.hwmons.length
 
-        Rectangle {
+        delegate: Rectangle {
             property QtObject hwmon: loader.hwmons[index]
 
-            Layout.preferredWidth: root.width / loader.hwmons.length - root.spacing
-            Layout.minimumWidth: Kirigami.Units.gridUnit * 10
-            Layout.fillHeight: true
-            color: palette.light
-            border.width: 1
-            border.color: "black"
-            radius: Kirigami.Units.smallSpacing
+            height: childrenRect.height
+            width: listView.width - listView.spacing * 2
+            x: listView.spacing
             clip: true
+            color: Kirigami.Theme.backgroundColor
 
             Column {
                 id: column
-                anchors.fill: parent
-                anchors.margins: Kirigami.Units.smallSpacing
+
+                width: parent.width
+                padding: root.spacing
 
                 Label {
                     anchors.horizontalCenter: parent.horizontalCenter
@@ -64,11 +68,11 @@ RowLayout {
                     model: hwmon.fans.length
 
                     RowLayout {
-                        width: parent.width
+                        width: parent.width - parent.padding * 2
 
                         Label {
-                            anchors.leftMargin: root.padding
-                            Layout.maximumWidth: parent.width - rpmValue.width - root.padding*2
+                            anchors.leftMargin: root.spacing
+                            Layout.maximumWidth: parent.width - rpmValue.width - root.spacing*2
                             Layout.alignment: Qt.AlignLeft
                             clip: true
                             text: "Fan " + (index+1) + ":"
@@ -76,7 +80,7 @@ RowLayout {
                         Label {
                             id: rpmValue
                             Layout.alignment: Qt.AlignRight
-                            anchors.rightMargin: root.padding
+                            anchors.rightMargin: root.spacing
                             text: hwmon.fans[index].rpm + " " + i18n("rpm")
                         }
                     }
@@ -85,19 +89,19 @@ RowLayout {
                     model: hwmon.temps.length
 
                     RowLayout {
-                        width: parent.width
+                        width: parent.width - parent.padding * 2
 
                         Label {
-                            anchors.leftMargin: root.padding
+                            anchors.leftMargin: root.spacing
                             text: hwmon.temps[index].name + ": "
-                            Layout.maximumWidth: parent.width - tempValue.width - root.padding*2
+                            Layout.maximumWidth: parent.width - tempValue.width - root.spacing*2
                             Layout.alignment: Qt.AlignLeft
                             clip: true
                         }
                         Label {
                             id: tempValue
                             Layout.alignment: Qt.AlignRight
-                            anchors.rightMargin: root.padding
+                            anchors.rightMargin: root.spacing
                             text: Units.fromCelsius(hwmon.temps[index].value, Fancontrol.Base.unit) + " " + i18n(Fancontrol.Base.unit)
                         }
                     }
@@ -105,8 +109,4 @@ RowLayout {
             }
         }
     }
-
-    SystemPalette {
-        id: palette
-    }
 }

+ 4 - 255
fancontrol-gui/package/contents/ui/SettingsTab.qml

@@ -18,264 +18,13 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Layouts 1.10
-import QtQuick.Controls 2.3
-import QtQuick.Dialogs 1.2
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 
 
-Item {
-    property QtObject systemdCom: Fancontrol.Base.hasSystemdCommunicator() ? Fancontrol.Base.systemdCom : null
-    property QtObject loader: Fancontrol.Base.loader
-    property int padding: Kirigami.Units.smallSpacing
-    property real textWidth: 0
-    property var locale: Qt.locale()
-
-    id: root
-
-    Column {
-        id: column
+Kirigami.Page {
+    Fancontrol.SettingsForm {
         anchors.fill: parent
-        anchors.margins: padding
-        spacing: Kirigami.Units.smallSpacing
-
-        RowLayout {
-            width: parent.width
-
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Interval:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            SpinBox {
-                id: intervalSpinBox
-
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                value: loader.interval
-                from: 1.0
-                editable: true
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 0) + ' ' + i18np("second", "seconds", value) }
-                onValueModified: loader.interval = value
-
-                Connections {
-                    target: loader
-                    onIntervalChanged: if (loader.interval != intervalSpinBox.value) intervalSpinBox.value = loader.interval
-                }
-            }
-        }
-        RowLayout {
-            width: parent.width
-
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Minimum temperature for fan graphs:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            SpinBox {
-                id: minTempBox
-
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-//                 decimals: 2
-                from: Fancontrol.Units.fromKelvin(0, Fancontrol.Base.unit)
-                inputMethodHints: Qt.ImhFormattedNumbersOnly
-                editable: true
-                value: Fancontrol.Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit)
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 2) + Fancontrol.Base.unit }
-                onValueModified: {
-                    Fancontrol.Base.minTemp = Fancontrol.Units.toCelsius(value, Fancontrol.Base.unit);
-                    if (value >= maxTempBox.value) maxTempBox.value = value + 1;
-                }
-
-                Connections {
-                    target: Fancontrol.Base
-                    onMinTempChanged: {
-                        if (Fancontrol.Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit) != minTempBox.value) {
-                            minTempBox.value = Fancontrol.Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit);
-                        }
-                    }
-                }
-            }
-        }
-        RowLayout {
-            width: parent.width
-
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Maximum temperature for fan graphs:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            SpinBox {
-                id: maxTempBox
-
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-//                 decimals: 2
-                from: Fancontrol.Units.fromKelvin(0, Fancontrol.Base.unit)
-                inputMethodHints: Qt.ImhFormattedNumbersOnly
-                editable: true
-                value: Fancontrol.Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit)
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 2) + Fancontrol.Base.unit }
-                onValueModified: {
-                    Fancontrol.Base.maxTemp = Fancontrol.Units.toCelsius(value, Fancontrol.Base.unit);
-                    if (value <= minTempBox.value) minTempBox.value = value - 1;
-                }
-
-                Connections {
-                    target: Fancontrol.Base
-                    onMaxTempChanged: {
-                        if (Fancontrol.Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit) != maxTempBox.value) {
-                            maxTempBox.value = Fancontrol.Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit);
-                        }
-                    }
-                }
-            }
-        }
-        RowLayout {
-            width: parent.width
-
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Path to the fancontrol config file:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            Fancontrol.OptionInput {
-                id: fileInput
-
-                Layout.fillWidth: true
-                value: Fancontrol.Base.configUrl.toString().replace("file://", "")
-                onTextChanged: Fancontrol.Base.configUrl = text;
-            }
-            Button {
-                icon.name: "document-open"
-//                 tooltip: i18n("Open config file")
-                onClicked: openFileDialog.open();
-            }
-        }
-        Loader {
-            active: !!systemdCom
-            sourceComponent: RowLayout {
-                width: column.width
-
-                Label {
-                    Layout.preferredWidth: root.textWidth
-                    clip: true
-                    text: i18n("Name of the fancontrol systemd service:")
-                    horizontalAlignment: Text.AlignRight
-                    Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-                }
-                Fancontrol.OptionInput {
-                    id: serviceNameInput
-
-                    Layout.minimumWidth: implicitWidth
-                    Layout.fillWidth: true
-                    color: !!systemdCom && systemdCom.serviceExists ? "green" : "red"
-                    value: Fancontrol.Base.serviceName
-                    onTextChanged: Fancontrol.Base.serviceName = text
-                }
-            }
-        }
-        Loader {
-            active: !!systemdCom
-            sourceComponent: RowLayout {
-                width: column.width
-
-                Label {
-                    Layout.preferredWidth: root.textWidth
-                    clip: true
-                    text: i18n("Enable service at boot:")
-                    horizontalAlignment: Text.AlignRight
-                    Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-                }
-                CheckBox {
-                    id: autostartBox
-
-                    Layout.minimumWidth: implicitWidth
-                    Layout.fillWidth: true
-                    checked: !!systemdCom ? systemdCom.serviceEnabled : false
-                    onCheckedChanged: {
-                        if (!!systemdCom) {
-                            systemdCom.serviceEnabled = checked;
-                        }
-                    }
-
-                    Connections {
-                        target: systemdCom
-                        onServiceEnabledChanged: if (systemdCom.serviceEnabled != autostartBox.checked) autostartBox.checked = systemdCom.serviceEnabled
-                    }
-                }
-            }
-        }
-        RowLayout {
-            width: column.width
-
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Show tray icon:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            CheckBox {
-                id: trayBox
-
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                checked: Fancontrol.Base.showTray
-                onCheckedChanged: Fancontrol.Base.showTray = checked
-
-                Connections {
-                    target: Fancontrol.Base
-                    onShowTrayChanged: if (Fancontrol.Base.showTray != trayBox.checked) trayBox.checked = Fancontrol.Base.showTray
-                }
-            }
-        }
-        RowLayout {
-            width: column.width
-            enabled: Fancontrol.Base.showTray
-
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Start minimized:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            CheckBox {
-                id: startMinimizedBox
-
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                checked: Fancontrol.Base.startMinimized
-                onCheckedChanged: Fancontrol.Base.startMinimized = checked
-
-                Connections {
-                    target: Fancontrol.Base
-                    onStartMinimizedChanged: if (Fancontrol.Base.startMinimized != startMinimizedBox.checked) startMinimizedBox.checked = Fancontrol.Base.startMinimized
-                }
-            }
-        }
-
-        FileDialog {
-            id: openFileDialog
-            title: i18n("Please choose a configuration file")
-            folder: "file:///etc"
-            selectExisting: true
-            selectMultiple: false
-            modality: Qt.NonModal
-
-            onAccepted: fileInput.text = fileUrl;
-        }
     }
 }

+ 1 - 1
fancontrol-gui/src/systemtrayicon.cpp

@@ -27,7 +27,7 @@ SystemTrayIcon::SystemTrayIcon(QObject *parent) : KStatusNotifierItem(QStringLit
 {
     setCategory(KStatusNotifierItem::ApplicationStatus);
 
-    m_profilesMenu = contextMenu()->addMenu(i18n("Profiles"));
+    m_profilesMenu = contextMenu()->addMenu(i18n("Apply profile"));
 }
 
 void SystemTrayIcon::setProfileModel(QStringListModel* model)

+ 3 - 1
import/CMakeLists.txt

@@ -12,9 +12,11 @@ set(LIB_SRCS src/hwmon.cpp
 
 set(QML_FILES qml/qmldir
               qml/ErrorDialog.qml
-              qml/OptionInput.qml
+              qml/FanHeader.qml
               qml/FanItem.qml
               qml/PwmPoint.qml
+              qml/ProfilesDialog.qml
+              qml/SettingsForm.qml
               qml/StatusPoint.qml
               qml/colors.js
               qml/math.js

+ 3 - 3
import/qml/ErrorDialog.qml

@@ -18,9 +18,9 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 
 

+ 70 - 0
import/qml/FanHeader.qml

@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019  Malte Veerman <malte.veerman@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import org.kde.kirigami 2.3 as Kirigami
+
+
+Item {
+    property QtObject fan
+
+    implicitHeight: layout.childrenRect.height
+
+    RowLayout {
+        id: layout
+
+        anchors.fill: parent
+        anchors.leftMargin: Kirigami.Units.smallSpacing
+        anchors.rightMargin: Kirigami.Units.smallSpacing
+
+        TextEdit {
+            id: nameField
+
+            Layout.alignment: Qt.AlignLeft
+            text: !!fan ? fan.name : ""
+            color: Kirigami.Theme.textColor
+            horizontalAlignment: TextEdit.AlignLeft
+            wrapMode: TextEdit.Wrap
+            font.bold: true
+            font.pointSize: Kirigami.Theme.defaultFont.pointSize + 2
+            selectByMouse: true
+
+            onTextChanged: if (!!fan && fan.name != text) fan.name = text
+
+            Connections {
+                target: !!fan ? fan : null
+                onNameChanged: if (fan.name != nameField.text) nameField.text = fan.name
+            }
+
+            MouseArea {
+                anchors.fill: parent
+                cursorShape: Qt.IBeamCursor
+                acceptedButtons: Qt.NoButton
+            }
+        }
+        Label {
+            Layout.alignment: Qt.AlignRight
+            text: !!fan ? fan.path : ""
+            horizontalAlignment: Text.AlignRight
+        }
+    }
+}

+ 20 - 96
import/qml/FanItem.qml

@@ -18,17 +18,17 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.10
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import org.kde.kirigami 2.2 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 import "math.js" as MoreMath
 import "units.js" as Units
 import "colors.js" as Colors
 
 
-Rectangle {
+Item {
     property QtObject fan
     property QtObject systemdCom
     property QtObject tempModel
@@ -40,10 +40,7 @@ Rectangle {
     property real convertedMaxTemp: Units.fromCelsius(maxTemp, unit)
 
     id: root
-    color: "transparent"
-    border.color: palette.windowText
-    border.width: 2
-    radius: Kirigami.Units.smallSpacing
+
     clip: false
 
     onConvertedMinTempChanged: {
@@ -56,61 +53,17 @@ Rectangle {
     }
     onFanChanged: curveCanvas.requestPaint()
 
-    SystemPalette {
-        id: palette
-        colorGroup: SystemPalette.Active
-    }
-    SystemPalette {
-        id: disabledPalette
-        colorGroup: SystemPalette.Disabled
-    }
-
-    TextEdit {
-        id: nameField
-
-        anchors {
-            left: parent.left
-            leftMargin: margin
-            right: parent.right
-            rightMargin: margin
-            top: parent.top
-            topMargin: margin
-        }
-        visible: root.height >= height + margin * 2
-        text: !!fan ? fan.name : ""
-        color: palette.text
-        horizontalAlignment: TextEdit.AlignLeft
-        wrapMode: TextEdit.Wrap
-        font.bold: true
-        font.pointSize: 14
-        selectByMouse: true
-
-        onTextChanged: if (!!fan && fan.name != text) fan.name = text
-
-        Connections {
-            target: fan
-            onNameChanged: if (fan.name != nameField.text) nameField.text = fan.name
-        }
-
-        MouseArea {
-            anchors.fill: parent
-            cursorShape: Qt.IBeamCursor
-            acceptedButtons: Qt.NoButton
-        }
-    }
-
     Item {
         id: graph
 
         property int fontSize: MoreMath.bound(8, height / 20 + 1, 16)
-        property QtObject pal: !!fan ? fan.hasTemp ? palette : disabledPalette : disabledPalette
         property int verticalScalaCount: 6
         property var horIntervals: MoreMath.intervals(root.convertedMinTemp, root.convertedMaxTemp, 10)
 
         anchors {
             left: parent.left
             right: parent.right
-            top: nameField.bottom
+            top: parent.top
             bottom: settingsArea.top
         }
         visible: graphBackground.height > 0 && graphBackground.width > 0
@@ -130,12 +83,12 @@ Rectangle {
 
                 model: graph.verticalScalaCount
 
-                Label {
+                Text {
                     x: verticalScala.width - implicitWidth - graph.fontSize / 3
                     y: graphBackground.height - graphBackground.height / (graph.verticalScalaCount - 1) * index - graph.fontSize * 2 / 3
                     horizontalAlignment: Text.AlignRight
-                    color: graph.pal.text
-                    text: i18n("%1\%", index * (100 / (graph.verticalScalaCount - 1)))
+                    color: Kirigami.Theme.textColor
+                    text: Number(index * (100 / (graph.verticalScalaCount - 1))).toLocaleString(locale, 'f', 0) + locale.percent
                     font.pixelSize: graph.fontSize
                 }
             }
@@ -154,11 +107,11 @@ Rectangle {
             Repeater {
                 model: graph.horIntervals.length;
 
-                Label {
+                Text {
                     x: graphBackground.scaleX(Units.toCelsius(graph.horIntervals[index], unit)) - width/2
                     y: horizontalScala.height / 2 - implicitHeight / 2
-                    color: graph.pal.text
-                    text: i18n("%1" + unit, graph.horIntervals[index])
+                    color: Kirigami.Theme.textColor
+                    text: Number(graph.horIntervals[index]).toLocaleString() + i18n(unit)
                     font.pixelSize: graph.fontSize
                 }
             }
@@ -167,10 +120,8 @@ Rectangle {
         Rectangle {
             id: graphBackground
 
-            property alias pal: graph.pal
-
-            color: pal.light
-            border.color: pal.text
+            color: Kirigami.Theme.backgroundColor
+            border.color: Kirigami.Theme.textColor
             border.width: 2
             radius: 1
 
@@ -205,8 +156,6 @@ Rectangle {
                 anchors.margins: parent.border.width
                 renderStrategy: Canvas.Cooperative
 
-                property alias pal: graphBackground.pal
-
                 onPaint: {
                     var c = curveCanvas.getContext("2d");
                     c.clearRect(0, 0, width, height);
@@ -256,15 +205,13 @@ Rectangle {
                 anchors.margins: parent.border.width
                 renderStrategy: Canvas.Cooperative
 
-                property alias pal: graphBackground.pal
-
                 onPaint: {
                     var c = meshCanvas.getContext("2d");
                     c.clearRect(0, 0, width, height);
 
                     //draw mesh
                     c.beginPath();
-                    c.strokeStyle = Colors.setAlpha(pal.text, 0.3);
+                    c.strokeStyle = Colors.setAlpha(Kirigami.Theme.textColor, 0.3);
 
                     //horizontal lines
                     for (var i=0; i<=100; i+=20) {
@@ -298,7 +245,7 @@ Rectangle {
             }
             PwmPoint {
                 id: stopPoint
-                color: !!fan ? fan.hasTemp ? "blue" : Qt.tint(graph.pal.light, Qt.rgba(0, 0, 1, 0.5)) : "transparent"
+                color: !!fan ? fan.hasTemp ? "blue" : Qt.tint(Kirigami.Theme.disabledTextColor, Qt.rgba(0, 0, 1, 0.5)) : "transparent"
                 size: graph.fontSize
                 visible: !!fan ? fan.hasTemp : false
                 drag.maximumX: Math.min(graphBackground.scaleX(graphBackground.scaleTemp(maxPoint.x)-1), maxPoint.x-1)
@@ -323,7 +270,7 @@ Rectangle {
             }
             PwmPoint {
                 id: maxPoint
-                color: !!fan ? fan.hasTemp ? "red" : Qt.tint(graph.pal.light, Qt.rgba(1, 0, 0, 0.5)) : "transparent"
+                color: !!fan ? fan.hasTemp ? "red" : Qt.tint(Kirigami.Theme.disabledTextColor, Qt.rgba(1, 0, 0, 0.5)) : "transparent"
                 size: graph.fontSize
                 visible: !!fan ? fan.hasTemp : false
                 drag.minimumX: Math.max(graphBackground.scaleX(graphBackground.scaleTemp(stopPoint.x)+1), stopPoint.x+1)
@@ -359,7 +306,7 @@ Rectangle {
             bottom: parent.bottom
             bottomMargin: padding
         }
-        visible: root.height >= nameField.height + height + 2*margin
+        visible: root.height >= height + 2*margin
         clip: true
         spacing: 2
 
@@ -450,7 +397,7 @@ Rectangle {
                 to: 100
                 editable: true
                 value: !!fan ? Math.round(fan.minStart / 2.55) : 0
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 1) + i18n("%") }
+                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 1) + locale.percent }
                 onValueModified: {
                     if (!!fan) {
                         fan.minStart = Math.round(value * 2.55)
@@ -467,28 +414,5 @@ Rectangle {
                 }
             }
         }
-
-        RowLayout {
-            visible: !!systemdCom
-
-            Item {
-                Layout.fillWidth: true
-            }
-            Button {
-                id: testButton
-
-                text: !!fan ? fan.testing ? i18n("Abort test") : i18n("Test start and stop values") : ""
-                icon.name: "dialog-password"
-                Layout.alignment: Qt.AlignRight
-                onClicked: {
-                    if (fan.testing) {
-                        fan.abortTest();
-                    } else {
-                        minStartInput.value = Qt.binding(function() { return Math.round(fan.minStart / 2.55) });
-                        fan.test();
-                    }
-                }
-            }
-        }
     }
 }

+ 0 - 81
import/qml/OptionInput.qml

@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2015  Malte Veerman <malte.veerman@gmail.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation; either version 2 of the License, 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 Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser 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.4
-import QtQuick.Layouts 1.1
-import org.kde.kirigami 2.0 as Kirigami
-
-
-FocusScope {
-    property alias text: textField.text
-    property alias font: textField.font
-    property alias inputMethodHints: textField.inputMethodHints
-    property alias validator: textField.validator
-    property alias color: textField.color
-    property real margin: Kirigami.Units.smallSpacing
-    property var value
-    property var locale: Qt.locale()
-
-    id: root
-    implicitHeight: textField.implicitHeight + margin * 2
-    implicitWidth: textField.implicitWidth + margin * 2
-
-    onValueChanged: {
-        if (textField.text != value) {
-            textField.text = value;
-        }
-    }
-
-    Rectangle {
-        id: rect
-        anchors.fill: parent
-        border.width: 1
-        radius: 2
-        color: enabled ? palette.base : disabledPalette.base
-        border.color: enabled ? mouseArea.containsMouse || root.activeFocus ? palette.highlight : palette.windowText : disabledPalette.windowText
-
-        TextInput {
-            id: textField
-            anchors.fill: parent
-            anchors.leftMargin: margin
-            horizontalAlignment: TextEdit.AlignLeft
-            verticalAlignment: TextEdit.AlignVCenter
-            renderType: Text.NativeRendering
-            selectByMouse: true
-            color: enabled ? palette.text : disabledPalette.text
-
-            MouseArea {
-                id: mouseArea
-                anchors.fill: parent
-                hoverEnabled: true
-                cursorShape: Qt.IBeamCursor
-                acceptedButtons: Qt.NoButton
-            }
-        }
-
-        SystemPalette {
-            id: palette
-        }
-        SystemPalette {
-            id: disabledPalette
-            colorGroup: SystemPalette.Disabled
-        }
-    }
-}

+ 126 - 0
import/qml/ProfilesDialog.qml

@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <malte.veerman@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
+import org.kde.kirigami 2.3 as Kirigami
+import Fancontrol.Qml 1.0 as Fancontrol
+
+
+Dialog {
+    title: i18n("Manage profiles")
+    width: Kirigami.Units.gridUnit * 20
+    height: Kirigami.Units.gridUnit * 20
+    standardButtons: Dialog.Close
+
+    RowLayout {
+        anchors.fill: parent
+
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            Kirigami.Theme.colorSet: Kirigami.Theme.View
+            color: Kirigami.Theme.backgroundColor
+            border.width: 1
+            border.color: Kirigami.Theme.textColor
+
+            ListView {
+                id: profilesListView
+
+                anchors.fill: parent
+                anchors.margins: parent.border.width
+                model: Fancontrol.Base.profileModel
+                clip: true
+                currentIndex: Fancontrol.Base.currentProfileIndex
+                boundsBehavior: Flickable.StopAtBounds
+                flickableDirection: Flickable.AutoFlickIfNeeded
+                header: Kirigami.BasicListItem {
+                    label: '<b>' + i18n("Profiles") + '</b>'
+                    reserveSpaceForIcon: false
+                    hoverEnabled: false
+                    separatorVisible: false
+                    leftPadding: Kirigami.Units.smallSpacing
+                }
+                delegate: Kirigami.BasicListItem {
+                    label: {
+                        if (Fancontrol.Base.currentProfileIndex === index)
+                            return display + ' ' + i18n("(current profile)")
+                        else
+                            return display
+                    }
+                    reserveSpaceForIcon: false
+                    hoverEnabled: true
+                    highlighted: ListView.isCurrentItem
+                    separatorVisible: false
+
+                    onPressedChanged: if (pressed) profilesListView.currentIndex = index;
+                }
+            }
+        }
+
+        ColumnLayout {
+            Layout.fillHeight: true
+
+            Button {
+                text: i18n("Apply profile")
+                enabled: Fancontrol.Base.currentProfileIndex != profilesListView.currentIndex
+                onClicked: Fancontrol.Base.applyProfile(profilesListView.currentIndex)
+            }
+            Button {
+                text: i18n("Create new profile")
+                enabled: Fancontrol.Base.currentProfileIndex == -1
+                onClicked: newProfileNameDialog.open()
+            }
+            Button {
+                text: i18n("Save to profile")
+                enabled: Fancontrol.Base.currentProfileIndex != profilesListView.currentIndex && profilesListView.currentIndex >= 0
+                onClicked: Fancontrol.Base.saveProfile(profilesListView.currentItem.label)
+            }
+            Button {
+                text: i18n("Delete profile")
+                enabled: profilesListView.currentIndex >= 0
+                onClicked: Fancontrol.Base.deleteProfile(profilesListView.currentIndex)
+            }
+            Item {
+                Layout.fillHeight: true
+            }
+        }
+    }
+
+    Dialog {
+        id: newProfileNameDialog
+
+        title: i18n("New profile's name")
+        standardButtons: Dialog.Ok | Dialog.Cancel
+        visible: false
+        anchors.centerIn: parent
+
+        onAccepted: {
+            Fancontrol.Base.saveProfile(newProfileNameField.text);
+            newProfileNameField.text = "";
+        }
+        onRejected: newProfileNameField.text = ""
+
+        TextField {
+            id: newProfileNameField
+        }
+    }
+}

+ 17 - 12
import/qml/PwmPoint.qml

@@ -18,9 +18,9 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 import "units.js" as Units
 
@@ -68,24 +68,29 @@ Rectangle {
 
     Rectangle {
         id: tooltip
+
         x: parent.width
         y: - height
-        width: Math.max(pwm.width, temp.width)
-        height: pwm.height + temp.height
-        radius: 4
+        width: column.width
+        height: column.height
+        radius: Kirigami.Units.smallSpacing / 2
         color: Qt.rgba(parent.color.r, parent.color.g, parent.color.b, 0.5)
         visible: root.enabled && (pwmMouse.containsMouse || drag.active)
 
         Column {
-            Label {
-                id: pwm
+            id: column
+
+            padding: Kirigami.Units.smallSpacing
+
+            Text {
                 font.pixelSize: root.size * 1.5
-                text: Number(Math.round(root.pwm / 2.55)).toLocaleString(locale, 'f', 1) + i18n('%')
+                text: Number(Units.fromCelsius(root.temp, unit)).toLocaleString(locale, 'f', 0) + i18n(unit)
+                color: Kirigami.Theme.textColor
             }
-            Label {
-                id: temp
+            Text {
                 font.pixelSize: root.size * 1.5
-                text: Math.round(Units.fromCelsius(root.temp, unit)) + i18n(unit)
+                text: Number(root.pwm / 2.55).toLocaleString(locale, 'f', 1) + locale.percent
+                color: Kirigami.Theme.textColor
             }
         }
     }

+ 174 - 0
import/qml/SettingsForm.qml

@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <malte.veerman@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.6
+import QtQuick.Layouts 1.2
+import QtQuick.Controls 2.1
+import QtQuick.Dialogs 1.2
+import org.kde.kirigami 2.3 as Kirigami
+import Fancontrol.Qml 1.0 as Fancontrol
+
+
+Kirigami.FormLayout {
+    id: root
+
+    property QtObject systemdCom: Fancontrol.Base.hasSystemdCommunicator() ? Fancontrol.Base.systemdCom : null
+    property QtObject loader: Fancontrol.Base.loader
+    property bool showAll: true
+
+    SpinBox {
+        id: intervalSpinBox
+
+        Kirigami.FormData.label: i18n("Interval:")
+        Layout.fillWidth: true
+        value: loader.interval
+        from: 1.0
+        editable: true
+        textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 0) + ' ' + i18np("second", "seconds", value) }
+        onValueModified: loader.interval = value
+
+        Connections {
+            target: loader
+            onIntervalChanged: if (loader.interval != intervalSpinBox.value) intervalSpinBox.value = loader.interval
+        }
+    }
+    SpinBox {
+        id: minTempBox
+
+        Kirigami.FormData.label: i18n("Minimum temperature for fan graphs:")
+        Layout.fillWidth: true
+        from: Fancontrol.Units.fromKelvin(0, Fancontrol.Base.unit)
+        inputMethodHints: Qt.ImhFormattedNumbersOnly
+        editable: true
+        value: Fancontrol.Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit)
+        textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 2) + Fancontrol.Base.unit }
+        onValueModified: {
+            Fancontrol.Base.minTemp = Fancontrol.Units.toCelsius(value, Fancontrol.Base.unit);
+            if (value >= maxTempBox.value) maxTempBox.value = value + 1;
+        }
+
+        Connections {
+            target: Fancontrol.Base
+            onMinTempChanged: {
+                if (Fancontrol.Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit) != minTempBox.value) {
+                    minTempBox.value = Fancontrol.Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit);
+                }
+            }
+        }
+    }
+    SpinBox {
+        id: maxTempBox
+
+        Kirigami.FormData.label: i18n("Maximum temperature for fan graphs:")
+        Layout.fillWidth: true
+        from: Fancontrol.Units.fromKelvin(0, Fancontrol.Base.unit)
+        inputMethodHints: Qt.ImhFormattedNumbersOnly
+        editable: true
+        value: Fancontrol.Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit)
+        textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 2) + Fancontrol.Base.unit }
+        onValueModified: {
+            Fancontrol.Base.maxTemp = Fancontrol.Units.toCelsius(value, Fancontrol.Base.unit);
+            if (value <= minTempBox.value) minTempBox.value = value - 1;
+        }
+
+        Connections {
+            target: Fancontrol.Base
+            onMaxTempChanged: {
+                if (Fancontrol.Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit) != maxTempBox.value) {
+                    maxTempBox.value = Fancontrol.Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit);
+                }
+            }
+        }
+    }
+    RowLayout {
+        Kirigami.FormData.label: i18n("Path to the fancontrol config file:")
+        Layout.fillWidth: true
+
+        TextField {
+            id: fileInput
+
+            Layout.fillWidth: true
+            text: Fancontrol.Base.configUrl.toString().replace("file://", "")
+            onTextChanged: Fancontrol.Base.configUrl = text;
+
+            Connections {
+                target: Fancontrol.Base
+                onConfigUrlChanged: if(Fancontrol.Base.configUrl.toString().replace("file://", "") != fileInput.text) fileInput.text = Fancontrol.Base.configUrl.toString().replace("file://", "")
+            }
+        }
+        Button {
+            icon.name: "document-open"
+            //                 tooltip: i18n("Open config file")
+            onClicked: openFileDialog.open();
+        }
+    }
+    TextField {
+        id: serviceNameInput
+
+        visible: !!systemdCom
+        Kirigami.FormData.label: i18n("Name of the fancontrol systemd service:")
+        Layout.fillWidth: true
+        color: !!systemdCom && systemdCom.serviceExists ? "green" : "red"
+        text: Fancontrol.Base.serviceName
+        onTextChanged: Fancontrol.Base.serviceName = text
+
+        Connections {
+            target: Fancontrol.Base
+            onServiceNameChanged: if(Fancontrol.Base.serviceName != serviceNameInput.text) serviceNameInput.text = Fancontrol.Base.serviceName
+        }
+    }
+    CheckBox {
+        id: trayBox
+
+        Kirigami.FormData.label: i18n("Show tray icon:")
+        checked: Fancontrol.Base.showTray
+        visible: showAll
+        onCheckedChanged: Fancontrol.Base.showTray = checked
+
+        Connections {
+            target: Fancontrol.Base
+            onShowTrayChanged: if (Fancontrol.Base.showTray != trayBox.checked) trayBox.checked = Fancontrol.Base.showTray
+        }
+    }
+    CheckBox {
+        id: startMinimizedBox
+
+        Kirigami.FormData.label: i18n("Start minimized:")
+        checked: Fancontrol.Base.startMinimized
+        visible: showAll
+        onCheckedChanged: Fancontrol.Base.startMinimized = checked
+
+        Connections {
+            target: Fancontrol.Base
+            onStartMinimizedChanged: if (Fancontrol.Base.startMinimized != startMinimizedBox.checked) startMinimizedBox.checked = Fancontrol.Base.startMinimized
+        }
+    }
+
+    FileDialog {
+        id: openFileDialog
+        title: i18n("Please choose a configuration file")
+        folder: "file:///etc"
+        selectExisting: true
+        selectMultiple: false
+        modality: Qt.NonModal
+
+        onAccepted: fileInput.text = fileUrl;
+    }
+}

+ 30 - 23
import/qml/StatusPoint.qml

@@ -18,9 +18,9 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import org.kde.kirigami 2.0 as Kirigami
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import org.kde.kirigami 2.3 as Kirigami
 import Fancontrol.Qml 1.0 as Fancontrol
 import "units.js" as Units
 import "math.js" as MoreMath
@@ -33,6 +33,7 @@ Rectangle {
     property Item background: parent
     property real unscaledTemp: !!fan && fan.hasTemp && !!fan.temp ? fan.temp.value : 0
     property real unscaledPwm: !!fan ? fan.pwm : 0
+    property real unscaledRpm: !!fan ? fan.rpm : 0
     property var locale: Qt.locale()
     readonly property real centerX: x + width / 2
     readonly property real centerY: y + height / 2
@@ -51,17 +52,23 @@ Rectangle {
         SpringAnimation {
             epsilon: 0.1
             spring: 1.0
-            damping: 0.4
+            damping: 0.6
         }
     }
     Behavior on unscaledPwm {
         SpringAnimation {
             epsilon: 0.1
             spring: 1.0
-            damping: 0.4
+            damping: 0.6
+        }
+    }
+    Behavior on unscaledRpm {
+        SpringAnimation {
+            epsilon: 0.1
+            spring: 1.0
+            damping: 0.6
         }
     }
-
     MouseArea {
         id: pwmMouse
 
@@ -74,31 +81,31 @@ Rectangle {
 
         x: parent.width
         y: - height
-        width: Math.max(pwm.width, rpm.width)
-        height: temp.height + pwm.height + rpm.height
-        radius: 4
+        width: column.width
+        height: column.height
+        radius: Kirigami.Units.smallSpacing / 2
         color: Qt.rgba(parent.color.r, parent.color.g, parent.color.b, 0.5)
         visible: root.enabled && pwmMouse.containsMouse
 
         Column {
-            Label {
-                id: temp
+            id: column
 
-                font.pixelSize: root.height * 1.5
-                text: (!!fan && fan.hasTemp ? Math.round(Units.fromCelsius(root.unscaledTemp, unit)) : "0") + i18n(unit)
+            padding: Kirigami.Units.smallSpacing
 
+            Text {
+                font.pixelSize: root.size * 1.5
+                text: Number(Units.fromCelsius(root.unscaledTemp, unit)).toLocaleString(locale, 'f', 1) + i18n(unit)
+                color: Kirigami.Theme.textColor
             }
-            Label {
-                id: pwm
-
-                font.pixelSize: root.height * 1.5
-                text: Number(Math.round(unscaledPwm / 2.55)).toLocaleString(locale, 'f', 1) + i18n('%')
+            Text {
+                font.pixelSize: root.size * 1.5
+                text: Number(unscaledPwm / 2.55).toLocaleString(locale, 'f', 1) + locale.percent
+                color: Kirigami.Theme.textColor
             }
-            Label {
-                id: rpm
-
-                font.pixelSize: root.height * 1.5
-                text: (!!fan ? fan.rpm : "0") + i18n("rpm")
+            Text {
+                font.pixelSize: root.size * 1.5
+                text: Number(unscaledRpm).toLocaleString(locale, 'f', 0) + i18n("rpm")
+                color: Kirigami.Theme.textColor
             }
         }
     }

+ 3 - 1
import/qml/qmldir

@@ -2,8 +2,10 @@ module Fancontrol.Qml
 plugin fancontrol_qml_plugin
 internal StatusPoint StatusPoint.qml
 internal PwmPoint PwmPoint.qml
+FanHeader 1.0 FanHeader.qml
 FanItem 1.0 FanItem.qml
 ErrorDialog 1.0 ErrorDialog.qml
-OptionInput 1.0 OptionInput.qml
+ProfilesDialog 1.0 ProfilesDialog.qml
+SettingsForm 1.0 SettingsForm.qml
 Units 1.0 units.js
 

+ 52 - 20
import/src/guibase.cpp

@@ -48,6 +48,7 @@ GUIBase::GUIBase(QObject *parent) : QObject(parent),
 {
     connect(this, &GUIBase::unitChanged, m_tempModel, &TempModel::setUnit);
     connect(m_loader, &Loader::needsSaveChanged, this, &GUIBase::needsApplyChanged);
+    connect(m_loader, &Loader::configChanged, this, &GUIBase::currentProfileChanged);
 
 #ifndef NO_SYSTEMD
     connect(m_loader, &Loader::requestSetServiceActive, m_com, &SystemdCommunicator::setServiceActive);
@@ -77,20 +78,30 @@ GUIBase::~GUIBase()
 void GUIBase::load()
 {
     Config::instance()->load();
-    m_configValid = m_loader->load(configUrl());
 
+    // Check profiles
+    auto profiles = Config::instance()->findItem(QStringLiteral("Profiles"))->property().toStringList();
     auto profileNames = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList();
+
+    while (profiles.size() > profileNames.size())
+        profiles.removeLast();
+
+    while (profiles.size() < profileNames.size())
+        profileNames.removeLast();
+
+    Config::instance()->findItem(QStringLiteral("Profiles"))->setProperty(profiles);
+    Config::instance()->findItem(QStringLiteral("ProfileNames"))->setProperty(profileNames);
+
     m_profileModel->setStringList(profileNames);
 
-    Config::instance()->setCurrentGroup(QStringLiteral("preferences"));
-    int currentProfile = Config::instance()->findItem(QStringLiteral("CurrentProfile"))->property().toInt();
-    emit profileChanged(currentProfile);
+    m_configValid = m_loader->load(configUrl());
 
 #ifndef NO_SYSTEMD
     m_com->setServiceName(serviceName());
     m_com->reset();
 #endif
 
+    emit currentProfileChanged();
     emit serviceNameChanged();
     emit minTempChanged();
     emit maxTempChanged();
@@ -272,15 +283,15 @@ void GUIBase::handleInfo(const QString &info)
         qInfo() << info;
 }
 
-void GUIBase::applyProfile(const QString& profile)
+void GUIBase::applyProfile(const QString& profileName)
 {
-    if (!Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList().contains(profile))
+    if (!Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList().contains(profileName))
     {
-        handleError(i18n("Unable to apply unknown profile: %1", profile));
+        handleError(i18n("Unable to apply unknown profile: %1", profileName));
         return;
     }
 
-    int index = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList().indexOf(profile);
+    int index = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList().indexOf(profileName);
 
     applyProfile(index);
 }
@@ -306,22 +317,19 @@ void GUIBase::applyProfile(int index)
         return;
 
     m_loader->load(newConfig);
-
-    Config::instance()->findItem(QStringLiteral("CurrentProfile"))->setProperty(index);
-    emit profileChanged(index);
 }
 
-void GUIBase::saveProfile(const QString& profile, bool updateModel)
+void GUIBase::saveProfile(const QString& profileName, bool updateModel)
 {
     auto profileNames = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList();
-    int index = profileNames.indexOf(profile);
+    int index = profileNames.indexOf(profileName);
 
     if (index < 0)
     {
         index = profileNames.size();
 
         auto profileNames = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList();
-        Config::instance()->findItem(QStringLiteral("ProfileNames"))->setProperty(profileNames << profile);
+        Config::instance()->findItem(QStringLiteral("ProfileNames"))->setProperty(profileNames << profileName);
 
         if (updateModel)
             m_profileModel->insertRow(index);
@@ -331,13 +339,15 @@ void GUIBase::saveProfile(const QString& profile, bool updateModel)
     profiles.insert(index, m_loader->config());
     Config::instance()->findItem(QStringLiteral("Profiles"))->setProperty(profiles);
 
+    emit currentProfileChanged();
+
     if (updateModel)
-        m_profileModel->setData(m_profileModel->index(index, 0), profile);
+        m_profileModel->setData(m_profileModel->index(index, 0), profileName);
 }
 
-void GUIBase::deleteProfile(const QString& profile, bool updateModel)
+void GUIBase::deleteProfile(const QString& profileName, bool updateModel)
 {
-    int index = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList().indexOf(profile);
+    int index = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList().indexOf(profileName);
 
     deleteProfile(index, updateModel);
 }
@@ -345,18 +355,40 @@ void GUIBase::deleteProfile(const QString& profile, bool updateModel)
 void GUIBase::deleteProfile(int index, bool updateModel)
 {
     auto profileNames = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList();
+    auto profiles = Config::instance()->findItem(QStringLiteral("Profiles"))->property().toStringList();
 
-    if (index < 0 || index >= profileNames.size())
+    if (index < 0 || index >= profileNames.size() || index >= profiles.size())
+    {
+        qDebug() << "Index out of bounds";
         return;
+    }
 
     profileNames.removeAt(index);
     Config::instance()->findItem(QStringLiteral("ProfileNames"))->setProperty(profileNames);
-    auto profiles = Config::instance()->findItem(QStringLiteral("Profiles"))->property().toStringList();
-    profileNames.removeAt(index);
+    profiles.removeAt(index);
     Config::instance()->findItem(QStringLiteral("Profiles"))->setProperty(profiles);
 
+    emit currentProfileChanged();
+
     if (updateModel)
         m_profileModel->removeRow(index);
 }
 
+QString GUIBase::currentProfile() const
+{
+    const int index = currentProfileIndex();
+
+    if (index == -1)
+        return i18n("No profile");
+
+    const auto profileNames = Config::instance()->findItem(QStringLiteral("ProfileNames"))->property().toStringList();
+    return profileNames.value(index);
+}
+
+int GUIBase::currentProfileIndex() const
+{
+    const auto profiles = Config::instance()->findItem(QStringLiteral("Profiles"))->property().toStringList();
+    return profiles.indexOf(m_loader->config());
+}
+
 }

+ 8 - 4
import/src/guibase.h

@@ -52,6 +52,8 @@ class GUIBase : public QObject
     Q_PROPERTY(PwmFanModel *pwmFanModel READ pwmFanModel CONSTANT)
     Q_PROPERTY(TempModel *tempModel READ tempModel CONSTANT)
     Q_PROPERTY(QStringListModel *profileModel READ profileModel CONSTANT)
+    Q_PROPERTY(QString currentProfile READ currentProfile NOTIFY currentProfileChanged)
+    Q_PROPERTY(int currentProfileIndex READ currentProfileIndex NOTIFY currentProfileChanged)
     Q_PROPERTY(Loader* loader READ loader CONSTANT)
     Q_PROPERTY(qreal minTemp READ minTemp WRITE setMinTemp NOTIFY minTempChanged)
     Q_PROPERTY(qreal maxTemp READ maxTemp WRITE setMaxTemp NOTIFY maxTempChanged)
@@ -95,14 +97,16 @@ public:
     PwmFanModel *pwmFanModel() const { return m_pwmFanModel; }
     TempModel *tempModel() const { return m_tempModel; }
     QStringListModel *profileModel() const { return m_profileModel; }
+    QString currentProfile() const;
+    int currentProfileIndex() const;
 
     Q_INVOKABLE bool hasSystemdCommunicator() const;
     Q_INVOKABLE void apply();
     Q_INVOKABLE void reset();
-    Q_INVOKABLE void applyProfile(const QString &profile);
+    Q_INVOKABLE void applyProfile(const QString &profileName);
     Q_INVOKABLE void applyProfile(int);
-    Q_INVOKABLE void saveProfile(const QString &profile, bool updateModel = true);
-    Q_INVOKABLE void deleteProfile(const QString &profile, bool updateModel = true);
+    Q_INVOKABLE void saveProfile(const QString &profileName, bool updateModel = true);
+    Q_INVOKABLE void deleteProfile(const QString &profileName, bool updateModel = true);
     Q_INVOKABLE void deleteProfile(int, bool updateModel = true);
 
 public slots:
@@ -123,7 +127,7 @@ signals:
     void needsApplyChanged();
     void showTrayChanged();
     void startMinimizedChanged();
-    void profileChanged(int profile);
+    void currentProfileChanged();
 
 private:
 

+ 1 - 0
kcm/fancontrol-kcm.desktop

@@ -7,6 +7,7 @@ X-KDE-ServiceTypes=KCModule
 X-KDE-Library=kcm_fancontrol
 X-KDE-PluginKeyword=Fans
 X-KDE-System-Settings-Parent-Category=hardware
+X-KDE-ParentApp=kcontrol
 
 Name=Fancontrol
 Comment=Control PWM-fans

+ 169 - 272
kcm/package/contents/ui/KCM.qml

@@ -18,28 +18,31 @@
  */
 
 
-import QtQuick 2.4
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.10
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.2
 import QtQuick.Dialogs 1.2
-import org.kde.kirigami 2.0 as Kirigami
-import org.kde.kcm 1.0
+import org.kde.kirigami 2.3 as Kirigami
+import org.kde.kcm 1.0 as KCM
 import Fancontrol.Qml 1.0 as Fancontrol
 
 
-Item {
+Kirigami.Page {
     property QtObject loader: Fancontrol.Base.loader
     property QtObject systemdCom: Fancontrol.Base.systemdCom
     property QtObject pwmFanModel: Fancontrol.Base.pwmFanModel
     property QtObject tempModel: Fancontrol.Base.tempModel
     property QtObject profileModel: Fancontrol.Base.profileModel
-    property var locale: Qt.locale()
     property real textWidth: 0
     property var pwmFans: pwmFanModel.fans
+    property QtObject fan: pwmFans[fansListView.currentIndex]
 
     id: root
-    implicitWidth: 1024
-    implicitHeight: 768
+
+    implicitWidth: Kirigami.Units.gridUnit * 50
+    implicitHeight: Kirigami.Units.gridUnit * 40
+
+    KCM.ConfigModule.quickHelp: i18n("This module lets you configure your PWM fans.")
 
     Connections {
         target: Fancontrol.Base
@@ -58,11 +61,50 @@ Item {
         onAboutToDefault: enabledBox.checked = false
     }
 
+//     contextualActions: [
+//         Kirigami.Action {
+//             text: i18n("Profiles")
+//
+//             Kirigami.Action {
+//                 id: loadProfilesAction
+//
+//                 text: i18n("Load profile")
+//             }
+//             Kirigami.Action {
+//                 text: i18n("Save profile")
+//                 icon.name: "document-save"
+//                 onTriggered: Fancontrol.Base.saveProfile(profileComboBox.saveText)
+//             }
+//             Kirigami.Action {
+//                 text: i18n("Delete profile")
+//                 icon.name: "edit-delete"
+//                 onTriggered: Fancontrol.Base.deleteProfile(profileComboBox.currentIndex)
+//             }
+//         },
+//         Kirigami.Action {
+//             text: loader.sensorsDetected ? i18n("Detect fans again") : i18n("Detect fans")
+//             icon.name: "dialog-password"
+//             onTriggered: loader.detectSensors()
+//         },
+//         Kirigami.Action {
+//             visible: !!systemdCom && !!fan
+//             text: !!fan ? fan.testing ? i18n("Abort test") : i18n("Test start and stop values") : ""
+//             icon.name: "dialog-password"
+//             onTriggered: {
+//                 if (fan.testing) {
+//                     fan.abortTest();
+//                 } else {
+//                     fan.test();
+//                 }
+//             }
+//         }
+//     ]
+
     ColumnLayout {
         id: noFansInfo
 
-        width: parent.width
-        anchors.verticalCenter: parent.verticalCenter
+        width: root.width
+        y: root.height / 2 - height / 2
         spacing: Kirigami.Units.smallSpacing * 2
         visible: pwmFans.length === 0
 
@@ -81,17 +123,14 @@ Item {
         }
     }
 
-    CheckBox {
+    header: CheckBox {
         id: enabledBox
 
-        anchors.top: parent.top
-        visible: pwmFans.length > 0
         text: i18n("Control fans manually")
         checked: systemdCom.serviceEnabled && systemdCom.serviceActive;
         onCheckedChanged: {
             systemdCom.serviceActive = enabledBox.checked;
             loader.restartServiceAfterTesting = checked;
-            autostartBox.checked = true;
         }
 
         Connections {
@@ -100,303 +139,161 @@ Item {
         }
     }
 
-    ColumnLayout {
-        id: bodyLayout
-
-        width: parent.width
-        anchors.bottom: advancedButton.top
-        anchors.top: enabledBox.bottom
-        anchors.bottomMargin: advancedButton.height / 4
-        visible: enabledBox.checked
-
-        CheckBox {
-            id: autostartBox
-
-            text: i18n("Enable service at boot")
-            checked: systemdCom.serviceEnabled
-            onCheckedChanged: systemdCom.serviceEnabled = checked
-
-            Connections {
-                target: systemdCom
-                onServiceEnabledChanged: if (systemdCom.serviceEnabled != autostartBox.checked) autostartBox.checked = systemdCom.serviceEnabled
-            }
-        }
-
-        RowLayout {
-            id: profileRow
-
-            Label {
-                text: i18n("Profile:")
-                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-                renderType: Text.NativeRendering
-            }
-            ComboBox {
-                id: profileComboBox
-
-                property string saveText: editText.length > 0 ? editText : currentText
+    Rectangle {
+        id: fansListViewBackground
 
-                editable: true
-                model: profileModel
-                textRole: "display"
-                Layout.fillWidth: true
-                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+        Kirigami.Theme.colorSet: Kirigami.Theme.View
 
-                onActivated: Fancontrol.Base.applyProfile(index)
-            }
-            Button {
-                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                action: saveProfileAction
-            }
-            Button {
-                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                action: deleteProfileAction
-            }
+        anchors {
+            top: parent.top
+            bottom: parent.bottom
+            left: parent.left
         }
+        width: root.width / 5
+        color: Kirigami.Theme.backgroundColor
+        visible: enabledBox.checked
+        border.width: 1
+        border.color: Kirigami.Theme.textColor
 
-        RowLayout {
-            visible: enabledBox.checked && pwmFans.length > 0
-
-            Label {
-                text: i18n("Fan:")
-                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
-                renderType: Text.NativeRendering
-            }
-            ComboBox {
-                id: fanComboBox
+        ListView {
+            id: fansListView
 
-                model: pwmFanModel
-                textRole: "display"
-                Layout.fillWidth: true
-                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-            }
-            Button {
-                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                text: i18n("Detect fans")
-                icon.name: kcm.needsAuthorization ? "dialog-password" : ""
-                onClicked: loader.detectSensors()
+            anchors.fill: parent
+            anchors.margins: fansListViewBackground.border.width
+            clip: true
+            boundsBehavior: Flickable.StopAtBounds
+            flickableDirection: Flickable.AutoFlickIfNeeded
+            model: pwmFanModel
+            header: Kirigami.BasicListItem {
+                label: '<b>' + i18n("Fans") + '</b>'
+                reserveSpaceForIcon: false
+                hoverEnabled: false
+                separatorVisible: false
+                leftPadding: Kirigami.Units.smallSpacing
             }
-        }
-
-        Loader {
-            Layout.fillWidth: true
-            Layout.fillHeight: true
-            active: pwmFans.length > fanComboBox.currentIndex && fanComboBox.currentIndex >= 0
-            sourceComponent: Fancontrol.FanItem {
-                unit: Fancontrol.Base.unit
-                fan: pwmFans[fanComboBox.currentIndex]
-                systemdCom: root.systemdCom
-                tempModel: root.tempModel
-                minTemp: Fancontrol.Base.minTemp
-                maxTemp: Fancontrol.Base.maxTemp
+            delegate: Kirigami.BasicListItem {
+                label: display
+                reserveSpaceForIcon: false
+                hoverEnabled: true
+                highlighted: ListView.isCurrentItem
+                separatorVisible: false
+
+                onPressedChanged: if (pressed) fansListView.currentIndex = index;
             }
         }
     }
 
-    Item {
-        id: advancedButton
-
-        property bool expanded: false
+    Fancontrol.FanHeader {
+        id: fanHeader
 
-        anchors.bottom: settingsArea.top
-        width: parent.width
-        height: advancedArrow.height
+        fan: root.fan
         visible: enabledBox.checked
-
-        Image {
-            id: advancedArrow
-
-            source: parent.expanded ? "image://icon/go-down" : "image://icon/go-next"
-            fillMode: Image.PreserveAspectFit
-            height: advancedLabel.implicitHeight
+        anchors {
+            top: parent.top
+            left: fansListViewBackground.right
+            right: parent.right
+            margins: Kirigami.Units.smallSpacing
         }
-        Label {
-            id: advancedLabel
+    }
 
-            anchors.left: advancedArrow.right
-            text: i18n("Advanced settings")
-            font.bold: true
+    Loader {
+        anchors {
+            top: fanHeader.bottom
+            left: fansListViewBackground.right
+            right: parent.right
+            bottom: settingsColumn.top
         }
-        MouseArea {
-            anchors.fill: parent
-            onClicked: parent.expanded = !parent.expanded
+        active: !!root.fan
+        visible: enabledBox.checked
+        sourceComponent: Fancontrol.FanItem {
+            unit: Fancontrol.Base.unit
+            fan: root.fan
+            systemdCom: root.systemdCom
+            tempModel: root.tempModel
+            minTemp: Fancontrol.Base.minTemp
+            maxTemp: Fancontrol.Base.maxTemp
         }
     }
 
-    ColumnLayout {
-        id: settingsArea
-
-        visible: enabledBox.checked && parent.height - enabledBox.height - bodyLayout.height - advancedButton.height > height
-        width: parent.width
-        anchors.bottom: parent.bottom
-        clip: true
-        state: advancedButton.expanded ? "VISIBLE" : "HIDDEN"
+    Column {
+        id: settingsColumn
 
-        states: [
-            State {
-                name: "VISIBLE"
-
-                PropertyChanges {
-                    target: settingsArea
-                    height: settingsArea.implicitHeight
-                }
-            },
-            State {
-                name: "HIDDEN"
+        anchors {
+            left: fansListViewBackground.right
+            right: parent.right
+            bottom: parent.bottom
+        }
+        height: childrenRect.height
+        visible: enabledBox.checked
 
-                PropertyChanges {
-                    target: settingsArea
-                    height: 0
-                }
-            }
-        ]
+        Item {
+            id: advancedButton
 
-        transitions: Transition {
-            NumberAnimation {
-                properties: "height"
-                easing.type: Easing.InOutQuad
-            }
-        }
+            property bool expanded: false
 
-        RowLayout {
             width: parent.width
+            height: childrenRect.height
 
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Interval:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            SpinBox {
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                value: loader.interval
-                editable: true
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 0) + ' ' + i18np("second", "seconds", value) }
-                from: 1.0
-                onValueModified: loader.interval = value
-            }
-        }
-        RowLayout {
-            width: parent.width
+            Image {
+                id: advancedArrow
 
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Minimum temperature for fan graphs:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            SpinBox {
-                id: minTempBox
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                to: maxTempBox.value
-                from: Units.fromKelvin(0, Fancontrol.Base.unit)
-                value: Units.fromCelsius(Fancontrol.Base.minTemp, Fancontrol.Base.unit)
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 2) + Fancontrol.Base.unit }
-                onValueChanged: Fancontrol.Base.minTemp = Units.toCelsius(value, Fancontrol.Base.unit)
+                source: parent.expanded ? "image://icon/go-down" : "image://icon/go-next"
+                fillMode: Image.PreserveAspectFit
+                height: advancedLabel.implicitHeight
             }
-        }
-        RowLayout {
-            width: parent.width
-
             Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Maximum temperature for fan graphs:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            SpinBox {
-                id: maxTempBox
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                from: minTempBox.value
-                value: Units.fromCelsius(Fancontrol.Base.maxTemp, Fancontrol.Base.unit)
-                editable: true
-                textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 2) + Fancontrol.Base.unit }
-                onValueChanged: Fancontrol.Base.maxTemp = Units.toCelsius(value, Fancontrol.Base.unit)
-            }
-        }
-        RowLayout {
-            width: parent.width
+                id: advancedLabel
 
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Name of the fancontrol systemd service:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
+                anchors.left: advancedArrow.right
+                text: i18n("Advanced settings")
+                font.bold: true
             }
-            Fancontrol.OptionInput {
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                color: systemdCom.serviceExists ? "green" : "red"
-                value: Fancontrol.Base.serviceName
-                onTextChanged: Fancontrol.Base.serviceName = text
+            MouseArea {
+                anchors.fill: parent
+                onClicked: parent.expanded = !parent.expanded
             }
         }
-        RowLayout {
+
+        Fancontrol.SettingsForm {
+            id: settingsArea
+
             width: parent.width
+            showAll: false
+            clip: true
+            state: advancedButton.expanded ? "VISIBLE" : "HIDDEN"
+
+            states: [
+                State {
+                    name: "VISIBLE"
+
+                    PropertyChanges {
+                        target: settingsArea
+                        height: implicitHeight
+                    }
+                },
+                State {
+                    name: "HIDDEN"
+
+                    PropertyChanges {
+                        target: settingsArea
+                        height: 0
+                    }
+                }
+            ]
 
-            Label {
-                Layout.preferredWidth: root.textWidth
-                clip: true
-                text: i18n("Path to the fancontrol config file:")
-                horizontalAlignment: Text.AlignRight
-                Component.onCompleted: root.textWidth = Math.max(root.textWidth, contentWidth)
-            }
-            Fancontrol.OptionInput {
-                Layout.minimumWidth: implicitWidth
-                Layout.fillWidth: true
-                value: Fancontrol.Base.configUrl.toString().replace("file://", "")
-                color: Fancontrol.Base.configValid ? "green" : "red"
-                onTextChanged: Fancontrol.Base.configUrl = text
-            }
-            Button {
-                action: loadAction
+            transitions: Transition {
+                NumberAnimation {
+                    properties: "height"
+                    easing.type: Easing.InOutQuad
+                }
             }
         }
     }
 
-    Action {
-        id: saveProfileAction
-
-        text: i18n("Save profile")
-        icon.name: "document-save"
-        onTriggered: Fancontrol.Base.saveProfile(profileComboBox.saveText)
-    }
-    Action {
-        id: deleteProfileAction
-
-        text: i18n("Delete profile")
-        icon.name: "edit-delete"
-        onTriggered: Fancontrol.Base.deleteProfile(profileComboBox.currentIndex)
-    }
-    Action {
-        id: loadAction
-
-        icon.name: "document-open"
-        onTriggered: openFileDialog.open()
-//         tooltip: i18n("Load configuration file")
-        shortcut: StandardKey.Open
-    }
-
-    FileDialog {
-        id: openFileDialog
-
-        title: i18n("Please choose a configuration file")
-        folder: "file:///etc"
-        selectExisting: true
-        selectMultiple: false
-
-        onAccepted: Fancontrol.Base.configUrl = fileUrl;
-    }
-
     Fancontrol.ErrorDialog {
         id: errorDialog
 
         modal: true
+        anchors.centerIn: root
     }
 }