From 2c0075ec6bcf84049647c4676a6d543f216b23bb Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 8 Sep 2021 14:23:32 +0200 Subject: [PATCH 1/3] Improve lights control page --- nymea-app/ui/components/BrightnessSlider.qml | 11 +- .../ui/components/ColorTemperaturePicker.qml | 80 +++++---- nymea-app/ui/devicepages/LightDevicePage.qml | 153 +++++++++--------- 3 files changed, 131 insertions(+), 113 deletions(-) diff --git a/nymea-app/ui/components/BrightnessSlider.qml b/nymea-app/ui/components/BrightnessSlider.qml index edc3d570..5f2d32cb 100644 --- a/nymea-app/ui/components/BrightnessSlider.qml +++ b/nymea-app/ui/components/BrightnessSlider.qml @@ -35,8 +35,8 @@ import "../utils" Item { id: root - implicitWidth: orientation == Qt.Horizontal ? 300 : Style.hugeIconSize - implicitHeight: orientation == Qt.Horizontal ? Style.hugeIconSize : 300 + implicitWidth: orientation == Qt.Horizontal ? 300 : 12 + implicitHeight: orientation == Qt.Horizontal ? 12 : 300 property Thing thing: null @@ -84,14 +84,15 @@ Item { y: root.orientation === Qt.Vertical ? root.height - dragHandle.height - ((actionQueue.pendingValue || root.value) * (root.height - dragHandle.height) / 100) : 0 - height: root.orientation === Qt.Horizontal ? parent.height : 8 - width: root.orientation === Qt.Horizontal ? 8 : parent.width - radius: 4 + height: 14 + width: 14 + radius: 7 color: Style.foregroundColor } MouseArea { anchors.fill: parent + anchors.margins: -Style.smallMargins onPositionChanged: { var minCt = root.colorTemperatureStateType.minValue; var maxCt = root.colorTemperatureStateType.maxValue diff --git a/nymea-app/ui/components/ColorTemperaturePicker.qml b/nymea-app/ui/components/ColorTemperaturePicker.qml index 483b49a6..a5dc60cb 100644 --- a/nymea-app/ui/components/ColorTemperaturePicker.qml +++ b/nymea-app/ui/components/ColorTemperaturePicker.qml @@ -23,16 +23,10 @@ Item { } Rectangle { - id: clipRect - anchors.fill: parent - radius: Style.cornerRadius - } - - LinearGradient { - anchors.fill: parent - start: root.orientation == Qt.Horizontal ? Qt.point(0, 0) : Qt.point(0, height) - end: root.orientation == Qt.Horizontal ? Qt.point(width, 0) : Qt.point(0, 0) - source: clipRect + width: Math.min(parent.width, parent.height) + anchors.centerIn: parent + height: width + radius: width / 2 gradient: Gradient { GradientStop { position: 0.0; color: "#dfffff" } GradientStop { position: 0.5; color: "#ffffea" } @@ -40,30 +34,48 @@ Item { } } - Rectangle { - id: dragHandle - property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) - x: orientation == Qt.Horizontal ? valuePercentage * (root.width - dragHandle.width) : 0 - y: root.orientation === Qt.Vertical ? root.height - dragHandle.height - (valuePercentage * (root.height - dragHandle.height)) : 0 - height: root.orientation == Qt.Horizontal ? parent.height : 8 - width: root.orientation == Qt.Horizontal ? 8 : parent.width - radius: 4 - color: Qt.tint(Style.backgroundColor, Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.5)) - } +// Rectangle { +// id: clipRect +// anchors.fill: parent +// radius: Style.cornerRadius +// } - MouseArea { - anchors.fill: parent - onPositionChanged: { - var minCt = root.colorTemperatureStateType.minValue; - var maxCt = root.colorTemperatureStateType.maxValue - var ct; - if (root.orientation == Qt.Horizontal) { - ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) - } else { - ct = Math.min(maxCt, Math.max(minCt, ((height - mouseY) * (maxCt - minCt) / (height - dragHandle.height)) + minCt)) - } - actionQueue.sendValue(ct); - } - } +// LinearGradient { +// anchors.fill: parent +// start: root.orientation == Qt.Horizontal ? Qt.point(0, 0) : Qt.point(0, height) +// end: root.orientation == Qt.Horizontal ? Qt.point(width, 0) : Qt.point(0, 0) +// source: clipRect +// gradient: Gradient { +// GradientStop { position: 0.0; color: "#dfffff" } +// GradientStop { position: 0.5; color: "#ffffea" } +// GradientStop { position: 1.0; color: "#ffd649" } +// } +// } + +// Rectangle { +// id: dragHandle +// property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) +// x: orientation == Qt.Horizontal ? valuePercentage * (root.width - dragHandle.width) : 0 +// y: root.orientation === Qt.Vertical ? root.height - dragHandle.height - (valuePercentage * (root.height - dragHandle.height)) : 0 +// height: root.orientation == Qt.Horizontal ? parent.height : 8 +// width: root.orientation == Qt.Horizontal ? 8 : parent.width +// radius: 4 +// color: Qt.tint(Style.backgroundColor, Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.5)) +// } + +// MouseArea { +// anchors.fill: parent +// onPositionChanged: { +// var minCt = root.colorTemperatureStateType.minValue; +// var maxCt = root.colorTemperatureStateType.maxValue +// var ct; +// if (root.orientation == Qt.Horizontal) { +// ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) +// } else { +// ct = Math.min(maxCt, Math.max(minCt, ((height - mouseY) * (maxCt - minCt) / (height - dragHandle.height)) + minCt)) +// } +// actionQueue.sendValue(ct); +// } +// } } diff --git a/nymea-app/ui/devicepages/LightDevicePage.qml b/nymea-app/ui/devicepages/LightDevicePage.qml index cbf445a1..4be900b2 100644 --- a/nymea-app/ui/devicepages/LightDevicePage.qml +++ b/nymea-app/ui/devicepages/LightDevicePage.qml @@ -30,7 +30,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.1 +import QtQuick.Layouts 1.3 import QtQuick.Controls.Material 2.1 import Nymea 1.0 import QtGraphicalEffects 1.0 @@ -57,10 +57,10 @@ ThingPageBase { GridLayout { anchors.fill: parent - anchors.margins: app.margins + anchors.margins: Style.bigMargins columns: app.landscape ? root.statesCount : 1 - rowSpacing: app.margins - columnSpacing: app.margins + rowSpacing: Style.bigMargins + columnSpacing: Style.bigMargins Layout.alignment: Qt.AlignCenter GridLayout { @@ -79,72 +79,88 @@ ThingPageBase { ListElement { name: "reading"; ct: "57"; bri: 100; color: "#f4de00" } ListElement { name: "relax"; ct: "95" ; bri: 55; color: "#ffaf2a"} } - delegate: Item { - Layout.preferredWidth: Style.hugeIconSize + delegate: ProgressButton { Layout.preferredHeight: Style.hugeIconSize - Layout.fillWidth: true - Layout.fillHeight: app.landscape - ItemDelegate { - height: Style.hugeIconSize - width: height - anchors.centerIn: parent - - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - - contentItem: Rectangle { - color: model.color - radius: Style.cornerRadius - - ColorIcon { - anchors.fill: parent - name: "../images/lighting/" + model.name + ".svg" - color: "white" - } - } + Layout.preferredWidth: Style.hugeIconSize + imageSource: "../images/lighting/" + model.name + ".svg" + longpressEnabled: false +// mode: "normal" +// backgroundColor: model.color - onClicked: { - // Translate from % to absolute value in min/max - // % : 100 = abs : (max - min) - print("min,max", root.ctStateType, root.ctStateType.minValue, root.ctStateType.maxValue) - var absoluteCtValue = (model.ct * (root.ctStateType.maxValue - root.ctStateType.minValue) / 100) + root.ctStateType.minValue - var params = []; - var param1 = {}; - param1["paramName"] = root.ctActionType.paramTypes.get(0).name; - param1["value"] = absoluteCtValue; - params.push(param1) - root.thing.executeAction(root.ctActionType.name, params) - params = []; - param1 = {}; - param1["paramName"] = root.brightnessActionType.paramTypes.get(0).name; - param1["value"] = model.bri; - params.push(param1) - root.thing.executeAction(root.brightnessActionType.name, params) - } + onClicked: { + // Translate from % to absolute value in min/max + // % : 100 = abs : (max - min) + print("min,max", root.ctStateType, root.ctStateType.minValue, root.ctStateType.maxValue) + var absoluteCtValue = (model.ct * (root.ctStateType.maxValue - root.ctStateType.minValue) / 100) + root.ctStateType.minValue + var params = []; + var param1 = {}; + param1["paramName"] = root.ctActionType.paramTypes.get(0).name; + param1["value"] = absoluteCtValue; + params.push(param1) + root.thing.executeAction(root.ctActionType.name, params) + params = []; + param1 = {}; + param1["paramName"] = root.brightnessActionType.paramTypes.get(0).name; + param1["value"] = model.bri; + params.push(param1) + root.thing.executeAction(root.brightnessActionType.name, params) } } } } - ColorPicker { + StackLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.maximumHeight: width - thing: root.thing - visible: root.thing.thingClass.stateTypes.findByName("color") !== null + currentIndex: selectionTabs.currentIndex + + Repeater { + model: modeModel + delegate: Loader { + sourceComponent: model.comp + } + } + + Component { + id: colorPickerComponent + ColorPicker { + anchors.fill: parent + thing: root.thing + } + } + + Component { + id: colorTemperatureComponent + ColorTemperaturePicker { + anchors.fill: parent + thing: root.thing + orientation: app.landscape ? Qt.Vertical : Qt.Horizontal + } + } } - ColorTemperaturePicker { - Layout.fillWidth: !app.landscape - Layout.fillHeight: app.landscape - thing: root.thing - orientation: app.landscape ? Qt.Vertical : Qt.Horizontal - visible: root.thing.thingClass.stateTypes.findByName("colorTemperature") !== null + ListModel { + id: modeModel + Component.onCompleted: { + if (root.thing.thingClass.stateTypes.findByName("color") !== null) { + append({modelData: qsTr("Color"), comp: colorPickerComponent}) + } + if (root.thing.thingClass.stateTypes.findByName("colorTemperature") !== null) { + append({modelData: qsTr("Temperature"), comp: colorTemperatureComponent}) + } + } } + SelectionTabs { + id: selectionTabs + Layout.fillWidth: true + model: modeModel + visible: modeModel.count > 1 + } + + GridLayout { id: basicItems Layout.fillWidth: !app.landscape @@ -154,25 +170,14 @@ ThingPageBase { rowSpacing: app.margins columns: (app.landscape && (root.colorState !== null && root.ctState !== null)) || (!app.landscape && (root.colorState === null && root.ctState === null)) ? 1 : 2 - Rectangle { - Layout.preferredWidth: Style.hugeIconSize - Layout.preferredHeight: width - radius: Style.cornerRadius - color: root.colorState ? root.colorState.value : "red" -// color: Qt.tint(Style.backgroundColor, Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.1)) - ColorIcon { - anchors.centerIn: parent - height: Style.bigIconSize - width: height - name: root.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" - color: root.colorState ? - NymeaUtils.isDark(root.colorState.value) ? "white" : "black" : "white" - } - MouseArea { - anchors.fill: parent - onClicked: { - root.thing.executeAction("power", [{paramName: "power", value: !root.powerState.value}]) - } + + ProgressButton { + imageSource: root.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" + mode: "normal" + size: Style.bigIconSize + longpressEnabled: false + onClicked: { + root.thing.executeAction("power", [{paramName: "power", value: !root.powerState.value}]) } } From 554575698bc6bc15bd4f2bb36c9f0a7d5898747c Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 19 Sep 2021 18:00:28 +0200 Subject: [PATCH 2/3] More work on the lights pages --- nymea-app/configuredhostsmodel.cpp | 1 + nymea-app/resources.qrc | 2 +- nymea-app/ui/components/ColorPicker.qml | 146 +++++++- .../ui/components/ColorTemperaturePicker.qml | 71 ++-- nymea-app/ui/delegates/InterfaceTile.qml | 2 +- .../devicelistpages/LightThingsListPage.qml | 2 +- nymea-app/ui/devicepages/LightDevicePage.qml | 193 ---------- nymea-app/ui/devicepages/LightThingPage.qml | 338 ++++++++++++++++++ nymea-app/ui/utils/NymeaUtils.qml | 2 +- 9 files changed, 514 insertions(+), 243 deletions(-) delete mode 100644 nymea-app/ui/devicepages/LightDevicePage.qml create mode 100644 nymea-app/ui/devicepages/LightThingPage.qml diff --git a/nymea-app/configuredhostsmodel.cpp b/nymea-app/configuredhostsmodel.cpp index 27b2f005..0a97f0c4 100644 --- a/nymea-app/configuredhostsmodel.cpp +++ b/nymea-app/configuredhostsmodel.cpp @@ -171,6 +171,7 @@ void ConfiguredHostsModel::saveToDisk() QSettings settings; settings.beginGroup("ConfiguredHosts"); settings.remove(""); + settings.setValue("currentIndex", m_currentIndex); for (int i = 0; i < m_list.count(); i++) { settings.beginGroup(QString::number(i)); settings.setValue("uuid", m_list.at(i)->uuid()); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 6b5f9e70..b18e92d8 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -58,7 +58,7 @@ ui/devicepages/GarageThingPage.qml ui/devicepages/AwningThingPage.qml ui/devicepages/NotificationsDevicePage.qml - ui/devicepages/LightDevicePage.qml + ui/devicepages/LightThingPage.qml ui/devicepages/FingerprintReaderDevicePage.qml ui/devicepages/DeviceLogPage.qml ui/devicelistpages/GenericThingsListPage.qml diff --git a/nymea-app/ui/components/ColorPicker.qml b/nymea-app/ui/components/ColorPicker.qml index ebd4bc07..6d73a2ca 100644 --- a/nymea-app/ui/components/ColorPicker.qml +++ b/nymea-app/ui/components/ColorPicker.qml @@ -8,10 +8,25 @@ Item { property Thing thing: null + readonly property State colorState: thing ? thing.stateByName("color") : null + readonly property State powerState: thing ? thing.stateByName("power") : null + + Connections { + target: colorState + onValueChanged: { + if (actionQueue.pendingValue === null) { + actionQueue.useStoredPoint = false + } + } + } + ActionQueue { id: actionQueue thing: root.thing stateType: thing.thingClass.stateTypes.findByName("color") + + property bool useStoredPoint: false + property point storedPoint: Qt.point(0, 0) } ConicalGradient { @@ -30,6 +45,29 @@ Item { GradientStop { position: 0.833; color: Qt.rgba(1, 0, 1, 1) } GradientStop { position: 1.000; color: Qt.rgba(1, 0, 0, 1) } } + onWidthChanged: dragHandle.updatePoint() + onHeightChanged: dragHandle.updatePoint() + + RadialGradient { + anchors.fill: gradient + gradient: Gradient{ + GradientStop { position: 0.05; color: Qt.rgba(1, 1, 1, 1) } + GradientStop { position: 0.10; color: Qt.rgba(1, 1, 1, .9) } + GradientStop { position: 0.20; color: Qt.rgba(1, 1, 1, .7) } + GradientStop { position: 0.30; color: Qt.rgba(1, 1, 1, .5) } + GradientStop { position: 0.40; color: Qt.rgba(1, 1, 1, .3) } + GradientStop { position: 0.50; color: "transparent" } + } + } + } + + Desaturate { + id: colorizer + anchors.fill: gradient + source: gradient + desaturation: root.powerState.value === true ? 0 : 1 + Behavior on desaturation { NumberAnimation { duration: Style.animationDuration } } + visible: false } Rectangle { @@ -39,19 +77,100 @@ Item { } OpacityMask { anchors.fill: gradient - source: gradient + source: colorizer maskSource: mask } - RadialGradient { - anchors.fill: gradient - gradient: Gradient{ - GradientStop { position: 0.05; color: Qt.rgba(1, 1, 1, 1) } - GradientStop { position: 0.10; color: Qt.rgba(1, 1, 1, .9) } - GradientStop { position: 0.20; color: Qt.rgba(1, 1, 1, .7) } - GradientStop { position: 0.30; color: Qt.rgba(1, 1, 1, .5) } - GradientStop { position: 0.40; color: Qt.rgba(1, 1, 1, .3) } - GradientStop { position: 0.50; color: "transparent" } + + Rectangle { + id: dragHandle + width: 20 + height: 20 + radius: height / 2 + color: Style.backgroundColor + border.color: Style.foregroundColor + border.width: 2 + + x: point.x + gradient.width / 2 + gradient.x - width / 2 + y: point.y + gradient.height / 2 + gradient.y - height / 2 + + property color shownColor: root.colorState ? actionQueue.pendingValue || root.colorState.value : "white`" + onShownColorChanged: updatePoint() +// Component.onCompleted: updatePoint() + + property point point: Qt.point(0,0); + function updatePoint() { + + if (actionQueue.useStoredPoint) { + point = actionQueue.storedPoint + return + } + + print("current color:", shownColor.r, shownColor.g, shownColor.b) + + var whitePart = Math.min(Math.min(shownColor.r, shownColor.g), shownColor.b) + + var stopIndex = 0 + var progressInStop = 0 + if (shownColor.r === 1) { + if (shownColor.g > shownColor.b) { + stopIndex = 0 + progressInStop = shownColor.g - whitePart + } else { + stopIndex = 5 + progressInStop = 1 - shownColor.b + whitePart + } + } + if (shownColor.g === 1) { + if (shownColor.r > shownColor.b) { + stopIndex = 1 + progressInStop = 1 - shownColor.r + whitePart + } else { + stopIndex = 2 + progressInStop = shownColor.b - whitePart + } + } + if (shownColor.b === 1) { + if (shownColor.r > shownColor.g) { + stopIndex = 4 + progressInStop = shownColor.r - whitePart + } else { + stopIndex = 3 + progressInStop = 1-shownColor.g + whitePart + } + } + + var stopBefore = g.stops[stopIndex] + var stopAfter = g.stops[stopIndex+1] + + print("stopIndex", stopIndex) + print("stopBefore:", stopBefore.color.r, stopBefore.color.g, stopBefore.color.b) + print("stopAfter:", stopAfter.color.r, stopAfter.color.g, stopAfter.color.b) + print("progressInStop", progressInStop) + + + print("beforePosition", stopBefore.position) + + var positionInGradient = stopBefore.position + (stopAfter.position - stopBefore.position) * progressInStop + + print("positionInGradient", positionInGradient) + + var degrees = 360 * positionInGradient; + degrees -= 90; + + var radian = degrees * 0.0174532925 + + var radius = gradient.height * 0.9 / 2 * (1-whitePart) + + + var x = radius * Math.cos(radian) + var y = radius * Math.sin(radian) + + print("degrees", degrees) + print("radius", radius) + + print("Setting point to", x, y) + point = Qt.point(x, y) } } @@ -98,6 +217,13 @@ Item { 1) actionQueue.sendValue(color); + + // Store the coordinates (limited to the circle) as the above calculation is lossy so we can't precicely + // calcuate the position from the color but we don't want the drag handle jumping while dragging. + var rad = (angle - 90) / 180 * Math.PI + var radius = Math.min(distanceFromCenter, width * 0.9 / 2) + actionQueue.storedPoint = Qt.point(radius * Math.cos(rad), radius * Math.sin(rad)) + actionQueue.useStoredPoint = true } function calculateAngle(mouseX, mouseY) { diff --git a/nymea-app/ui/components/ColorTemperaturePicker.qml b/nymea-app/ui/components/ColorTemperaturePicker.qml index a5dc60cb..a734ef39 100644 --- a/nymea-app/ui/components/ColorTemperaturePicker.qml +++ b/nymea-app/ui/components/ColorTemperaturePicker.qml @@ -10,9 +10,10 @@ Item { property Thing thing: null - property int orientation: Qt.Horizontal + property int orientation: Qt.Vertical readonly property StateType colorTemperatureStateType: root.thing.thingClass.stateTypes.findByName("colorTemperature") + readonly property State powerState: root.thing.stateByName("power") property int value: thing.stateByName("colorTemperature").value @@ -23,10 +24,12 @@ Item { } Rectangle { + id: background width: Math.min(parent.width, parent.height) anchors.centerIn: parent height: width radius: width / 2 + visible: false gradient: Gradient { GradientStop { position: 0.0; color: "#dfffff" } GradientStop { position: 0.5; color: "#ffffea" } @@ -34,48 +37,44 @@ Item { } } -// Rectangle { -// id: clipRect -// anchors.fill: parent -// radius: Style.cornerRadius -// } + Desaturate { + anchors.fill: background + source: background + desaturation: root.powerState.value === true ? 0 : 1 + Behavior on desaturation { NumberAnimation { duration: Style.animationDuration } } + } -// LinearGradient { -// anchors.fill: parent -// start: root.orientation == Qt.Horizontal ? Qt.point(0, 0) : Qt.point(0, height) -// end: root.orientation == Qt.Horizontal ? Qt.point(width, 0) : Qt.point(0, 0) -// source: clipRect -// gradient: Gradient { -// GradientStop { position: 0.0; color: "#dfffff" } -// GradientStop { position: 0.5; color: "#ffffea" } -// GradientStop { position: 1.0; color: "#ffd649" } -// } -// } + Rectangle { + id: dragHandle + property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) + width: 20 + height: 20 + radius: height / 2 + color: Style.backgroundColor + border.color: Style.foregroundColor + border.width: 2 + x: (parent.width - width) / 2 + y: parent.height * valuePercentage - (height / 2) -// Rectangle { -// id: dragHandle -// property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) -// x: orientation == Qt.Horizontal ? valuePercentage * (root.width - dragHandle.width) : 0 -// y: root.orientation === Qt.Vertical ? root.height - dragHandle.height - (valuePercentage * (root.height - dragHandle.height)) : 0 -// height: root.orientation == Qt.Horizontal ? parent.height : 8 -// width: root.orientation == Qt.Horizontal ? 8 : parent.width -// radius: 4 -// color: Qt.tint(Style.backgroundColor, Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.5)) -// } + } -// MouseArea { -// anchors.fill: parent -// onPositionChanged: { -// var minCt = root.colorTemperatureStateType.minValue; -// var maxCt = root.colorTemperatureStateType.maxValue -// var ct; + MouseArea { + anchors.fill: parent + onPositionChanged: { + var minCt = root.colorTemperatureStateType.minValue; + var maxCt = root.colorTemperatureStateType.maxValue + var ct; // if (root.orientation == Qt.Horizontal) { // ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) // } else { + // ct : y = max : height + ct = mouseY * (maxCt - minCt) / height + minCt + ct = Math.min(maxCt, ct) + ct = Math.max(minCt, ct) // ct = Math.min(maxCt, Math.max(minCt, ((height - mouseY) * (maxCt - minCt) / (height - dragHandle.height)) + minCt)) // } -// actionQueue.sendValue(ct); -// } -// } + actionQueue.sendValue(ct); + } + } } diff --git a/nymea-app/ui/delegates/InterfaceTile.qml b/nymea-app/ui/delegates/InterfaceTile.qml index 77392fa6..4f99725b 100644 --- a/nymea-app/ui/delegates/InterfaceTile.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -209,7 +209,7 @@ MainPageTile { case "light": var group = engine.thingManager.createGroup(Interfaces.findByName("colorlight"), thingsProxy); print("opening lights page for group", group) - pageStack.push("../devicepages/LightDevicePage.qml", {thing: group}) + pageStack.push("../devicepages/LightThingPage.qml", {thing: group}) } } } diff --git a/nymea-app/ui/devicelistpages/LightThingsListPage.qml b/nymea-app/ui/devicelistpages/LightThingsListPage.qml index 884dbe39..f11ef46c 100644 --- a/nymea-app/ui/devicelistpages/LightThingsListPage.qml +++ b/nymea-app/ui/devicelistpages/LightThingsListPage.qml @@ -107,7 +107,7 @@ ThingsListPageBase { property bool colorInverted: tileColored && NymeaUtils.isDark(Style.foregroundColor) === NymeaUtils.isDark(colorState.value) onClicked: { - if (isEnabled && (colorState || colorTemperatureState)) { + if (isEnabled /*&& (colorState || colorTemperatureState)*/) { root.enterPage(index) } else { itemDelegate.wobble() diff --git a/nymea-app/ui/devicepages/LightDevicePage.qml b/nymea-app/ui/devicepages/LightDevicePage.qml deleted file mode 100644 index 4be900b2..00000000 --- a/nymea-app/ui/devicepages/LightDevicePage.qml +++ /dev/null @@ -1,193 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project 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 General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.9 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 -import QtQuick.Controls.Material 2.1 -import Nymea 1.0 -import QtGraphicalEffects 1.0 -import "../components" - -ThingPageBase { - id: root - - readonly property State powerState: thing.stateByName("power") - - readonly property State brightnessState: thing.stateByName("brightness") - readonly property ActionType brightnessActionType: thingClass.actionTypes.findByName("brightness"); - - readonly property State colorState: thing.stateByName("color") - - readonly property StateType ctStateType: thingClass.stateTypes.findByName("colorTemperature") - readonly property State ctState: thing.stateByName("colorTemperature") - readonly property ActionType ctActionType: thingClass.actionTypes.findByName("colorTemperature") - - readonly property int statesCount: (powerState !== null ? 1 : 0) + - (brightnessState !== null ? 1 : 0) + - (ctState !== null ? 1 : 0) + - (colorState !== null ? 1 : 0) - - GridLayout { - anchors.fill: parent - anchors.margins: Style.bigMargins - columns: app.landscape ? root.statesCount : 1 - rowSpacing: Style.bigMargins - columnSpacing: Style.bigMargins - Layout.alignment: Qt.AlignCenter - - GridLayout { - Layout.fillHeight: true - Layout.fillWidth: !app.landscape - columnSpacing: app.margins - rowSpacing: app.margins - Layout.alignment: Qt.AlignHCenter - visible: root.ctStateType !== null - columns: app.landscape ? 1 : 4 - - Repeater { - model: ListModel { - ListElement { name: "activate"; ct: "0"; bri: 100; color: "#00c5ff" } - ListElement { name: "concentrate"; ct: "23"; bri: 100; color: "#3dddff" } - ListElement { name: "reading"; ct: "57"; bri: 100; color: "#f4de00" } - ListElement { name: "relax"; ct: "95" ; bri: 55; color: "#ffaf2a"} - } - delegate: ProgressButton { - Layout.preferredHeight: Style.hugeIconSize - Layout.preferredWidth: Style.hugeIconSize - imageSource: "../images/lighting/" + model.name + ".svg" - longpressEnabled: false -// mode: "normal" -// backgroundColor: model.color - - - onClicked: { - // Translate from % to absolute value in min/max - // % : 100 = abs : (max - min) - print("min,max", root.ctStateType, root.ctStateType.minValue, root.ctStateType.maxValue) - var absoluteCtValue = (model.ct * (root.ctStateType.maxValue - root.ctStateType.minValue) / 100) + root.ctStateType.minValue - var params = []; - var param1 = {}; - param1["paramName"] = root.ctActionType.paramTypes.get(0).name; - param1["value"] = absoluteCtValue; - params.push(param1) - root.thing.executeAction(root.ctActionType.name, params) - params = []; - param1 = {}; - param1["paramName"] = root.brightnessActionType.paramTypes.get(0).name; - param1["value"] = model.bri; - params.push(param1) - root.thing.executeAction(root.brightnessActionType.name, params) - } - } - } - } - - StackLayout { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.maximumHeight: width - currentIndex: selectionTabs.currentIndex - - Repeater { - model: modeModel - delegate: Loader { - sourceComponent: model.comp - } - } - - Component { - id: colorPickerComponent - ColorPicker { - anchors.fill: parent - thing: root.thing - } - } - - Component { - id: colorTemperatureComponent - ColorTemperaturePicker { - anchors.fill: parent - thing: root.thing - orientation: app.landscape ? Qt.Vertical : Qt.Horizontal - } - } - } - - ListModel { - id: modeModel - Component.onCompleted: { - if (root.thing.thingClass.stateTypes.findByName("color") !== null) { - append({modelData: qsTr("Color"), comp: colorPickerComponent}) - } - if (root.thing.thingClass.stateTypes.findByName("colorTemperature") !== null) { - append({modelData: qsTr("Temperature"), comp: colorTemperatureComponent}) - } - } - } - - SelectionTabs { - id: selectionTabs - Layout.fillWidth: true - model: modeModel - visible: modeModel.count > 1 - } - - - GridLayout { - id: basicItems - Layout.fillWidth: !app.landscape - Layout.fillHeight: app.landscape - Layout.alignment: Qt.AlignHCenter - columnSpacing: app.margins - rowSpacing: app.margins - columns: (app.landscape && (root.colorState !== null && root.ctState !== null)) - || (!app.landscape && (root.colorState === null && root.ctState === null)) ? 1 : 2 - - ProgressButton { - imageSource: root.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" - mode: "normal" - size: Style.bigIconSize - longpressEnabled: false - onClicked: { - root.thing.executeAction("power", [{paramName: "power", value: !root.powerState.value}]) - } - } - - BrightnessSlider { - Layout.fillWidth: orientation == Qt.Horizontal - Layout.fillHeight: orientation == Qt.Vertical - thing: root.thing - orientation: basicItems.columns === 1 ? Qt.Vertical : Qt.Horizontal - visible: root.thing.stateByName("brightness") !== null - } - } - } -} diff --git a/nymea-app/ui/devicepages/LightThingPage.qml b/nymea-app/ui/devicepages/LightThingPage.qml new file mode 100644 index 00000000..2892b23b --- /dev/null +++ b/nymea-app/ui/devicepages/LightThingPage.qml @@ -0,0 +1,338 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.9 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.1 +import Nymea 1.0 +import QtGraphicalEffects 1.0 +import "../components" +import "../utils" + +ThingPageBase { + id: root + + readonly property State powerState: thing.stateByName("power") + + readonly property State brightnessState: thing.stateByName("brightness") + readonly property ActionType brightnessActionType: thingClass.actionTypes.findByName("brightness"); + + readonly property State colorState: thing.stateByName("color") + + readonly property StateType ctStateType: thingClass.stateTypes.findByName("colorTemperature") + readonly property State ctState: thing.stateByName("colorTemperature") + readonly property ActionType ctActionType: thingClass.actionTypes.findByName("colorTemperature") + + readonly property int statesCount: (powerState !== null ? 1 : 0) + + (brightnessState !== null ? 1 : 0) + + (ctState !== null ? 1 : 0) + + (colorState !== null ? 1 : 0) + + GridLayout { + anchors.fill: parent + anchors.margins: Style.bigMargins + columns: app.landscape ? root.statesCount : 1 + rowSpacing: Style.bigMargins + columnSpacing: Style.bigMargins + Layout.alignment: Qt.AlignCenter + + GridLayout { + Layout.fillHeight: true + Layout.fillWidth: !app.landscape + columnSpacing: app.margins + rowSpacing: app.margins + Layout.alignment: Qt.AlignHCenter + visible: root.ctStateType !== null + columns: app.landscape ? 1 : 4 + + Repeater { + model: ListModel { + ListElement { name: "activate"; ct: "0"; bri: 100; color: "#00c5ff" } + ListElement { name: "concentrate"; ct: "23"; bri: 100; color: "#3dddff" } + ListElement { name: "reading"; ct: "57"; bri: 100; color: "#f4de00" } + ListElement { name: "relax"; ct: "95" ; bri: 55; color: "#ffaf2a"} + } + delegate: ProgressButton { + Layout.preferredHeight: Style.hugeIconSize + Layout.preferredWidth: Style.hugeIconSize + imageSource: "../images/lighting/" + model.name + ".svg" + longpressEnabled: false +// mode: "normal" +// backgroundColor: model.color + + + onClicked: { + // Translate from % to absolute value in min/max + // % : 100 = abs : (max - min) + print("min,max", root.ctStateType, root.ctStateType.minValue, root.ctStateType.maxValue) + var absoluteCtValue = (model.ct * (root.ctStateType.maxValue - root.ctStateType.minValue) / 100) + root.ctStateType.minValue + var params = []; + var param1 = {}; + param1["paramName"] = root.ctActionType.paramTypes.get(0).name; + param1["value"] = absoluteCtValue; + params.push(param1) + root.thing.executeAction(root.ctActionType.name, params) + params = []; + param1 = {}; + param1["paramName"] = root.brightnessActionType.paramTypes.get(0).name; + param1["value"] = model.bri; + params.push(param1) + root.thing.executeAction(root.brightnessActionType.name, params) + } + } + } + } + + ColumnLayout { + spacing: Style.margins + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumHeight: width + currentIndex: selectionTabs.currentIndex + + Repeater { + model: modeModel + delegate: Loader { + sourceComponent: model.comp + } + } + + Component { + id: colorPickerComponent + ColorPicker { + anchors.fill: parent + thing: root.thing + } + } + + Component { + id: colorTemperatureComponent + ColorTemperaturePicker { + anchors.fill: parent + thing: root.thing + orientation: app.landscape ? Qt.Vertical : Qt.Horizontal + } + } + + Component { + id: brightnessComponent + Item { + id: brightnessController + property Thing thing: root.thing + readonly property State brightnessState: thing ? thing.stateByName("brightness") : null + readonly property State colorState: thing ? thing.stateByName("color") : null + readonly property State powerState: thing ? thing.stateByName("power") : null + + ActionQueue { + id: actionQueue + thing: brightnessController.thing + stateType: thing.thingClass.stateTypes.findByName("brightness") + } + + Rectangle { + id: brightnessCircle + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + radius: width / 2 + color: Style.tileBackgroundColor + + } + + ShaderEffect { + anchors.fill: brightnessCircle + property Item source: ShaderEffectSource { + id: shaderSource + anchors.fill: parent + sourceItem: brightnessCircle + hideSource: true + } + property color inColor: Style.tileBackgroundColor + property color outColor: powerState.value === true + ? colorState ? colorState.value : "#ffd649" + : Style.tileOverlayColor + Behavior on outColor { ColorAnimation { duration: Style.animationDuration } } + + property real threshold: 0.1 + property real brightness: 1 - (actionQueue.pendingValue || brightnessState.value) / 100 + + fragmentShader: " + varying highp vec2 qt_TexCoord0; + uniform sampler2D source; + uniform highp vec4 outColor; + uniform highp vec4 inColor; + uniform lowp float threshold; + uniform lowp float qt_Opacity; + uniform lowp float brightness; + void main() { + bool isOn = qt_TexCoord0.y > brightness; + lowp vec4 sourceColor = texture2D(source, qt_TexCoord0); + if (isOn) { + gl_FragColor = mix(vec4(outColor.rgb, 1.0) * sourceColor.a, sourceColor, step(threshold, distance(sourceColor.rgb / sourceColor.a, inColor.rgb))) * qt_Opacity; + } else { + gl_FragColor = sourceColor; + } + }" + + } + + MouseArea { + anchors.fill: brightnessCircle + onMouseYChanged: { + var progress = 1 - mouseY / height + actionQueue.sendValue(progress * 100); + } + } + } + } + + Component { + id: powerComponent + Item { + id: powerController + property Thing thing: root.thing + readonly property State powerState: thing ? thing.stateByName("power") : null + + property color borderColor: "#ffd649" + ActionQueue { + id: actionQueue + thing: powerController.thing + stateType: thing.thingClass.stateTypes.findByName("power") + } + + Rectangle { + id: background + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + color: Style.tileBackgroundColor + radius: width / 2 + + ColorIcon { + anchors.centerIn: parent + size: Style.hugeIconSize + name: (actionQueue.pendingValue || powerState.value) === true ? "light-on" : "light-off" + // color: (actionQueue.pendingValue || powerState.value) === true ? Style.accentColor : Style.iconColor + } + + MouseArea { + anchors.fill: parent + onClicked: { + actionQueue.sendValue(!powerState.value) + } + } + } + + RadialGradient { + id: gradient + anchors.fill: background + visible: false + gradient: Gradient{ + GradientStop { position: .45; color: "transparent" } + GradientStop { position: .5; color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 1) } + } + } + OpacityMask { + opacity: (actionQueue.pendingValue || powerState.value) === true ? 1 : 0 + anchors.fill: gradient + source: gradient + maskSource: background + Behavior on opacity { NumberAnimation { duration: Style.animationDuration } } + + } + } + } + } + + ListModel { + id: modeModel + Component.onCompleted: { + if (root.colorState) { + append({modelData: qsTr("Color"), comp: colorPickerComponent}) + } + if (root.ctState) { + append({modelData: qsTr("Temperature"), comp: colorTemperatureComponent}) + } + if (root.brightnessState && !root.ctState && !root.colorState) { + append({modelData: qsTr("Brightness"), comp: brightnessComponent}) + } + if (!root.colorState && !root.ctState && !root.brightnessState) { + append({modelData: qsTr("Power"), comp: powerComponent}) + } + } + } + + SelectionTabs { + id: selectionTabs + Layout.fillWidth: true + model: modeModel + visible: modeModel.count > 1 + } + } + + + + GridLayout { + id: basicItems + Layout.fillWidth: !app.landscape + Layout.fillHeight: app.landscape + Layout.alignment: Qt.AlignHCenter + columnSpacing: app.margins + rowSpacing: app.margins + columns: app.landscape ? 1 : 2 + visible: powerButton.visible || brightnessSlider.visible + + ProgressButton { + id: powerButton + imageSource: root.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" + mode: "normal" + size: Style.bigIconSize + longpressEnabled: false + visible: root.brightnessState || root.ctState || root.colorState + onClicked: { + root.thing.executeAction("power", [{paramName: "power", value: !root.powerState.value}]) + } + } + + BrightnessSlider { + id: brightnessSlider + Layout.fillWidth: orientation == Qt.Horizontal + Layout.fillHeight: orientation == Qt.Vertical + Layout.alignment: Qt.AlignHCenter + thing: root.thing + orientation: basicItems.columns === 1 ? Qt.Vertical : Qt.Horizontal + visible: root.brightnessState && (root.ctState || root.colorState) + } + } + } +} diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index 05870cad..b2450cf0 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -38,7 +38,7 @@ Item { } else if (interfaceList.indexOf("garagedoor") >= 0 ) { page = "GarageThingPage.qml"; } else if (interfaceList.indexOf("light") >= 0) { - page = "LightDevicePage.qml"; + page = "LightThingPage.qml"; } else if (interfaceList.indexOf("shutter") >= 0 || interfaceList.indexOf("blind") >= 0) { page = "ShutterDevicePage.qml"; } else if (interfaceList.indexOf("awning") >= 0) { From 1661dd4ebac5aca66957012a7df60743d7e5c124 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 20 Sep 2021 16:57:59 +0200 Subject: [PATCH 3/3] More work on all sorts of things pages --- libnymea-app/types/interfaces.cpp | 8 +- libnymea-app/types/thing.cpp | 7 +- nymea-app/resources.qrc | 3 + nymea-app/ui/Nymea.qml | 1 + nymea-app/ui/StyleBase.qml | 12 +- nymea-app/ui/components/BrightnessSlider.qml | 8 +- nymea-app/ui/components/CircleBackground.qml | 98 +++ nymea-app/ui/components/ColorPicker.qml | 4 +- .../ui/components/ColorTemperaturePicker.qml | 32 +- nymea-app/ui/components/Dial.qml | 419 ++++-------- nymea-app/ui/components/ProgressButton.qml | 66 +- nymea-app/ui/components/ShutterControls.qml | 92 +-- nymea-app/ui/customviews/GarageController.qml | 101 ++- .../ui/customviews/ThermostatController.qml | 22 +- nymea-app/ui/delegates/InterfaceTile.qml | 2 + .../ClosablesThingsListPage.qml | 2 + .../devicelistpages/GarageThingsListPage.qml | 20 + nymea-app/ui/devicepages/AwningThingPage.qml | 123 ++-- nymea-app/ui/devicepages/CoolingThingPage.qml | 59 ++ .../ui/devicepages/EvChargerThingPage.qml | 74 +++ .../ui/devicepages/HeatingDevicePage.qml | 60 +- .../ui/devicepages/IrrigationDevicePage.qml | 67 +- nymea-app/ui/devicepages/LightThingPage.qml | 52 +- .../ui/devicepages/PowersocketDevicePage.qml | 62 +- .../ui/devicepages/ShutterDevicePage.qml | 595 +++++++++++------- .../ui/devicepages/ThermostatDevicePage.qml | 59 +- .../ui/devicepages/VentilationDevicePage.qml | 96 ++- nymea-app/ui/utils/NymeaUtils.qml | 6 +- 28 files changed, 1158 insertions(+), 992 deletions(-) create mode 100644 nymea-app/ui/components/CircleBackground.qml create mode 100644 nymea-app/ui/devicepages/CoolingThingPage.qml create mode 100644 nymea-app/ui/devicepages/EvChargerThingPage.qml diff --git a/libnymea-app/types/interfaces.cpp b/libnymea-app/types/interfaces.cpp index 40cf3c2b..98a11ba2 100644 --- a/libnymea-app/types/interfaces.cpp +++ b/libnymea-app/types/interfaces.cpp @@ -144,9 +144,13 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent) addInterface("heating", tr("Heating")); addStateType("heating", "power", QVariant::Bool, true, tr("Heating enabled"), tr("Heating enabled changed"), tr("Enable heating")); + addStateType("heating", "percentage", QVariant::Int, true, tr("Percentage"), tr("Percentage changed"), tr("Set percentage"), 0, 100); + + + addInterface("cooling", tr("Cooling")); + addStateType("cooling", "power", QVariant::Bool, true, tr("Cooling enabled"), tr("Cooling enabled changed"), tr("Enable cooling")); + addStateType("cooling", "percentage", QVariant::Int, true, tr("Percentage"), tr("Percentage changed"), tr("Set percentage"), 0, 100); - addInterface("extendedheating", tr("Heatings"), {"heating"}); - addStateType("extendedheating", "percentage", QVariant::Int, true, tr("Percentage"), tr("Percentage changed"), tr("Set percentage"), 0, 100); addInterface("media", tr("Media")); diff --git a/libnymea-app/types/thing.cpp b/libnymea-app/types/thing.cpp index 9a544b9b..1e0e5f21 100644 --- a/libnymea-app/types/thing.cpp +++ b/libnymea-app/types/thing.cpp @@ -32,7 +32,8 @@ #include "thingclass.h" #include "thingmanager.h" -#include +#include +Q_DECLARE_LOGGING_CATEGORY(dcThingManager) Thing::Thing(ThingManager *thingManager, ThingClass *thingClass, const QUuid &parentId, QObject *parent) : QObject(parent), @@ -220,6 +221,10 @@ void Thing::setStateValue(const QUuid &stateTypeId, const QVariant &value) int Thing::executeAction(const QString &actionName, const QVariantList ¶ms) { ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName); + if (!actionType) { + qCWarning(dcThingManager) << "No such action name" << actionName << "in thing class" << m_thingClass->name(); + return -1; + } QVariantList finalParams; foreach (const QVariant ¶mVariant, params) { diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index b18e92d8..fd26f6ce 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -254,5 +254,8 @@ ui/system/ZigbeeNetworkPage.qml ui/components/ConnectionInfoDialog.qml ui/components/ButtonControls.qml + ui/components/CircleBackground.qml + ui/devicepages/CoolingThingPage.qml + ui/devicepages/EvChargerThingPage.qml diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 820d62fa..933c721a 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -149,6 +149,7 @@ ApplicationWindow { "powersocket", "thermostat", "heating", + "cooling", "smartlock", "doorbell", "irrigation", diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index a3405182..d6e75302 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -80,7 +80,8 @@ Item { "smartmeterconsumer": "orange", "smartmeterproducer": "lightgreen", "energymeter": "deepskyblue", - "heating" : "gainsboro", + "heating" : "crimson", + "cooling": "dodgerBlue", "thermostat": "dodgerblue", "irrigation": "lightblue", "windspeedsensor": "blue", @@ -88,7 +89,9 @@ Item { "watersensor": "aqua", "phsensor": "green", "o2sensor": "lightblue", - "orpsensor": "yellow" + "orpsensor": "yellow", + "powersocket": "aquamarine", + "evcharger": "limegreen" } property var stateColors: { @@ -97,7 +100,10 @@ Item { "currentPower": "deepskyblue", } - property color red: "#952727" + property color red: "indianred" + property color green: "mediumseagreen" + property color yellow: "gold" + property color white: "white" property color gray: "gray" property color darkGray: "darkGray" diff --git a/nymea-app/ui/components/BrightnessSlider.qml b/nymea-app/ui/components/BrightnessSlider.qml index 5f2d32cb..84a5a583 100644 --- a/nymea-app/ui/components/BrightnessSlider.qml +++ b/nymea-app/ui/components/BrightnessSlider.qml @@ -40,7 +40,7 @@ Item { property Thing thing: null - readonly property StateType colorTemperatureStateType: root.thing.thingClass.stateTypes.findByName("brightness") + readonly property StateType brightnessStateType: root.thing.thingClass.stateTypes.findByName("brightness") property int value: thing.stateByName("brightness").value @@ -49,7 +49,7 @@ Item { ActionQueue { id: actionQueue thing: root.thing - stateType: root.colorTemperatureStateType + stateName: "brightness" } Rectangle { @@ -94,8 +94,8 @@ Item { anchors.fill: parent anchors.margins: -Style.smallMargins onPositionChanged: { - var minCt = root.colorTemperatureStateType.minValue; - var maxCt = root.colorTemperatureStateType.maxValue + var minCt = root.brightnessStateType.minValue; + var maxCt = root.brightnessStateType.maxValue var ct; if (root.orientation == Qt.Horizontal) { ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) diff --git a/nymea-app/ui/components/CircleBackground.qml b/nymea-app/ui/components/CircleBackground.qml new file mode 100644 index 00000000..8731e98b --- /dev/null +++ b/nymea-app/ui/components/CircleBackground.qml @@ -0,0 +1,98 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtGraphicalEffects 1.0 +import Nymea 1.0 +import "../utils" + +Item { + id: root + implicitHeight: 400 + implicitWidth: 400 + + property alias iconSource: icon.name + property color onColor: Style.accentColor + property bool on: false + + readonly property Item contentItem: background + + signal clicked() + + + Rectangle { + id: background + anchors.centerIn: parent + height: Math.min(400, Math.min(parent.height, parent.width)) + width: height + radius: width / 2 + color: Style.tileBackgroundColor + } + + MouseArea { + anchors.fill: background + onClicked: root.clicked() + } + + ColorIcon { + id: icon + anchors.centerIn: background + size: Style.hugeIconSize + color: root.on ? root.onColor : Style.iconColor + Behavior on color { ColorAnimation { duration: Style.animationDuration } } + } + + RadialGradient { + id: gradient + anchors.fill: background + visible: false + gradient: Gradient{ + GradientStop { position: .45; color: "transparent" } + GradientStop { position: .5; color: root.onColor } + } + } + + OpacityMask { + id: opacityMask + opacity: root.on ? 1 : 0 + anchors.fill: gradient + source: gradient + maskSource: background + Behavior on opacity { NumberAnimation { duration: Style.animationDuration } } + + } + + Item { + id: contentContainer + anchors.fill: background + } + +} diff --git a/nymea-app/ui/components/ColorPicker.qml b/nymea-app/ui/components/ColorPicker.qml index 6d73a2ca..5d25d498 100644 --- a/nymea-app/ui/components/ColorPicker.qml +++ b/nymea-app/ui/components/ColorPicker.qml @@ -32,7 +32,7 @@ Item { ConicalGradient { id: gradient anchors.centerIn: parent - width: Math.min(parent.width, parent.height) + width: Math.min(400, Math.min(parent.width, parent.height)) height: width visible: false gradient: Gradient{ @@ -176,7 +176,7 @@ Item { MouseArea { id: mouseArea - anchors.fill: parent + anchors.fill: gradient onPositionChanged: { var angle = calculateAngle(mouseX, mouseY) diff --git a/nymea-app/ui/components/ColorTemperaturePicker.qml b/nymea-app/ui/components/ColorTemperaturePicker.qml index a734ef39..a16d219b 100644 --- a/nymea-app/ui/components/ColorTemperaturePicker.qml +++ b/nymea-app/ui/components/ColorTemperaturePicker.qml @@ -25,7 +25,7 @@ Item { Rectangle { id: background - width: Math.min(parent.width, parent.height) + width: Math.min(400, Math.min(parent.width, parent.height)) anchors.centerIn: parent height: width radius: width / 2 @@ -35,6 +35,19 @@ Item { GradientStop { position: 0.5; color: "#ffffea" } GradientStop { position: 1.0; color: "#ffd649" } } + + Rectangle { + id: dragHandle + property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) + width: 20 + height: 20 + radius: height / 2 + color: Style.backgroundColor + border.color: Style.foregroundColor + border.width: 2 + x: (background.width - width) / 2 + y: (background.height - height) * valuePercentage + } } Desaturate { @@ -44,22 +57,9 @@ Item { Behavior on desaturation { NumberAnimation { duration: Style.animationDuration } } } - Rectangle { - id: dragHandle - property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) - width: 20 - height: 20 - radius: height / 2 - color: Style.backgroundColor - border.color: Style.foregroundColor - border.width: 2 - x: (parent.width - width) / 2 - y: parent.height * valuePercentage - (height / 2) - - } MouseArea { - anchors.fill: parent + anchors.fill: background onPositionChanged: { var minCt = root.colorTemperatureStateType.minValue; var maxCt = root.colorTemperatureStateType.maxValue @@ -68,7 +68,7 @@ Item { // ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) // } else { // ct : y = max : height - ct = mouseY * (maxCt - minCt) / height + minCt + ct = mouseY * (maxCt - minCt) / (height) + minCt ct = Math.min(maxCt, ct) ct = Math.max(minCt, ct) // ct = Math.min(maxCt, Math.max(minCt, ((height - mouseY) * (maxCt - minCt) / (height - dragHandle.height)) + minCt)) diff --git a/nymea-app/ui/components/Dial.qml b/nymea-app/ui/components/Dial.qml index 3d51079f..b8c03fff 100644 --- a/nymea-app/ui/components/Dial.qml +++ b/nymea-app/ui/components/Dial.qml @@ -32,321 +32,174 @@ import QtQuick 2.5 import QtQuick.Controls 2.2 import Nymea 1.0 import QtQuick.Layouts 1.2 -import QtQuick.Controls.Material 2.2 +import "../utils" -ColumnLayout { - id: dial +Item { + id: root property Thing thing: null - property alias device: dial.thing - property StateType stateType: null + property string stateName: "" + property StateType stateType: thing ? thing.thingClass.stateTypes.findByName(stateName) : null - property bool showValueLabel: true - property int steps: 10 property color color: Style.accentColor - property int maxAngle: 235 + property int precision: 1 - // value : max = angle : maxAngle - function valueToAngle(value) { - return (value - from) * maxAngle / (to - from) - } - function angleToValue(angle) { - return (to - from) * angle / maxAngle + from + readonly property State progressState: thing ? thing.states.getState(stateType.id) : null + readonly property State powerState: thing ? thing.stateByName("power") : null + + property int startAngle: 135 + property int maxAngle: 270 + readonly property int steps: canvas.roundToPrecision(root.stateType.maxValue - root.stateType.minValue) / root.precision + 1 + readonly property double stepSize: (root.stateType.maxValue - root.stateType.minValue) / steps + readonly property double anglePerStep: maxAngle / steps + + + ActionQueue { + id: actionQueue + thing: root.thing + stateType: root.stateType + onPendingValueChanged: canvas.requestPaint() } - readonly property State deviceState: thing && stateType ? thing.states.getState(stateType.id) : null - readonly property double from: dial.stateType ? dial.stateType.minValue : 0 - readonly property double to: dial.stateType ? dial.stateType.maxValue : 100 - readonly property double anglePerStep: maxAngle / dial.steps - readonly property double startAngle: -(dial.steps * dial.anglePerStep) / 2 - - readonly property StateType powerStateType: dial.thing.thingClass.stateTypes.findByName("power") - readonly property State powerState: powerStateType ? dial.thing.states.getState(powerStateType.id) : null - - QtObject { - id: d - property int pendingActionId: -1 - property real valueCache: 0 - property bool valueCacheDirty: false - - property bool busy: rotateMouseArea.pressed || pendingActionId != -1 || valueCacheDirty - - property color onColor: dial.color - property color offColor: "#808080" - property color poweredColor: dial.powerStateType - ? (dial.powerState.value === true ? onColor : offColor) - : onColor - - - function enqueueSetValue(value) { - if (d.pendingActionId == -1) { - executeAction(value); - return; - } else { - valueCache = value - valueCacheDirty = true; - } - } - - function executeAction(value) { - var params = [] - var param = {} - param["paramName"] = dial.stateType.name - param["value"] = value - params.push(param) - d.pendingActionId = dial.thing.executeAction(dial.stateType.name, params) - } + ActionQueue { + id: powerActionQueue + thing: root.thing + stateName: "power" } + Connections { - target: engine.thingManager - onExecuteActionReply: { - if (d.pendingActionId == commandId) { - d.pendingActionId = -1 - if (d.valueCacheDirty) { - d.executeAction(d.valueCache) - d.valueCacheDirty = false; - } - } - } - } - Connections { - target: dial.thing - onActionExecutionFinished: { - if (id == d.pendingActionId) { - d.pendingActionId = -1; - if (d.valueCacheDirty) { - d.executeAction(d.valueCache) - d.valueCacheDirty = false; - } - } - } - } - - Component.onCompleted: rotationButton.rotation = dial.valueToAngle(dial.thingState.value) - Connections { - target: dial.thingState + target: root.progressState onValueChanged: { - if (!d.busy) { - rotationButton.rotation = dial.valueToAngle(dial.thingState.value) + canvas.requestPaint() + } + } + + + Canvas { + id: canvas + anchors.centerIn: root + width: Math.min(root.width, root.height) + height: width + + function roundToPrecision(value) { + var tmp = Math.round(value / root.precision) * root.precision; + return tmp; + } + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + var center = { x: canvas.width / 2, y: canvas.height / 2 }; + + // Step lines + var currentValue = actionQueue.pendingValue || root.progressState.value + var currentStep; + if (root.progressState) { + currentStep = roundToPrecision(currentValue - root.stateType.minValue) / root.precision + } + + print("* current step", currentStep, root.steps, currentValue) + + for(var step = 0; step < steps; step += root.precision) { + var angle = step * anglePerStep + startAngle; + var innerRadius = canvas.width * 0.4 + var outerRadius = canvas.width * 0.5 + + if (step === currentStep) { + ctx.strokeStyle = root.color + innerRadius = canvas.width * 0.38 + ctx.lineWidth = 4; + } else { + ctx.strokeStyle = Style.tileOverlayColor; + ctx.lineWidth = 1; + } + + ctx.beginPath(); + // rotate + //convert to radians + var rad = angle * Math.PI/180; + var c = Math.cos(rad); + var s = Math.sin(rad); + var innerPointX = center.x + (innerRadius * c); + var innerPointY = center.y + (innerRadius * s); + var outerPointX = center.x + (outerRadius * c); + var outerPointY = center.x + (outerRadius * s); + + ctx.moveTo(innerPointX, innerPointY); + ctx.lineTo(outerPointX, outerPointY); + ctx.stroke(); + ctx.closePath(); } } } - Label { - id: topLabel - Layout.fillWidth: true - property var unit: dial.stateType ? dial.stateType.unit : Types.UnitNone - text: Types.toUiValue(rotateMouseArea.currentValue, unit) + Types.toUiUnit(unit) - font.pixelSize: app.largeFont * 1.5 - horizontalAlignment: Text.AlignHCenter - visible: dial.showValueLabel && dial.stateType !== null - } - Item { - id: buttonContainer - Layout.fillWidth: true - Layout.fillHeight: true + MouseArea { + anchors.fill: canvas - Item { - id: innerDial + property bool dragging: false + property double lastAngle + property double angleDiff: 0 - height: Math.min(parent.height, parent.width) * .9 - width: height - anchors.centerIn: parent - rotation: dial.startAngle + onPressed: { + angleDiff = 0 + lastAngle = calculateAngle(mouseX, mouseY) + } - Rectangle { - anchors.fill: rotationButton - radius: height / 2 - border.color: Style.foregroundColor - border.width: 2 - color: "transparent" - opacity: rotateMouseArea.pressed && !rotateMouseArea.grabbed ? .7 : 1 + onReleased: { + if (!dragging && root.powerState) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + powerActionQueue.sendValue(!root.powerState.value) + } + dragging = false + } + + onPositionChanged: { + var angle = calculateAngle(mouseX, mouseY) + var tmpDiff = angle - lastAngle + if (tmpDiff > 300) { + tmpDiff -= 360 + } + if (tmpDiff < -300) { + tmpDiff += 360 } - Item { - id: rotationButton - height: parent.height * .75 - width: height - anchors.centerIn: parent - visible: dial.stateType !== null - Behavior on rotation { - NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } - enabled: !rotateMouseArea.pressed && !d.busy - } + lastAngle = angle; - Item { - id: handle - anchors.horizontalCenter: parent.horizontalCenter - height: parent.height * .35 - width: height - -// Rectangle { anchors.fill: parent; color: "red"; opacity: .3} - - Rectangle { - height: parent.height * .5 - width: innerDial.width * 0.02 - radius: width / 2 - anchors.top: parent.top - anchors.topMargin: height * .25 - anchors.horizontalCenter: parent.horizontalCenter - color: d.poweredColor - Behavior on color { ColorAnimation { duration: 200 } } - } - } + angleDiff += tmpDiff + if (Math.abs(angleDiff) > 1) { + dragging = true } - Repeater { - id: indexLEDs - model: dial.steps + 1 - - Item { - height: parent.height - width: parent.width * .04 - anchors.centerIn: parent - rotation: dial.anglePerStep * index - visible: dial.stateType !== null - - Rectangle { - width: parent.width - height: width - radius: width / 2 - color: dial.deviceState && dial.angleToValue(parent.rotation) <= dial.deviceState.value ? d.poweredColor : d.offColor - Behavior on color { ColorAnimation { duration: 200 } } - } + var valueDiff = angleDiff / root.anglePerStep * root.stepSize + valueDiff = canvas.roundToPrecision(valueDiff) + if (Math.abs(valueDiff) > 0) { + var currentValue = actionQueue.pendingValue || root.progressState.value + var newValue = currentValue + valueDiff + newValue = Math.min(root.stateType.maxValue, Math.max(root.stateType.minValue, newValue)) + if (currentValue !== newValue) { + actionQueue.sendValue(newValue) } + var steps = Math.round(valueDiff / root.stepSize) + angleDiff -= steps * root.anglePerStep } } - Rectangle { - id: buttonBorder - height: innerDial.height * .8 - width: height - anchors.centerIn: parent - radius: height / 2 - border.color: Style.foregroundColor - opacity: .3 - border.width: width * .025 - color: "transparent" - } + function calculateAngle(mouseX, mouseY) { + // transform coords to center of dial + mouseX -= canvas.width / 2 + mouseY -= canvas.height / 2 - Label { - anchors { left: innerDial.left; bottom: innerDial.bottom; bottomMargin: innerDial.height * .1 } - text: "MIN" - font.pixelSize: innerDial.height * .06 - visible: dial.stateType !== null - } + var rad = Math.atan(mouseY / mouseX); + var angle = rad * 180 / Math.PI - Label { - anchors { right: innerDial.right; bottom: innerDial.bottom; bottomMargin: innerDial.height * .1 } - text: "MAX" - font.pixelSize: innerDial.height * .06 - visible: dial.stateType !== null - } + angle += 90; - ColorIcon { - anchors.centerIn: innerDial - height: innerDial.height * .2 - width: height - name: "../images/system-shutdown.svg" - visible: dial.powerStateType !== null - color: d.poweredColor - Behavior on color { ColorAnimation { duration: 200 } } - } + if (mouseX < 0 && mouseY >= 0) angle = 180 + angle; + if (mouseX < 0 && mouseY < 0) angle = 180 + angle; - MouseArea { - id: rotateMouseArea - anchors.fill: buttonBorder - onPressedChanged: PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) - -// Rectangle { anchors.fill: parent; color: "blue"; opacity: .3} - - property bool grabbed: false - onPressed: { - startX = mouseX - startY = mouseY - var mappedToHandle = mapToItem(handle, mouseX, mouseY); - if (mappedToHandle.x >= 0 - && mappedToHandle.x < handle.width - && mappedToHandle.y >= 0 - && mappedToHandle.y < handle.height - ) { - grabbed = true; - return; - } - } - onCanceled: grabbed = false; - - property bool dragging: false - onReleased: { - grabbed = false; - if (dial.powerStateType && !dragging) { - var params = [] - var param = {} - param["paramName"] = "power" - param["value"] = !dial.powerState.value - params.push(param) - dial.thing.executeAction("power", params) - } - dragging = false; - } - - readonly property int decimals: dial.stateType && dial.stateType.type.toLowerCase() === "int" ? 0 : 1 - property var currentValue: dial.deviceState ? dial.deviceState.value.toFixed(decimals) : 0 - property date lastVibration: new Date() - property int startX - property int startY - onPositionChanged: { - if (Math.abs(mouseX - startX) > 10 || Math.abs(mouseY - startY) > 10) { - dragging = true; - } - - if (!grabbed) { - return; - } - var angle = calculateAngle(mouseX, mouseY) - angle = (360 + angle - dial.startAngle) % 360; - - if (angle > 360 - ((360 - dial.maxAngle) / 2)) { - angle = 0; - } else if (angle > dial.maxAngle) { - angle = dial.maxAngle - } - - var newValue = Math.round(dial.angleToValue(angle) * 2) / 2; - rotationButton.rotation = angle; - newValue = newValue.toFixed(decimals) - - if (newValue != currentValue) { - currentValue = newValue; - if (newValue <= dial.stateType.minValue || newValue >= dial.stateType.maxValue) { - PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) - } else { - if (lastVibration.getTime() + 35 < new Date()) { - PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) - } - lastVibration = new Date() - } - - d.enqueueSetValue(newValue); - } - } - - function calculateAngle(mouseX, mouseY) { - // transform coords to center of dial - mouseX -= innerDial.width / 2 - mouseY -= innerDial.height / 2 - - var rad = Math.atan(mouseY / mouseX); - var angle = rad * 180 / Math.PI - - angle += 90; - - if (mouseX < 0 && mouseY >= 0) angle = 180 + angle; - if (mouseX < 0 && mouseY < 0) angle = 180 + angle; - - return angle; - } + return angle; } } } diff --git a/nymea-app/ui/components/ProgressButton.qml b/nymea-app/ui/components/ProgressButton.qml index 6da0c092..e536c123 100644 --- a/nymea-app/ui/components/ProgressButton.qml +++ b/nymea-app/ui/components/ProgressButton.qml @@ -46,7 +46,8 @@ Item { property alias color: icon.color property alias backgroundColor: background.color - property bool longpressEnabled: true + property bool longpressEnabled: false + property bool busy: false property int size: Style.iconSize @@ -100,6 +101,16 @@ Item { } } + NumberAnimation { + target: busyCanvas + property: "rotation" + from: 360 + to: 0 + duration: 2000 + loops: Animation.Infinite + running: root.busy + } + Rectangle { id: background anchors.fill: parent @@ -136,9 +147,8 @@ Item { property bool inverted: false readonly property int penWidth: 2 - onProgressChanged: { - requestPaint() - } + + onProgressChanged: requestPaint() Connections { target: buttonDelegate onPressedChanged: { @@ -152,21 +162,49 @@ Item { onPaint: { var ctx = getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = Qt.rgba(1, 0, 0, 1); - ctx.lineWidth = canvas.penWidth - ctx.strokeStyle = Style.accentColor - var start = -Math.PI / 2; - var stop = -Math.PI / 2; - if (inverted) { - start += canvas.progress * 2 * Math.PI - } else { - stop += canvas.progress * 2 * Math.PI + // Draw longpress progress + if (canvas.progress > 0) { + ctx.save(); + ctx.fillStyle = Qt.rgba(1, 0, 0, 1); + ctx.lineWidth = canvas.penWidth + ctx.strokeStyle = Style.accentColor + + var start = -Math.PI / 2; + var stop = -Math.PI / 2; + if (inverted) { + start += canvas.progress * 2 * Math.PI + } else { + stop += canvas.progress * 2 * Math.PI + } + + ctx.beginPath(); + ctx.arc(canvas.width / 2, canvas.height / 2, ((canvas.width - canvas.penWidth) / 2), start, stop); + ctx.stroke(); + ctx.closePath(); + ctx.restore(); } + } + } + Canvas { + id: busyCanvas + visible: root.busy + anchors.fill: parent + anchors.margins: -4 + onPaint: { + var ctx = getContext("2d"); + ctx.save(); + ctx.lineWidth = 2; + ctx.strokeStyle = root.backgroundColor; + var radius = (width - ctx.lineWidth) / 2 + var circumference = 2 * Math.PI * radius + var dashRatio = circumference / 6 / ctx.lineWidth + ctx.setLineDash([dashRatio, dashRatio]) ctx.beginPath(); - ctx.arc(canvas.width / 2, canvas.height / 2, ((canvas.width - canvas.penWidth) / 2), start, stop); + ctx.arc(width / 2, height / 2, (width - 2) / 2, 0, 2 * Math.PI); ctx.stroke(); + ctx.closePath(); } } diff --git a/nymea-app/ui/components/ShutterControls.qml b/nymea-app/ui/components/ShutterControls.qml index 5fe2a61c..a34a02ed 100644 --- a/nymea-app/ui/components/ShutterControls.qml +++ b/nymea-app/ui/components/ShutterControls.qml @@ -30,13 +30,13 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 import QtQuick.Layouts 1.3 import Nymea 1.0 -RowLayout { +Item { id: root - spacing: 0 + implicitHeight: size * 4 + implicitWidth: size * 7 property Thing thing: null readonly property State openState: thing.stateByName("state") @@ -48,47 +48,65 @@ RowLayout { signal activated(string button); - Item { Layout.fillWidth: true; Layout.fillHeight: true } + RowLayout { + anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter } + spacing: 0 - ProgressButton { - longpressEnabled: false - imageSource: root.invert ? "../images/down.svg" : "../images/up.svg" - mode: root.backgroundEnabled ? "normal" : "transparent" - size: root.size - color: root.openState && root.openState.value === "opening" ? Material.accent : Style.iconColor - onClicked: { - engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("open").id) - root.activated("open") + Item { Layout.fillWidth: true; Layout.fillHeight: true } + + ProgressButton { + imageSource: root.invert ? "../images/down.svg" : "../images/up.svg" + backgroundColor: root.backgroundEnabled ? Style.green : "transparent" + size: root.size + busy: root.openState ? root.openState.value === "opening" : openBusyTimer.running + onClicked: { + engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("open").id) + root.activated("open") + openBusyTimer.start() + closeBusyTimer.stop() + } + + Timer { + id: openBusyTimer + interval: 5000 + } } - } - Item { Layout.fillWidth: true; Layout.fillHeight: true } + Item { Layout.fillWidth: true; Layout.fillHeight: true } - ProgressButton { - visible: root.canStop - longpressEnabled: false - mode: root.backgroundEnabled ? "normal" : "transparent" - size: root.size - imageSource: "../images/media-playback-stop.svg" - onClicked: { - engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("stop").id) - root.activated("stop") + ProgressButton { + visible: root.canStop + backgroundColor: root.backgroundEnabled ? Style.yellow : "transparent" + size: root.size + imageSource: "../images/media-playback-stop.svg" + onClicked: { + engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("stop").id) + root.activated("stop") + openBusyTimer.stop() + closeBusyTimer.stop() + } } - } - Item { Layout.fillWidth: true; Layout.fillHeight: true } + Item { Layout.fillWidth: true; Layout.fillHeight: true } - ProgressButton { - imageSource: root.invert ? "../images/up.svg" : "../images/down.svg" - longpressEnabled: false - mode: root.backgroundEnabled ? "normal" : "transparent" - size: root.size - color: root.openState && root.openState.value === "closing" ? Material.accent : Style.iconColor - onClicked: { - engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("close").id) - root.activated("close") + ProgressButton { + imageSource: root.invert ? "../images/up.svg" : "../images/down.svg" + backgroundColor: root.backgroundEnabled ? Style.red : "transparent" + size: root.size + busy: root.openState ? root.openState.value === "closing" : closeBusyTimer.running + onClicked: { + engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("close").id) + root.activated("close") + openBusyTimer.stop(); + closeBusyTimer.start() + } + Timer { + id: closeBusyTimer + interval: 5000 + } } - } - Item { Layout.fillWidth: true; Layout.fillHeight: true } + Item { Layout.fillWidth: true; Layout.fillHeight: true } + } } + diff --git a/nymea-app/ui/customviews/GarageController.qml b/nymea-app/ui/customviews/GarageController.qml index 8c70e1aa..eacc7f0d 100644 --- a/nymea-app/ui/customviews/GarageController.qml +++ b/nymea-app/ui/customviews/GarageController.qml @@ -1,6 +1,7 @@ import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 +import QtGraphicalEffects 1.0 import "../components" import Nymea 1.0 @@ -42,86 +43,77 @@ Item { : Math.min(Math.min(parent.width, 500), parent.height - shutterControlsContainer.minimumHeight) Layout.preferredHeight: width - ColorIcon { + Rectangle { + id: background anchors.centerIn: parent - size: Math.min(parent.height, parent.width) - Style.hugeMargins * 2 - property string currentImage: { - if (root.isExtended) { - return NymeaUtils.pad(Math.round(root.percentageState.value / 10), 2) + "0" - } - if (root.intermediatePositionStateType) { - return root.stateState.value === "closed" ? "100" - : root.intermediatePositionState.value === false ? "000" : "050" - } - return "100" - } - name: "../images/garage/garage-" + currentImage + ".svg" + width: Math.min(500, Math.min(parent.width, parent.height) - Style.hugeMargins * 2) + height: width + radius: width / 2 + color: Style.tileBackgroundColor + } - - Item { - id: arrows + Item { + id: door + anchors.fill: background + Canvas { + id: canvas anchors.centerIn: parent - width: Style.iconSize * 2 - height: parent.height * .6 - clip: true - visible: root.stateStateType && (root.stateState.value === "opening" || root.stateState.value === "closing") - property bool up: root.stateState && root.stateState.value === "opening" + width: parent.width + height: parent.height + Style.margins + anchors.verticalCenterOffset: root.percentageState + ? -height * (1 - (root.percentageState.value / 100)) + : -height / 2 + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); - // NumberAnimation doesn't reload to/from while it's running. If we switch from closing to opening or vice versa - // we need to somehow stop and start the animation - property bool animationHack: true - onAnimationHackChanged: { - if (!animationHack) hackTimer.start(); - } - Timer { id: hackTimer; interval: 1; onTriggered: arrows.animationHack = true } - Connections { target: root.stateState; onValueChanged: arrows.animationHack = false } - - NumberAnimation { - target: arrowColumn - property: "y" - duration: 500 - easing.type: Easing.Linear - from: arrows.up ? Style.iconSize : -Style.iconSize - to: arrows.up ? -Style.iconSize : Style.iconSize - loops: Animation.Infinite - running: arrows.animationHack && root.stateState && (root.stateState.value === "opening" || root.stateState.value === "closing") - } - - Column { - id: arrowColumn - width: parent.width - - Repeater { - model: arrows.height / Style.iconSize + 1 - ColorIcon { - name: arrows.up ? "../images/up.svg" : "../images/down.svg" - width: parent.width - height: width - color: Style.accentColor - } + ctx.fillStyle = Style.tileForegroundColor + var segments = 10; + var segmentHeight = height / segments + var barHeight = segmentHeight - Style.smallMargins + for (var i = 0; i < segments; i++) { + ctx.fillRect(0, i * segmentHeight, width, barHeight) } } } } + + + OpacityMask { + anchors.fill: background + source: ShaderEffectSource { + sourceItem: door + hideSource: true + } + maskSource: background + } } Item { id: shutterControlsContainer Layout.fillWidth: true Layout.minimumWidth: minimumWidth - Layout.fillHeight: true + Layout.preferredHeight: Style.bigIconSize * 4 property int minimumWidth: Style.iconSize * 10 property int minimumHeight: Style.iconSize * 2.5 ProgressButton { anchors.centerIn: parent + mode: "highlight" visible: root.isImpulseBased longpressEnabled: false + size: Style.bigIconSize imageSource: "../images/closable-move.svg" + busy: busyTimer.running onClicked: { var actionTypeId = root.thing.thingClass.actionTypes.findByName("triggerImpulse").id print("Triggering impulse", actionTypeId) engine.thingManager.executeAction(root.thing.id, actionTypeId) + busyTimer.start(); + } + Timer { + id: busyTimer + interval: 5000 } } @@ -132,7 +124,6 @@ Item { anchors.centerIn: parent backgroundEnabled: true size: Style.bigIconSize - spacing: (parent.width - Style.iconSize*2*children.length) / (children.length - 1) visible: !root.isImpulseBased } } diff --git a/nymea-app/ui/customviews/ThermostatController.qml b/nymea-app/ui/customviews/ThermostatController.qml index 09b0c842..d32a8364 100644 --- a/nymea-app/ui/customviews/ThermostatController.qml +++ b/nymea-app/ui/customviews/ThermostatController.qml @@ -90,9 +90,9 @@ Item { if (targetTempStep === step) { if (currentTempStep && currentTempStep < targetTempStep) { - ctx.strokeStyle = "red"; + ctx.strokeStyle = app.interfaceToColor("heating"); } else if (currentTempStep && currentTempStep > targetTempStep) { - ctx.strokeStyle = "dodgerblue"; + ctx.strokeStyle = app.interfaceToColor("cooling"); } else { ctx.strokeStyle = Style.accentColor; } @@ -100,16 +100,16 @@ Item { ctx.lineWidth = 4; } else if (currentTempStep && currentTempStep === step) { if (currentTempStep < targetTempStep) { - ctx.strokeStyle = "red"; + ctx.strokeStyle = app.interfaceToColor("heating"); } else { - ctx.strokeStyle = "dodgerblue"; + ctx.strokeStyle = app.interfaceToColor("cooling"); } ctx.lineWidth = 3; } else if (currentTempStep && currentTempStep < step && step < targetTempStep) { - ctx.strokeStyle = "red"; + ctx.strokeStyle = app.interfaceToColor("heating"); ctx.lineWidth = 2; } else if (currentTempStep && currentTempStep > step && step > targetTempStep) { - ctx.strokeStyle = "dodgerblue"; + ctx.strokeStyle = app.interfaceToColor("cooling"); ctx.lineWidth = 2; } else { ctx.strokeStyle = Style.tileOverlayColor; @@ -127,8 +127,8 @@ Item { var outerPointX = center.x + (outerRadius * c); var outerPointY = center.x + (outerRadius * s); - context.moveTo(innerPointX, innerPointY); - context.lineTo(outerPointX, outerPointY); + ctx.moveTo(innerPointX, innerPointY); + ctx.lineTo(outerPointX, outerPointY); ctx.stroke(); ctx.closePath(); } @@ -166,16 +166,16 @@ Item { ColorIcon { width: Style.largeIconSize height: width - anchors { bottom: canvas.bottom; horizontalCenter: canvas.horizontalCenter } + anchors { bottom: canvas.bottom; horizontalCenter: canvas.horizontalCenter; margins: Style.margins } name: root.heatingOnState && root.heatingOnState.value === true ? "../images/thermostat/heating.svg" : root.coolingOnState && root.coolingOnState.value === true ? "../images/thermostat/cooling.svg" : "" color: root.heatingOnState && root.heatingOnState.value === true - ? "red" + ? app.interfaceToColor("heating") : root.coolingOnState && root.coolingOnState.value === true - ? "dodgerblue" + ? app.interfaceToColor("cooling") : Style.iconColor } diff --git a/nymea-app/ui/delegates/InterfaceTile.qml b/nymea-app/ui/delegates/InterfaceTile.qml index 4f99725b..40f39ad7 100644 --- a/nymea-app/ui/delegates/InterfaceTile.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -74,6 +74,7 @@ MainPageTile { // Open interface specific things list switch (iface.name) { case "heating": + case "cooling": case "sensor": page = "SensorsDeviceListPage.qml" break; @@ -172,6 +173,7 @@ MainPageTile { case "extendedsmartmeterconsumer": case "extendedsmartmeterproducer": case "heating": + case "cooling": case "thermostat": return sensorComponent; // return labelComponent; diff --git a/nymea-app/ui/devicelistpages/ClosablesThingsListPage.qml b/nymea-app/ui/devicelistpages/ClosablesThingsListPage.qml index 09bcaffd..56f2a385 100644 --- a/nymea-app/ui/devicelistpages/ClosablesThingsListPage.qml +++ b/nymea-app/ui/devicelistpages/ClosablesThingsListPage.qml @@ -143,6 +143,8 @@ ThingsListPageBase { ShutterControls { id: shutterControls Layout.fillWidth: false + Layout.preferredWidth: Style.iconSize * 5 + Layout.preferredHeight: Style.iconSize height: parent.height thing: itemDelegate.thing invert: root.invertControls diff --git a/nymea-app/ui/devicelistpages/GarageThingsListPage.qml b/nymea-app/ui/devicelistpages/GarageThingsListPage.qml index 919c1843..f1c2beb3 100644 --- a/nymea-app/ui/devicelistpages/GarageThingsListPage.qml +++ b/nymea-app/ui/devicelistpages/GarageThingsListPage.qml @@ -108,6 +108,26 @@ ThingsListPageBase { ThingStatusIcons { thing: itemDelegate.thing } + ProgressButton { + visible: itemDelegate.thing.thingClass.interfaces.indexOf("impulsegaragedoor") >= 0 + imageSource: "../images/closable-move.svg" + onClicked: { + var actionTypeId = itemDelegate.thing.thingClass.actionTypes.findByName("triggerImpulse").id + engine.thingManager.executeAction(itemDelegate.thing.id, actionTypeId) + } + } + + ShutterControls { + visible: itemDelegate.thing.thingClass.interfaces.indexOf("simpleclosable") >= 0 + id: shutterControls + Layout.fillWidth: false + Layout.preferredWidth: Style.iconSize * 5 + Layout.preferredHeight: Style.iconSize + height: parent.height + thing: itemDelegate.thing + invert: root.invertControls + enabled: itemDelegate.isEnabled + } } } } diff --git a/nymea-app/ui/devicepages/AwningThingPage.qml b/nymea-app/ui/devicepages/AwningThingPage.qml index 2b066224..cdd86df4 100644 --- a/nymea-app/ui/devicepages/AwningThingPage.qml +++ b/nymea-app/ui/devicepages/AwningThingPage.qml @@ -30,11 +30,12 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 -import QtQuick.Controls.Material 2.2 +import QtGraphicalEffects 1.0 import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" import "../customviews" +import "../utils" ThingPageBase { id: root @@ -48,75 +49,71 @@ ThingPageBase { anchors.fill: parent columns: root.landscape ? 2 : 1 - Item { - id: shutterImage - Layout.preferredWidth: root.landscape ? - Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) - : Math.min(Math.min(500, parent.width), parent.height - shutterControlsContainer.minimumHeight) - Layout.preferredHeight: width + CircleBackground { + id: background + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: Style.hugeMargins + onColor: Style.yellow + on: true + iconSource: "weathericons/weather-clear-day" - ColorIcon { + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "percentage" + } + + Item { + id: awning + anchors.fill: parent + + + Rectangle { + anchors.centerIn: parent + width: background.contentItem.width + height: background.contentItem.height + property real progress: root.percentageState ? + dragArea.pressed ? dragArea.draggedProgress : root.percentageState.value / 100 + : .5 + anchors.verticalCenterOffset: -height * (1 - progress) + color: Style.tileOverlayColor + } + } + + OpacityMask { + anchors.fill: background + source: ShaderEffectSource { + sourceItem: awning + hideSource: true + } + maskSource: background + } + + MouseArea { + id: dragArea anchors.centerIn: parent - size: Math.min(parent.height, parent.width) - Style.hugeMargins * 2 - name: root.isExtended ? - "../images/awning/awning-" + app.pad(Math.round(root.percentageState.value / 10) * 10, 3) + ".svg" - : "../images/awning/awning-100.svg" + width: background.contentItem.width + height: background.contentItem.height + property real draggedProgress: mouseY / height + onMouseYChanged: print("mouseY", mouseY, draggedProgress) + onReleased: { + actionQueue.sendValue(draggedProgress * 100) + } } } - Item { - id: shutterControlsContainer + + ShutterControls { + id: shutterControls Layout.fillWidth: true - Layout.fillHeight: true - Layout.margins: app.margins * 2 - property int minimumWidth: Style.iconSize * 2.7 * 3 - property int minimumHeight: Style.iconSize * 4.5 + Layout.minimumWidth: implicitWidth + Layout.preferredHeight: implicitHeight - ColumnLayout { - anchors.centerIn: parent - width: parent.width - spacing: app.margins - - Slider { - id: percentageSlider - Layout.fillWidth: true - from: 0 - to: 100 - stepSize: 1 - visible: isExtended - - Binding { - target: percentageSlider - property: "value" - value: root.percentageState.value - when: !percentageSlider.pressed - } - - onPressedChanged: { - if (pressed) { - return - } - print("should move", value) - - var actionType = root.thing.thingClass.actionTypes.findByName("percentage"); - var params = []; - var percentageParam = {} - percentageParam["paramTypeId"] = actionType.paramTypes.findByName("percentage").id; - percentageParam["value"] = value - params.push(percentageParam); - engine.thingManager.executeAction(root.thing.id, actionType.id, params); - } - } - - ShutterControls { - id: shutterControls - thing: root.thing - size: Style.bigIconSize - backgroundEnabled: true - invert: true - Layout.fillWidth: true - } - } + thing: root.thing + size: Style.bigIconSize + backgroundEnabled: true + invert: true } } } diff --git a/nymea-app/ui/devicepages/CoolingThingPage.qml b/nymea-app/ui/devicepages/CoolingThingPage.qml new file mode 100644 index 00000000..9128d13e --- /dev/null +++ b/nymea-app/ui/devicepages/CoolingThingPage.qml @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Controls.Material 2.1 +import Nymea 1.0 +import "../components" +import "../utils" + +ThingPageBase { + id: root + + readonly property State powerState: thing.stateByName("power") + + + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "power" + } + + CircleBackground { + anchors.fill: parent + anchors.margins: Style.hugeMargins + iconSource: "thermostat/cooling" + onColor: app.interfaceToColor("cooling") + on: actionQueue.pendingValue || root.powerState.value + onClicked: actionQueue.sendValue(!root.powerState.value) + } +} diff --git a/nymea-app/ui/devicepages/EvChargerThingPage.qml b/nymea-app/ui/devicepages/EvChargerThingPage.qml new file mode 100644 index 00000000..556763bc --- /dev/null +++ b/nymea-app/ui/devicepages/EvChargerThingPage.qml @@ -0,0 +1,74 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../utils" + +ThingPageBase { + id: root + + readonly property State powerState: thing.stateByName("power") + readonly property State maxChargingCurrentState: thing.stateByName("maxChargingCurrent") + readonly property StateType maxChargingCurrentStateType: thing.thingClass.stateTypes.findByName("maxChargingCurrent") + + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "power" + } + + CircleBackground { + id: background + anchors.fill: parent + anchors.margins: Style.hugeMargins + iconSource: "ev-charger" + onColor: app.interfaceToColor("evcharger") + on: (actionQueue.pendingValue || powerState.value) === true + onClicked: { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + actionQueue.sendValue(!root.powerState.value) + } + } + + Dial { + anchors.centerIn: parent + height: background.contentItem.height + width: background.contentItem.width + visible: root.maxChargingCurrentState + + thing: root.thing + stateName: "maxChargingCurrent" + color: app.interfaceToColor("evcharger") + } +} diff --git a/nymea-app/ui/devicepages/HeatingDevicePage.qml b/nymea-app/ui/devicepages/HeatingDevicePage.qml index cefd3401..953bce74 100644 --- a/nymea-app/ui/devicepages/HeatingDevicePage.qml +++ b/nymea-app/ui/devicepages/HeatingDevicePage.qml @@ -34,58 +34,26 @@ import QtQuick.Layouts 1.1 import QtQuick.Controls.Material 2.1 import Nymea 1.0 import "../components" +import "../utils" ThingPageBase { id: root readonly property State powerState: thing.stateByName("power") - readonly property ActionType powerActionType: thing.thingClass.actionTypes.findByName("power"); - GridLayout { + + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "power" + } + + CircleBackground { anchors.fill: parent - anchors.margins: app.margins - columns: app.landscape ? 2 : 1 - rowSpacing: app.margins - columnSpacing: app.margins - Layout.alignment: Qt.AlignCenter - - Item { - Layout.preferredWidth: Math.max(Style.iconSize * 6, parent.width / 5) - Layout.preferredHeight: width - Layout.topMargin: app.margins - Layout.bottomMargin: app.landscape ? app.margins : 0 - Layout.alignment: Qt.AlignCenter - Layout.rowSpan: app.landscape ? 4 : 1 - Layout.fillHeight: true - - AbstractButton { - height: Math.min(parent.height, parent.width) - width: height - anchors.centerIn: parent - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: root.powerState.value === true ? Style.accentColor : Style.iconColor - border.width: 4 - radius: width / 2 - } - - ColorIcon { - id: bulbIcon - anchors.fill: parent - anchors.margins: app.margins * 1.5 - name: "../images/thermostat/heating.svg" - color: root.powerState.value === true ? Style.accentColor : Style.iconColor - } - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.powerActionType.paramTypes.get(0).id; - param["value"] = !root.powerState.value; - params.push(param) - engine.thingManager.executeAction(root.thing.id, root.powerActionType.id, params); - } - } - } + anchors.margins: Style.hugeMargins + iconSource: "thermostat/heating" + onColor: app.interfaceToColor("heating") + on: actionQueue.pendingValue || root.powerState.value + onClicked: actionQueue.sendValue(!root.powerState.value) } } diff --git a/nymea-app/ui/devicepages/IrrigationDevicePage.qml b/nymea-app/ui/devicepages/IrrigationDevicePage.qml index 7202ccee..7119f97c 100644 --- a/nymea-app/ui/devicepages/IrrigationDevicePage.qml +++ b/nymea-app/ui/devicepages/IrrigationDevicePage.qml @@ -34,6 +34,7 @@ import QtQuick.Layouts 1.1 import QtQuick.Controls.Material 2.1 import Nymea 1.0 import "../components" +import "../utils" ThingPageBase { id: root @@ -149,54 +150,36 @@ ThingPageBase { filterValue: root.thing.id } + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "power" + } + GridLayout { id: mainGrid anchors.fill: parent - anchors.margins: app.margins columns: app.landscape ? 2 : 1 - Item { - Layout.preferredWidth: app.landscape ? parent.width * .4 : parent.width - Layout.preferredHeight: app.landscape ? parent.height : parent.height *.4 - - AbstractButton { - height: Math.min(Math.min(parent.height, parent.width), Style.iconSize * 5) - width: height - anchors.centerIn: parent - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: root.powerState.value === true ? Style.accentColor : Style.iconColoor - border.width: 4 - radius: width / 2 - } - - ColorIcon { - id: irrigationIcon - anchors.fill: parent - anchors.margins: app.margins * 1.5 - name: "../images/irrigation.svg" - color: root.powerState.value === true ? Style.accentColor : Style.iconColor - } - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.powerActionType.paramTypes.get(0).id; - param["value"] = !root.powerState.value; - params.push(param) - engine.thingManager.executeAction(root.thing.id, root.powerStateType.id, params); - PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) - } - } + CircleBackground { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: Style.hugeMargins; Layout.rightMargin: Style.hugeMargins; Layout.topMargin: Style.hugeMargins + iconSource: "irrigation" + onColor: app.interfaceToColor("irrigation") + on: actionQueue.pendingValue || root.powerState.value + onClicked: actionQueue.sendValue(!root.powerState.value) } + ColumnLayout { - Layout.preferredWidth: app.landscape ? parent.width * .6 : parent.width - Layout.preferredHeight: app.landscape ? parent.height : parent.height * .6 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Style.margins Item { Layout.fillWidth: true; Layout.fillHeight: true } Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins horizontalAlignment: Text.AlignHCenter text: root.isOn ? qsTr("Watering since") : history.lastWatering ? qsTr("Last watering") @@ -205,7 +188,7 @@ ThingPageBase { Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins horizontalAlignment: Text.AlignHCenter text: history.lastWatering ? Qt.formatDateTime(history.lastWatering) : "" font.pixelSize: app.largeFont @@ -213,7 +196,7 @@ ThingPageBase { } Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins + Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins; Layout.bottomMargin: Style.margins horizontalAlignment: Text.AlignHCenter font.pixelSize: app.smallFont text: { @@ -252,7 +235,7 @@ ThingPageBase { Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.topMargin: app.margins + Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins; Layout.topMargin: Style.margins horizontalAlignment: Text.AlignHCenter text: tagsProxy.count > 0 ? //: Irrigation will be turned of at, e.g. 09:00 @@ -299,7 +282,7 @@ ThingPageBase { Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins horizontalAlignment: Text.AlignHCenter visible: tagsProxy.count > 0 font.pixelSize: app.largeFont @@ -308,7 +291,7 @@ ThingPageBase { } Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins + Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins; Layout.bottomMargin: Style.margins horizontalAlignment: Text.AlignHCenter visible: tagsProxy.count > 0 font.pixelSize: app.smallFont diff --git a/nymea-app/ui/devicepages/LightThingPage.qml b/nymea-app/ui/devicepages/LightThingPage.qml index 2892b23b..b4d9f56e 100644 --- a/nymea-app/ui/devicepages/LightThingPage.qml +++ b/nymea-app/ui/devicepages/LightThingPage.qml @@ -112,7 +112,7 @@ ThingPageBase { } ColumnLayout { - spacing: Style.margins + spacing: Style.hugeMargins StackLayout { Layout.fillWidth: true @@ -162,7 +162,7 @@ ThingPageBase { Rectangle { id: brightnessCircle anchors.centerIn: parent - width: Math.min(parent.width, parent.height) + width: Math.min(400, Math.min(parent.width, parent.height)) height: width radius: width / 2 color: Style.tileBackgroundColor @@ -223,52 +223,22 @@ ThingPageBase { property Thing thing: root.thing readonly property State powerState: thing ? thing.stateByName("power") : null - property color borderColor: "#ffd649" + ActionQueue { id: actionQueue thing: powerController.thing stateType: thing.thingClass.stateTypes.findByName("power") } - Rectangle { - id: background - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - height: width - color: Style.tileBackgroundColor - radius: width / 2 - - ColorIcon { - anchors.centerIn: parent - size: Style.hugeIconSize - name: (actionQueue.pendingValue || powerState.value) === true ? "light-on" : "light-off" - // color: (actionQueue.pendingValue || powerState.value) === true ? Style.accentColor : Style.iconColor + CircleBackground { + anchors.fill: parent + anchors.margins: Style.bigMargins + onColor: "#ffd649" + iconSource: (actionQueue.pendingValue || powerState.value) === true ? "light-on" : "light-off" + on: (actionQueue.pendingValue || powerState.value) === true ? 1 : 0 + onClicked: { + actionQueue.sendValue(!powerState.value) } - - MouseArea { - anchors.fill: parent - onClicked: { - actionQueue.sendValue(!powerState.value) - } - } - } - - RadialGradient { - id: gradient - anchors.fill: background - visible: false - gradient: Gradient{ - GradientStop { position: .45; color: "transparent" } - GradientStop { position: .5; color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 1) } - } - } - OpacityMask { - opacity: (actionQueue.pendingValue || powerState.value) === true ? 1 : 0 - anchors.fill: gradient - source: gradient - maskSource: background - Behavior on opacity { NumberAnimation { duration: Style.animationDuration } } - } } } diff --git a/nymea-app/ui/devicepages/PowersocketDevicePage.qml b/nymea-app/ui/devicepages/PowersocketDevicePage.qml index 87cf3bd9..ea9d6c15 100644 --- a/nymea-app/ui/devicepages/PowersocketDevicePage.qml +++ b/nymea-app/ui/devicepages/PowersocketDevicePage.qml @@ -31,61 +31,31 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 -import QtQuick.Controls.Material 2.1 +import QtGraphicalEffects 1.0 import Nymea 1.0 import "../components" +import "../utils" ThingPageBase { id: root readonly property State powerState: thing.stateByName("power") - readonly property var powerActionType: thing.thingClass.actionTypes.findByName("power"); - GridLayout { + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "power" + } + + CircleBackground { anchors.fill: parent - anchors.margins: app.margins - columns: app.landscape ? 2 : 1 - rowSpacing: app.margins - columnSpacing: app.margins - Layout.alignment: Qt.AlignCenter - - Item { - Layout.preferredWidth: Math.max(Style.iconSize * 6, parent.width / 5) - Layout.preferredHeight: width - Layout.topMargin: app.margins - Layout.bottomMargin: app.landscape ? app.margins : 0 - Layout.alignment: Qt.AlignCenter - Layout.rowSpan: app.landscape ? 4 : 1 - Layout.fillHeight: true - - AbstractButton { - height: Math.min(parent.height, parent.width) - width: height - anchors.centerIn: parent - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: root.powerState.value === true ? Style.accentColor : Style.iconColor - border.width: 4 - radius: width / 2 - } - - ColorIcon { - id: bulbIcon - anchors.fill: parent - anchors.margins: app.margins * 1.5 - name: "../images/powersocket.svg" - color: root.powerState.value === true ? Style.accentColor : Style.iconColor - } - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.powerActionType.paramTypes.get(0).id; - param["value"] = !root.powerState.value; - params.push(param) - engine.thingManager.executeAction(root.thing.id, root.powerActionType.id, params); - } - } + anchors.margins: Style.hugeMargins + iconSource: "../images/powersocket.svg" + onColor: app.interfaceToColor("powersocket") + on: (actionQueue.pendingValue || powerState.value) === true + onClicked: { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + actionQueue.sendValue(!root.powerState.value) } } } diff --git a/nymea-app/ui/devicepages/ShutterDevicePage.qml b/nymea-app/ui/devicepages/ShutterDevicePage.qml index a4046232..2de5ba58 100644 --- a/nymea-app/ui/devicepages/ShutterDevicePage.qml +++ b/nymea-app/ui/devicepages/ShutterDevicePage.qml @@ -36,6 +36,7 @@ import QtGraphicalEffects 1.0 import Nymea 1.0 import "../components" import "../customviews" +import "../utils" ThingPageBase { id: root @@ -52,259 +53,391 @@ ThingPageBase { readonly property bool moving: movingState ? movingState.value === true : false - readonly property int percentage: percentageState ? percentageState.value : 50 - readonly property int angle: angleState ? angleState.value : 0 - onMovingChanged: if (!moving) angleMovable.visible = false +// onMovingChanged: if (!moving) angleMovable.visible = false GridLayout { anchors.fill: parent - columns: root.isVenetian ? - root.landscape ? 3 : 2 - : root.landscape ? 2 : 1 +// columns: root.isVenetian ? +// root.landscape ? 3 : 2 +// : root.landscape ? 2 : 1 + columns: root.landscape ? 2 : 1 - Item { - id: window - - Layout.preferredWidth: root.landscape ? - Math.min(parent.width *.4, parent.height) - : Math.min(Math.min(parent.width, 500), (parent.height - shutterControlsContainer.minimumHeight)) / (root.isVenetian ? 2 : 1) -// Layout.preferredWidth: root.landscape ? -// Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) - app.margins -// : Math.min(Math.min(parent.width, parent.height - shutterControlsContainer.minimumHeight), 500) - Layout.preferredHeight: root.landscape ? - width - : width * (root.isVenetian ? 2 : 1) - Layout.alignment: root.landscape ? Qt.AlignVCenter : Qt.AlignHCenter - clip: true - - ClosablesControlLarge { - anchors { left: parent.left; top: parent.top; bottom: parent.bottom; } - width: height - thing: root.thing - - ClosableArrowAnimation { - id: arrowAnimation - anchors.centerIn: parent - anchors.horizontalCenterOffset: isVenetian ? -width: 0 - - onStateChanged: { - if (state != "") { - animationTimer.start(); - } - } - - Timer { - id: animationTimer - running: false - interval: 5000 - repeat: false - onTriggered: parent.state = "" - } - } - - } - } - - - Item { - id: angleControls - Layout.preferredWidth: root.landscape ? window.width / 2 : window.width - Layout.preferredHeight: window.height - visible: root.isVenetian + CircleBackground { + id: background + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: Style.hugeMargins Item { + id: blind anchors.fill: parent - Item { - anchors { fill: parent; topMargin: parent.height * .09; bottomMargin: parent.height * 0.09; leftMargin: app.margins * 2; rightMargin: app.margins * 2 } + Rectangle { + anchors.centerIn: parent + height: parent.height + width: 2 + color: Style.accentColor + visible: root.angleState + } - Repeater { - model: 10 - Item { - width: parent.height * .1 - height: width - y: parent.height / 10 * index + Canvas { + id: canvas + anchors.centerIn: parent + anchors.horizontalCenterOffset: root.angleState ? -parent.width / 4 - Style.smallMargins: 0 + width: background.contentItem.width / (root.angleState ? 2 : 1) + height: background.contentItem.height - Rectangle { - anchors.centerIn: parent - width: parent.width - height: width / 4 - rotation: root.angle - color: "#808080" - } - } - } + property real progress: root.percentageState ? + percentageDragArea.pressed ? percentageDragArea.draggedProgress : root.percentageState.value / 100 + : .5 - Item { - id: angleMovable - anchors.fill: parent - property int angle: 0 - visible: false + anchors.verticalCenterOffset: -height * (1 - progress) - Repeater { - model: 10 - Item { - width: parent.height * .1 - height: width - y: parent.height / 10 * index + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); - Rectangle { - anchors.centerIn: parent - width: parent.width - height: width / 4 - rotation: angleMovable.angle - color: Style.foregroundColor - opacity: 0.1 - } - } - - } - } - - Item { - anchors { top: parent.top; bottom: parent.bottom; right: parent.right; rightMargin: app.margins / 2 } - width: parent.width * .5 - - Rectangle { - id: angleSlider - anchors.fill: parent - color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.1) - visible: false - ColorIcon { - anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; topMargin: app.margins } - height: Style.iconSize - width: Style.iconSize - name: "../images/up.svg" - } - ColorIcon { - anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: app.margins } - height: Style.iconSize - width: Style.iconSize - name: "../images/down.svg" - } - Rectangle { - width: parent.width - height: 2 - color: angleMouseArea.containsMouse ? Style.accentColor : "transparent" - y: angleMouseArea.mouseY - onYChanged: sliderMask.update() - } - - } - Rectangle { - id: mask - anchors.fill: parent - radius: Style.cornerRadius - color: "blue" - visible: false - } - OpacityMask { - id: sliderMask - anchors.fill: parent - source: angleSlider - maskSource: mask - } - - MouseArea { - id: angleMouseArea - anchors.fill: parent - // angle : totalAngle = mouseY : height - property int totalAngle: root.angleState ? root.angleStateType.maxValue - root.angleStateType.minValue : 0 - property int angle: root.angleState ? totalAngle * mouseY / height + root.angleStateType.minValue : 0 - hoverEnabled: true - - property int startY: 0 - - onPressed: { - startY = mouseY - angleMovable.visible = true - } - onMouseYChanged: if (pressed) angleMovable.angle = angle - - onReleased: { - print("released at", angle) - var targetAngle = 0 - if (Math.abs(mouseY - startY) < 5) { - print("clicked") - // clicked without drag - if (mouseY < width) { - print("top area") - // clicked in top area - if (root.angle > 5) { - targetAngle = 0; - } else { - targetAngle = root.angleStateType.minValue - } - } else if (mouseY > height - width){ - print("bottom area") - //clicked in bottom area - if (root.angle < -5) { - targetAngle = 0; - } else { - targetAngle = root.angleStateType.maxValue - } - } else { - targetAngle = angle - } - - } else { - targetAngle = angle - } - - angleMovable.angle = targetAngle - - - var actionType = root.thing.thingClass.actionTypes.findByName("angle"); - var params = []; - var percentageParam = {} - percentageParam["paramTypeId"] = actionType.paramTypes.findByName("angle").id; - percentageParam["value"] = targetAngle - params.push(percentageParam); - engine.thingManager.executeAction(root.thing.id, actionType.id, params); - - } + ctx.fillStyle = Style.tileForegroundColor + var segments = 10; + var segmentHeight = height / segments + var barHeight = segmentHeight - Style.smallMargins + for (var i = 0; i < segments; i++) { + ctx.fillRect(0, i * segmentHeight + (segmentHeight - barHeight) / 2, width, barHeight) } } } + + ActionQueue { + id: percentageActionQueue + thing: root.thing + stateName: "percentage" + } + + MouseArea { + id: percentageDragArea + anchors.centerIn: parent + anchors.horizontalCenterOffset: root.angleState ? -parent.width / 4 - Style.smallMargins: 0 + width: background.contentItem.width / (root.angleState ? 2 : 1) + height: background.contentItem.height + property real draggedProgress: Math.max(0, Math.min(1, mouseY / height)) + onReleased: percentageActionQueue.sendValue(mouseY / height * 100) + } + + Canvas { + id: angleCanvas + anchors.centerIn: parent + visible: root.angleState + anchors.horizontalCenterOffset: parent.width / 4 + width: background.contentItem.width / (root.angleState ? 2 : 1) + height: background.contentItem.height + + property real angle: root.angleState ? + angleDragArea.pressed ? angleDragArea.draggedAngle : root.angleState.value + : 0 + onAngleChanged: requestPaint() + + property real pendingAngle: angle + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + ctx.fillStyle = Style.tileForegroundColor + + var segments = 10; + var segmentHeight = height / segments + var barHeight = Style.smallMargins + var barWidth = width / 4 + ctx.beginPath(); + for (var i = 0; i < segments; i++) { + ctx.save() + ctx.translate(barWidth / 2 + Style.smallMargins, i * segmentHeight + (segmentHeight - barHeight) / 2) + ctx.rotate(angleCanvas.angle * Math.PI / 180) + ctx.fillRect(-barWidth / 2, -barHeight / 2, width / 4, barHeight) + ctx.restore() + } + ctx.closePath() + + + ctx.strokeStyle = Style.accentColor + ctx.lineWidth = 2 + + ctx.save() + ctx.beginPath(); + ctx.translate(barWidth / 2 + Style.smallMargins, (height - barHeight) / 2) + ctx.rotate(angleCanvas.pendingAngle * Math.PI / 180) + ctx.moveTo(-barWidth / 2, 0) + ctx.lineTo(width, 0) + ctx.stroke(); + ctx.closePath(); + ctx.restore() + + ctx.strokeStyle = Style.tileForegroundColor + + ctx.save() + ctx.beginPath(); + ctx.translate(barWidth / 2 + Style.smallMargins, (height - barHeight) / 2) + ctx.rotate(angleCanvas.angle * Math.PI / 180) + ctx.moveTo(-barWidth / 2, 0) + ctx.lineTo(width, 0) + ctx.stroke(); + ctx.closePath(); + ctx.restore() + } + + } + + ActionQueue { + id: angleActionQueue + thing: root.thing + stateName: "angle" + } + + MouseArea { + id: angleDragArea + visible: root.angleState + anchors.fill: angleCanvas + property real draggedAngle: root.angleState ? Math.min(root.angleStateType.maxValue, + Math.max(root.angleStateType.minValue, + mouseY / height * (root.angleStateType.maxValue - root.angleStateType.minValue) + root.angleStateType.minValue)) + : 0 + onReleased: { + print("sending angle", draggedAngle) + angleCanvas.pendingAngle = draggedAngle + angleActionQueue.sendValue(draggedAngle) + } + } + } + + OpacityMask { + anchors.fill: parent + source: ShaderEffectSource { + sourceItem: blind + sourceRect: background.contentItem.childrenRect + hideSource: true + } + maskSource: background + } + } +// Item { +// id: window - Item { - id: shutterControlsContainer - Layout.columnSpan: root.isVenetian && !root.landscape ? 2 : 1 +// Layout.preferredWidth: root.landscape ? +// Math.min(parent.width *.4, parent.height) +// : Math.min(Math.min(parent.width, 500), (parent.height - shutterControlsContainer.minimumHeight)) / (root.isVenetian ? 2 : 1) +//// Layout.preferredWidth: root.landscape ? +//// Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) - app.margins +//// : Math.min(Math.min(parent.width, parent.height - shutterControlsContainer.minimumHeight), 500) +// Layout.preferredHeight: root.landscape ? +// width +// : width * (root.isVenetian ? 2 : 1) +// Layout.alignment: root.landscape ? Qt.AlignVCenter : Qt.AlignHCenter +// clip: true + +// ClosablesControlLarge { +// anchors { left: parent.left; top: parent.top; bottom: parent.bottom; } +// width: height +// thing: root.thing + +// ClosableArrowAnimation { +// id: arrowAnimation +// anchors.centerIn: parent +// anchors.horizontalCenterOffset: isVenetian ? -width: 0 + +// onStateChanged: { +// if (state != "") { +// animationTimer.start(); +// } +// } + +// Timer { +// id: animationTimer +// running: false +// interval: 5000 +// repeat: false +// onTriggered: parent.state = "" +// } +// } + +// } +// } + + +// Item { +// id: angleControls +// Layout.preferredWidth: root.landscape ? window.width / 2 : window.width +// Layout.preferredHeight: window.height +// visible: root.isVenetian + +// Item { +// anchors.fill: parent + +// Item { +// anchors { fill: parent; topMargin: parent.height * .09; bottomMargin: parent.height * 0.09; leftMargin: app.margins * 2; rightMargin: app.margins * 2 } + +// Repeater { +// model: 10 +// Item { +// width: parent.height * .1 +// height: width +// y: parent.height / 10 * index + +// Rectangle { +// anchors.centerIn: parent +// width: parent.width +// height: width / 4 +// rotation: root.angle +// color: "#808080" +// } +// } +// } + +// Item { +// id: angleMovable +// anchors.fill: parent +// property int angle: 0 +// visible: false + +// Repeater { +// model: 10 +// Item { +// width: parent.height * .1 +// height: width +// y: parent.height / 10 * index + +// Rectangle { +// anchors.centerIn: parent +// width: parent.width +// height: width / 4 +// rotation: angleMovable.angle +// color: Style.foregroundColor +// opacity: 0.1 +// } +// } + +// } +// } + +// Item { +// anchors { top: parent.top; bottom: parent.bottom; right: parent.right; rightMargin: app.margins / 2 } +// width: parent.width * .5 + +// Rectangle { +// id: angleSlider +// anchors.fill: parent +// color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, 0.1) +// visible: false +// ColorIcon { +// anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; topMargin: app.margins } +// height: Style.iconSize +// width: Style.iconSize +// name: "../images/up.svg" +// } +// ColorIcon { +// anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: app.margins } +// height: Style.iconSize +// width: Style.iconSize +// name: "../images/down.svg" +// } +// Rectangle { +// width: parent.width +// height: 2 +// color: angleMouseArea.containsMouse ? Style.accentColor : "transparent" +// y: angleMouseArea.mouseY +// onYChanged: sliderMask.update() +// } + +// } +// Rectangle { +// id: mask +// anchors.fill: parent +// radius: Style.cornerRadius +// color: "blue" +// visible: false +// } +// OpacityMask { +// id: sliderMask +// anchors.fill: parent +// source: angleSlider +// maskSource: mask +// } + +// MouseArea { +// id: angleMouseArea +// anchors.fill: parent +// // angle : totalAngle = mouseY : height +// property int totalAngle: root.angleState ? root.angleStateType.maxValue - root.angleStateType.minValue : 0 +// property int angle: root.angleState ? totalAngle * mouseY / height + root.angleStateType.minValue : 0 +// hoverEnabled: true + +// property int startY: 0 + +// onPressed: { +// startY = mouseY +// angleMovable.visible = true +// } +// onMouseYChanged: if (pressed) angleMovable.angle = angle + +// onReleased: { +// print("released at", angle) +// var targetAngle = 0 +// if (Math.abs(mouseY - startY) < 5) { +// print("clicked") +// // clicked without drag +// if (mouseY < width) { +// print("top area") +// // clicked in top area +// if (root.angle > 5) { +// targetAngle = 0; +// } else { +// targetAngle = root.angleStateType.minValue +// } +// } else if (mouseY > height - width){ +// print("bottom area") +// //clicked in bottom area +// if (root.angle < -5) { +// targetAngle = 0; +// } else { +// targetAngle = root.angleStateType.maxValue +// } +// } else { +// targetAngle = angle +// } + +// } else { +// targetAngle = angle +// } + +// angleMovable.angle = targetAngle + + +// var actionType = root.thing.thingClass.actionTypes.findByName("angle"); +// var params = []; +// var percentageParam = {} +// percentageParam["paramTypeId"] = actionType.paramTypes.findByName("angle").id; +// percentageParam["value"] = targetAngle +// params.push(percentageParam); +// engine.thingManager.executeAction(root.thing.id, actionType.id, params); + +// } +// } +// } +// } +// } +// } + + + + ShutterControls { + id: shutterControls Layout.fillWidth: true - Layout.maximumWidth: 500 -// Layout.preferredWidth: root.landscape ? Math.max(parent.width / 2, shutterControls.implicitWidth) : parent.width - Layout.margins: app.margins * 2 - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - property int minimumHeight: Style.iconSize * 2.5 - property int minimumWidth: Style.iconSize * 2.5 * 3 - - ShutterControls { - id: shutterControls - thing: root.thing - width: parent.width - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - size: Style.bigIconSize - backgroundEnabled: true - spacing: (width - Style.iconSize*2*children.length) / (children.length - 1) - - property int count: children.length - - onActivated: { - if (button == "open") { - arrowAnimation.state = "opening" - } else if (button == "close") { - arrowAnimation.state = "closing" - } else { - arrowAnimation.state = "" - } - } - } + size: Style.bigIconSize + backgroundEnabled: true + thing: root.thing } } } diff --git a/nymea-app/ui/devicepages/ThermostatDevicePage.qml b/nymea-app/ui/devicepages/ThermostatDevicePage.qml index 2926190c..9334e7a3 100644 --- a/nymea-app/ui/devicepages/ThermostatDevicePage.qml +++ b/nymea-app/ui/devicepages/ThermostatDevicePage.qml @@ -35,6 +35,7 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" import "../customviews" +import "../utils" ThingPageBase { id: root @@ -58,48 +59,32 @@ ThingPageBase { anchors.margins: app.margins columns: app.landscape ? 2 : 1 - ThermostatController { + CircleBackground { Layout.fillWidth: true Layout.fillHeight: true - thing: root.thing + Layout.margins: Style.bigMargins + ThermostatController { + anchors.centerIn: parent + height: Math.min(400, Math.min(parent.height, parent.width)) + width: height + thing: root.thing + } } - Rectangle { - Layout.preferredWidth: app.landscape ? parent.width / 2 : parent.width - Layout.preferredHeight: 50 - visible: root.boostStateType - border.color: boostMouseArea.pressed || root.boostStateType && root.boostState.value === true ? Style.accentColor : Style.foregroundColor - border.width: 1 - radius: height / 2 - color: root.boostStateType && root.boostState.value === true ? Style.accentColor : "transparent" + ProgressButton { + Layout.alignment: Qt.AlignHCenter + Layout.margins: Style.bigMargins + size: Style.largeIconSize + imageSource: "thermostat/heating" + backgroundColor: app.interfaceToColor("heating") + visible: root.boostState + busy: actionQueue.pendingValue ? actionQueue.pendingValue : (root.boostState && root.boostState.value === true) + onClicked: actionQueue.sendValue(!root.boostState.value) - Row { - anchors.centerIn: parent - spacing: app.margins / 2 - ColorIcon { - height: Style.iconSize - width: Style.iconSize - name: "../images/sensors/temperature.svg" - color: root.boostStateType && root.boostState.value === true ? "red" : Style.iconColor - } - - Label { - text: qsTr("Boost") - anchors.verticalCenter: parent.verticalCenter - } - } - MouseArea { - id: boostMouseArea - anchors.fill: parent - onPressedChanged: PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.boostStateType.id - param["value"] = !root.boostState.value - params.push(param) - engine.thingManager.executeAction(root.thing.id, root.boostStateType.id, params); - } + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "boost" } } } diff --git a/nymea-app/ui/devicepages/VentilationDevicePage.qml b/nymea-app/ui/devicepages/VentilationDevicePage.qml index d49df611..e4101304 100644 --- a/nymea-app/ui/devicepages/VentilationDevicePage.qml +++ b/nymea-app/ui/devicepages/VentilationDevicePage.qml @@ -31,73 +31,55 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 -import QtQuick.Controls.Material 2.1 import Nymea 1.0 import "../components" +import "../utils" ThingPageBase { id: root readonly property State powerState: thing.stateByName("power") - readonly property ActionType powerActionType: thing.thingClass.actionTypes.findByName("power"); + readonly property State flowRateState: thing.stateByName("flowRate") + readonly property StateType flowRateStateType: thing.thingClass.stateTypes.findByName("flowRate") - GridLayout { + ActionQueue { + id: actionQueue + thing: root.thing + stateName: "power" + } + + CircleBackground { + id: background anchors.fill: parent - anchors.margins: app.margins - columns: app.landscape ? 2 : 1 - rowSpacing: app.margins - columnSpacing: app.margins - Layout.alignment: Qt.AlignCenter - - Item { - Layout.preferredWidth: Math.max(Style.iconSize * 6, parent.width / 5) - Layout.preferredHeight: width - Layout.topMargin: app.margins - Layout.bottomMargin: app.landscape ? app.margins : 0 - Layout.alignment: Qt.AlignCenter - Layout.rowSpan: app.landscape ? 4 : 1 - Layout.fillHeight: true - - AbstractButton { - height: Math.min(parent.height, parent.width) - width: height - anchors.centerIn: parent - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: root.powerState.value === true ? Style.accentColor : Style.iconColor - border.width: 4 - radius: width / 2 - } - - ColorIcon { - id: bulbIcon - anchors.fill: parent - anchors.margins: app.margins * 1.5 - name: "../images/ventilation.svg" - color: root.powerState.value === true ? Style.accentColor : Style.iconColor - - PropertyAnimation on rotation { - running: root.powerState.value === true - duration: 2000 - from: 0 - to: 360 - loops: Animation.Infinite - onDurationChanged: { - running = false; - running = true; - } - } - } - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.powerActionType.paramTypes.get(0).id; - param["value"] = !root.powerState.value; - params.push(param) - engine.thingManager.executeAction(root.thing.id, root.powerActionType.id, params); - } + anchors.margins: Style.hugeMargins + iconSource: "ventilation" + onColor: app.interfaceToColor("ventilation") + on: (actionQueue.pendingValue || powerState.value) === true + onClicked: { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + actionQueue.sendValue(!root.powerState.value) + } + PropertyAnimation on rotation { + running: root.powerState.value === true + duration: 2000 + from: 0 + to: 360 + loops: Animation.Infinite + onDurationChanged: { + running = false; + running = true; } } } + + Dial { + anchors.centerIn: parent + height: background.contentItem.height + width: background.contentItem.width + visible: root.flowRateState + + thing: root.thing + stateName: "flowRate" + color: app.interfaceToColor("ventilation") + } } diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index b2450cf0..c6f69483 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -29,6 +29,8 @@ Item { page = "WeatherDevicePage.qml"; } else if (interfaceList.indexOf("heating") >= 0) { page = "HeatingDevicePage.qml"; + } else if (interfaceList.indexOf("cooling") >= 0) { + page = "CoolingThingPage.qml"; } else if (interfaceList.indexOf("thermostat") >= 0) { page = "ThermostatDevicePage.qml"; } else if (interfaceList.indexOf("sensor") >= 0) { @@ -47,6 +49,8 @@ Item { page = "NotificationsDevicePage.qml"; } else if (interfaceList.indexOf("fingerprintreader") >= 0) { page = "FingerprintReaderDevicePage.qml"; + } else if (interfaceList.indexOf("evcharger") >= 0) { + page = "EvChargerThingPage.qml" } else if (interfaceList.indexOf("smartmeter") >= 0) { page = "SmartMeterDevicePage.qml" } else if (interfaceList.indexOf("powersocket") >= 0) { @@ -120,7 +124,7 @@ Item { "water": "/ui/images/sensors/water.svg", "wind": "/ui/images/sensors/windspeed.svg", "cloud": "/ui/images/weathericons/weather-clouds.svg", - "send": "/ui/images/send.svg" + "send": "/ui/images/send.svg", } function namedIcon(name) { if (!namedIcons.hasOwnProperty(name)) {