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)) {