Merge PR #666: Modernize and align controllable things pages

This commit is contained in:
Jenkins nymea 2021-09-20 17:02:03 +02:00
commit 0eed335226
31 changed files with 1623 additions and 1168 deletions

View File

@ -144,9 +144,13 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent)
addInterface("heating", tr("Heating")); addInterface("heating", tr("Heating"));
addStateType("heating", "power", QVariant::Bool, true, tr("Heating enabled"), tr("Heating enabled changed"), tr("Enable 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")); addInterface("media", tr("Media"));

View File

@ -32,7 +32,8 @@
#include "thingclass.h" #include "thingclass.h"
#include "thingmanager.h" #include "thingmanager.h"
#include <QDebug> #include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcThingManager)
Thing::Thing(ThingManager *thingManager, ThingClass *thingClass, const QUuid &parentId, QObject *parent) : Thing::Thing(ThingManager *thingManager, ThingClass *thingClass, const QUuid &parentId, QObject *parent) :
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 &params) int Thing::executeAction(const QString &actionName, const QVariantList &params)
{ {
ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName); 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; QVariantList finalParams;
foreach (const QVariant &paramVariant, params) { foreach (const QVariant &paramVariant, params) {

View File

@ -171,6 +171,7 @@ void ConfiguredHostsModel::saveToDisk()
QSettings settings; QSettings settings;
settings.beginGroup("ConfiguredHosts"); settings.beginGroup("ConfiguredHosts");
settings.remove(""); settings.remove("");
settings.setValue("currentIndex", m_currentIndex);
for (int i = 0; i < m_list.count(); i++) { for (int i = 0; i < m_list.count(); i++) {
settings.beginGroup(QString::number(i)); settings.beginGroup(QString::number(i));
settings.setValue("uuid", m_list.at(i)->uuid()); settings.setValue("uuid", m_list.at(i)->uuid());

View File

@ -58,7 +58,7 @@
<file>ui/devicepages/GarageThingPage.qml</file> <file>ui/devicepages/GarageThingPage.qml</file>
<file>ui/devicepages/AwningThingPage.qml</file> <file>ui/devicepages/AwningThingPage.qml</file>
<file>ui/devicepages/NotificationsDevicePage.qml</file> <file>ui/devicepages/NotificationsDevicePage.qml</file>
<file>ui/devicepages/LightDevicePage.qml</file> <file>ui/devicepages/LightThingPage.qml</file>
<file>ui/devicepages/FingerprintReaderDevicePage.qml</file> <file>ui/devicepages/FingerprintReaderDevicePage.qml</file>
<file>ui/devicepages/DeviceLogPage.qml</file> <file>ui/devicepages/DeviceLogPage.qml</file>
<file>ui/devicelistpages/GenericThingsListPage.qml</file> <file>ui/devicelistpages/GenericThingsListPage.qml</file>
@ -254,5 +254,8 @@
<file>ui/system/ZigbeeNetworkPage.qml</file> <file>ui/system/ZigbeeNetworkPage.qml</file>
<file>ui/components/ConnectionInfoDialog.qml</file> <file>ui/components/ConnectionInfoDialog.qml</file>
<file>ui/components/ButtonControls.qml</file> <file>ui/components/ButtonControls.qml</file>
<file>ui/components/CircleBackground.qml</file>
<file>ui/devicepages/CoolingThingPage.qml</file>
<file>ui/devicepages/EvChargerThingPage.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -149,6 +149,7 @@ ApplicationWindow {
"powersocket", "powersocket",
"thermostat", "thermostat",
"heating", "heating",
"cooling",
"smartlock", "smartlock",
"doorbell", "doorbell",
"irrigation", "irrigation",

View File

@ -80,7 +80,8 @@ Item {
"smartmeterconsumer": "orange", "smartmeterconsumer": "orange",
"smartmeterproducer": "lightgreen", "smartmeterproducer": "lightgreen",
"energymeter": "deepskyblue", "energymeter": "deepskyblue",
"heating" : "gainsboro", "heating" : "crimson",
"cooling": "dodgerBlue",
"thermostat": "dodgerblue", "thermostat": "dodgerblue",
"irrigation": "lightblue", "irrigation": "lightblue",
"windspeedsensor": "blue", "windspeedsensor": "blue",
@ -88,7 +89,9 @@ Item {
"watersensor": "aqua", "watersensor": "aqua",
"phsensor": "green", "phsensor": "green",
"o2sensor": "lightblue", "o2sensor": "lightblue",
"orpsensor": "yellow" "orpsensor": "yellow",
"powersocket": "aquamarine",
"evcharger": "limegreen"
} }
property var stateColors: { property var stateColors: {
@ -97,7 +100,10 @@ Item {
"currentPower": "deepskyblue", "currentPower": "deepskyblue",
} }
property color red: "#952727" property color red: "indianred"
property color green: "mediumseagreen"
property color yellow: "gold"
property color white: "white" property color white: "white"
property color gray: "gray" property color gray: "gray"
property color darkGray: "darkGray" property color darkGray: "darkGray"

View File

@ -35,12 +35,12 @@ import "../utils"
Item { Item {
id: root id: root
implicitWidth: orientation == Qt.Horizontal ? 300 : Style.hugeIconSize implicitWidth: orientation == Qt.Horizontal ? 300 : 12
implicitHeight: orientation == Qt.Horizontal ? Style.hugeIconSize : 300 implicitHeight: orientation == Qt.Horizontal ? 12 : 300
property Thing thing: null 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 property int value: thing.stateByName("brightness").value
@ -49,7 +49,7 @@ Item {
ActionQueue { ActionQueue {
id: actionQueue id: actionQueue
thing: root.thing thing: root.thing
stateType: root.colorTemperatureStateType stateName: "brightness"
} }
Rectangle { Rectangle {
@ -84,17 +84,18 @@ Item {
y: root.orientation === Qt.Vertical ? y: root.orientation === Qt.Vertical ?
root.height - dragHandle.height - ((actionQueue.pendingValue || root.value) * (root.height - dragHandle.height) / 100) root.height - dragHandle.height - ((actionQueue.pendingValue || root.value) * (root.height - dragHandle.height) / 100)
: 0 : 0
height: root.orientation === Qt.Horizontal ? parent.height : 8 height: 14
width: root.orientation === Qt.Horizontal ? 8 : parent.width width: 14
radius: 4 radius: 7
color: Style.foregroundColor color: Style.foregroundColor
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
anchors.margins: -Style.smallMargins
onPositionChanged: { onPositionChanged: {
var minCt = root.colorTemperatureStateType.minValue; var minCt = root.brightnessStateType.minValue;
var maxCt = root.colorTemperatureStateType.maxValue var maxCt = root.brightnessStateType.maxValue
var ct; var ct;
if (root.orientation == Qt.Horizontal) { if (root.orientation == Qt.Horizontal) {
ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt))

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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
}
}

View File

@ -8,16 +8,31 @@ Item {
property Thing thing: null 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 { ActionQueue {
id: actionQueue id: actionQueue
thing: root.thing thing: root.thing
stateType: thing.thingClass.stateTypes.findByName("color") stateType: thing.thingClass.stateTypes.findByName("color")
property bool useStoredPoint: false
property point storedPoint: Qt.point(0, 0)
} }
ConicalGradient { ConicalGradient {
id: gradient id: gradient
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(parent.width, parent.height) width: Math.min(400, Math.min(parent.width, parent.height))
height: width height: width
visible: false visible: false
gradient: Gradient{ gradient: Gradient{
@ -30,6 +45,29 @@ Item {
GradientStop { position: 0.833; color: Qt.rgba(1, 0, 1, 1) } GradientStop { position: 0.833; color: Qt.rgba(1, 0, 1, 1) }
GradientStop { position: 1.000; color: Qt.rgba(1, 0, 0, 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 { Rectangle {
@ -39,25 +77,106 @@ Item {
} }
OpacityMask { OpacityMask {
anchors.fill: gradient anchors.fill: gradient
source: gradient source: colorizer
maskSource: mask maskSource: mask
} }
RadialGradient {
anchors.fill: gradient Rectangle {
gradient: Gradient{ id: dragHandle
GradientStop { position: 0.05; color: Qt.rgba(1, 1, 1, 1) } width: 20
GradientStop { position: 0.10; color: Qt.rgba(1, 1, 1, .9) } height: 20
GradientStop { position: 0.20; color: Qt.rgba(1, 1, 1, .7) } radius: height / 2
GradientStop { position: 0.30; color: Qt.rgba(1, 1, 1, .5) } color: Style.backgroundColor
GradientStop { position: 0.40; color: Qt.rgba(1, 1, 1, .3) } border.color: Style.foregroundColor
GradientStop { position: 0.50; color: "transparent" } 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)
} }
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: gradient
onPositionChanged: { onPositionChanged: {
var angle = calculateAngle(mouseX, mouseY) var angle = calculateAngle(mouseX, mouseY)
@ -98,6 +217,13 @@ Item {
1) 1)
actionQueue.sendValue(color); 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) { function calculateAngle(mouseX, mouseY) {

View File

@ -10,9 +10,10 @@ Item {
property Thing thing: null 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 StateType colorTemperatureStateType: root.thing.thingClass.stateTypes.findByName("colorTemperature")
readonly property State powerState: root.thing.stateByName("power")
property int value: thing.stateByName("colorTemperature").value property int value: thing.stateByName("colorTemperature").value
@ -23,45 +24,55 @@ Item {
} }
Rectangle { Rectangle {
id: clipRect id: background
anchors.fill: parent width: Math.min(400, Math.min(parent.width, parent.height))
radius: Style.cornerRadius anchors.centerIn: parent
} height: width
radius: width / 2
LinearGradient { visible: false
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 { gradient: Gradient {
GradientStop { position: 0.0; color: "#dfffff" } GradientStop { position: 0.0; color: "#dfffff" }
GradientStop { position: 0.5; color: "#ffffea" } GradientStop { position: 0.5; color: "#ffffea" }
GradientStop { position: 1.0; color: "#ffd649" } 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
}
} }
Rectangle { Desaturate {
id: dragHandle anchors.fill: background
property double valuePercentage: ((actionQueue.pendingValue || root.value) - root.colorTemperatureStateType.minValue) / (root.colorTemperatureStateType.maxValue - root.colorTemperatureStateType.minValue) source: background
x: orientation == Qt.Horizontal ? valuePercentage * (root.width - dragHandle.width) : 0 desaturation: root.powerState.value === true ? 0 : 1
y: root.orientation === Qt.Vertical ? root.height - dragHandle.height - (valuePercentage * (root.height - dragHandle.height)) : 0 Behavior on desaturation { NumberAnimation { duration: Style.animationDuration } }
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 { MouseArea {
anchors.fill: parent anchors.fill: background
onPositionChanged: { onPositionChanged: {
var minCt = root.colorTemperatureStateType.minValue; var minCt = root.colorTemperatureStateType.minValue;
var maxCt = root.colorTemperatureStateType.maxValue var maxCt = root.colorTemperatureStateType.maxValue
var ct; var ct;
if (root.orientation == Qt.Horizontal) { // if (root.orientation == Qt.Horizontal) {
ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt)) // ct = Math.min(maxCt, Math.max(minCt, (mouseX * (maxCt - minCt) / (width - dragHandle.width)) + minCt))
} else { // } else {
ct = Math.min(maxCt, Math.max(minCt, ((height - mouseY) * (maxCt - minCt) / (height - dragHandle.height)) + minCt)) // 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);
} }
} }

View File

@ -32,321 +32,174 @@ import QtQuick 2.5
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import Nymea 1.0 import Nymea 1.0
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Controls.Material 2.2 import "../utils"
ColumnLayout { Item {
id: dial id: root
property Thing thing: null property Thing thing: null
property alias device: dial.thing property string stateName: ""
property StateType stateType: null property StateType stateType: thing ? thing.thingClass.stateTypes.findByName(stateName) : null
property bool showValueLabel: true
property int steps: 10
property color color: Style.accentColor property color color: Style.accentColor
property int maxAngle: 235 property int precision: 1
// value : max = angle : maxAngle readonly property State progressState: thing ? thing.states.getState(stateType.id) : null
function valueToAngle(value) { readonly property State powerState: thing ? thing.stateByName("power") : null
return (value - from) * maxAngle / (to - from)
} property int startAngle: 135
function angleToValue(angle) { property int maxAngle: 270
return (to - from) * angle / maxAngle + from 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 ActionQueue {
readonly property double from: dial.stateType ? dial.stateType.minValue : 0 id: powerActionQueue
readonly property double to: dial.stateType ? dial.stateType.maxValue : 100 thing: root.thing
readonly property double anglePerStep: maxAngle / dial.steps stateName: "power"
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)
}
} }
Connections { Connections {
target: engine.thingManager target: root.progressState
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
onValueChanged: { onValueChanged: {
if (!d.busy) { canvas.requestPaint()
rotationButton.rotation = dial.valueToAngle(dial.thingState.value) }
}
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 { MouseArea {
id: buttonContainer anchors.fill: canvas
Layout.fillWidth: true
Layout.fillHeight: true
Item { property bool dragging: false
id: innerDial property double lastAngle
property double angleDiff: 0
height: Math.min(parent.height, parent.width) * .9 onPressed: {
width: height angleDiff = 0
anchors.centerIn: parent lastAngle = calculateAngle(mouseX, mouseY)
rotation: dial.startAngle }
Rectangle { onReleased: {
anchors.fill: rotationButton if (!dragging && root.powerState) {
radius: height / 2 PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
border.color: Style.foregroundColor powerActionQueue.sendValue(!root.powerState.value)
border.width: 2 }
color: "transparent" dragging = false
opacity: rotateMouseArea.pressed && !rotateMouseArea.grabbed ? .7 : 1 }
onPositionChanged: {
var angle = calculateAngle(mouseX, mouseY)
var tmpDiff = angle - lastAngle
if (tmpDiff > 300) {
tmpDiff -= 360
}
if (tmpDiff < -300) {
tmpDiff += 360
} }
Item { lastAngle = angle;
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
}
Item { angleDiff += tmpDiff
id: handle if (Math.abs(angleDiff) > 1) {
anchors.horizontalCenter: parent.horizontalCenter dragging = true
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 } }
}
}
} }
Repeater { var valueDiff = angleDiff / root.anglePerStep * root.stepSize
id: indexLEDs valueDiff = canvas.roundToPrecision(valueDiff)
model: dial.steps + 1 if (Math.abs(valueDiff) > 0) {
var currentValue = actionQueue.pendingValue || root.progressState.value
Item { var newValue = currentValue + valueDiff
height: parent.height newValue = Math.min(root.stateType.maxValue, Math.max(root.stateType.minValue, newValue))
width: parent.width * .04 if (currentValue !== newValue) {
anchors.centerIn: parent actionQueue.sendValue(newValue)
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 steps = Math.round(valueDiff / root.stepSize)
angleDiff -= steps * root.anglePerStep
} }
} }
Rectangle { function calculateAngle(mouseX, mouseY) {
id: buttonBorder // transform coords to center of dial
height: innerDial.height * .8 mouseX -= canvas.width / 2
width: height mouseY -= canvas.height / 2
anchors.centerIn: parent
radius: height / 2
border.color: Style.foregroundColor
opacity: .3
border.width: width * .025
color: "transparent"
}
Label { var rad = Math.atan(mouseY / mouseX);
anchors { left: innerDial.left; bottom: innerDial.bottom; bottomMargin: innerDial.height * .1 } var angle = rad * 180 / Math.PI
text: "MIN"
font.pixelSize: innerDial.height * .06
visible: dial.stateType !== null
}
Label { angle += 90;
anchors { right: innerDial.right; bottom: innerDial.bottom; bottomMargin: innerDial.height * .1 }
text: "MAX"
font.pixelSize: innerDial.height * .06
visible: dial.stateType !== null
}
ColorIcon { if (mouseX < 0 && mouseY >= 0) angle = 180 + angle;
anchors.centerIn: innerDial if (mouseX < 0 && mouseY < 0) angle = 180 + angle;
height: innerDial.height * .2
width: height
name: "../images/system-shutdown.svg"
visible: dial.powerStateType !== null
color: d.poweredColor
Behavior on color { ColorAnimation { duration: 200 } }
}
MouseArea { return angle;
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;
}
} }
} }
} }

View File

@ -46,7 +46,8 @@ Item {
property alias color: icon.color property alias color: icon.color
property alias backgroundColor: background.color property alias backgroundColor: background.color
property bool longpressEnabled: true property bool longpressEnabled: false
property bool busy: false
property int size: Style.iconSize 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 { Rectangle {
id: background id: background
anchors.fill: parent anchors.fill: parent
@ -136,9 +147,8 @@ Item {
property bool inverted: false property bool inverted: false
readonly property int penWidth: 2 readonly property int penWidth: 2
onProgressChanged: {
requestPaint() onProgressChanged: requestPaint()
}
Connections { Connections {
target: buttonDelegate target: buttonDelegate
onPressedChanged: { onPressedChanged: {
@ -152,21 +162,49 @@ Item {
onPaint: { onPaint: {
var ctx = getContext("2d"); var ctx = getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height); 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; // Draw longpress progress
var stop = -Math.PI / 2; if (canvas.progress > 0) {
if (inverted) { ctx.save();
start += canvas.progress * 2 * Math.PI ctx.fillStyle = Qt.rgba(1, 0, 0, 1);
} else { ctx.lineWidth = canvas.penWidth
stop += canvas.progress * 2 * Math.PI 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.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.stroke();
ctx.closePath();
} }
} }

View File

@ -30,13 +30,13 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import Nymea 1.0 import Nymea 1.0
RowLayout { Item {
id: root id: root
spacing: 0 implicitHeight: size * 4
implicitWidth: size * 7
property Thing thing: null property Thing thing: null
readonly property State openState: thing.stateByName("state") readonly property State openState: thing.stateByName("state")
@ -48,47 +48,65 @@ RowLayout {
signal activated(string button); 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 { Item { Layout.fillWidth: true; Layout.fillHeight: true }
longpressEnabled: false
imageSource: root.invert ? "../images/down.svg" : "../images/up.svg" ProgressButton {
mode: root.backgroundEnabled ? "normal" : "transparent" imageSource: root.invert ? "../images/down.svg" : "../images/up.svg"
size: root.size backgroundColor: root.backgroundEnabled ? Style.green : "transparent"
color: root.openState && root.openState.value === "opening" ? Material.accent : Style.iconColor size: root.size
onClicked: { busy: root.openState ? root.openState.value === "opening" : openBusyTimer.running
engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("open").id) onClicked: {
root.activated("open") 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 { ProgressButton {
visible: root.canStop visible: root.canStop
longpressEnabled: false backgroundColor: root.backgroundEnabled ? Style.yellow : "transparent"
mode: root.backgroundEnabled ? "normal" : "transparent" size: root.size
size: root.size imageSource: "../images/media-playback-stop.svg"
imageSource: "../images/media-playback-stop.svg" onClicked: {
onClicked: { engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("stop").id)
engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("stop").id) root.activated("stop")
root.activated("stop") openBusyTimer.stop()
closeBusyTimer.stop()
}
} }
}
Item { Layout.fillWidth: true; Layout.fillHeight: true } Item { Layout.fillWidth: true; Layout.fillHeight: true }
ProgressButton { ProgressButton {
imageSource: root.invert ? "../images/up.svg" : "../images/down.svg" imageSource: root.invert ? "../images/up.svg" : "../images/down.svg"
longpressEnabled: false backgroundColor: root.backgroundEnabled ? Style.red : "transparent"
mode: root.backgroundEnabled ? "normal" : "transparent" size: root.size
size: root.size busy: root.openState ? root.openState.value === "closing" : closeBusyTimer.running
color: root.openState && root.openState.value === "closing" ? Material.accent : Style.iconColor onClicked: {
onClicked: { engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("close").id)
engine.thingManager.executeAction(root.thing.id, root.thing.thingClass.actionTypes.findByName("close").id) root.activated("close")
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 }
}
} }

View File

@ -1,6 +1,7 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0
import "../components" import "../components"
import Nymea 1.0 import Nymea 1.0
@ -42,86 +43,77 @@ Item {
: Math.min(Math.min(parent.width, 500), parent.height - shutterControlsContainer.minimumHeight) : Math.min(Math.min(parent.width, 500), parent.height - shutterControlsContainer.minimumHeight)
Layout.preferredHeight: width Layout.preferredHeight: width
ColorIcon { Rectangle {
id: background
anchors.centerIn: parent anchors.centerIn: parent
size: Math.min(parent.height, parent.width) - Style.hugeMargins * 2 width: Math.min(500, Math.min(parent.width, parent.height) - Style.hugeMargins * 2)
property string currentImage: { height: width
if (root.isExtended) { radius: width / 2
return NymeaUtils.pad(Math.round(root.percentageState.value / 10), 2) + "0" color: Style.tileBackgroundColor
} }
if (root.intermediatePositionStateType) {
return root.stateState.value === "closed" ? "100"
: root.intermediatePositionState.value === false ? "000" : "050"
}
return "100"
}
name: "../images/garage/garage-" + currentImage + ".svg"
Item {
Item { id: door
id: arrows anchors.fill: background
Canvas {
id: canvas
anchors.centerIn: parent anchors.centerIn: parent
width: Style.iconSize * 2 width: parent.width
height: parent.height * .6 height: parent.height + Style.margins
clip: true anchors.verticalCenterOffset: root.percentageState
visible: root.stateStateType && (root.stateState.value === "opening" || root.stateState.value === "closing") ? -height * (1 - (root.percentageState.value / 100))
property bool up: root.stateState && root.stateState.value === "opening" : -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 ctx.fillStyle = Style.tileForegroundColor
// we need to somehow stop and start the animation var segments = 10;
property bool animationHack: true var segmentHeight = height / segments
onAnimationHackChanged: { var barHeight = segmentHeight - Style.smallMargins
if (!animationHack) hackTimer.start(); for (var i = 0; i < segments; i++) {
} ctx.fillRect(0, i * segmentHeight, width, barHeight)
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
}
} }
} }
} }
} }
OpacityMask {
anchors.fill: background
source: ShaderEffectSource {
sourceItem: door
hideSource: true
}
maskSource: background
}
} }
Item { Item {
id: shutterControlsContainer id: shutterControlsContainer
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: minimumWidth Layout.minimumWidth: minimumWidth
Layout.fillHeight: true Layout.preferredHeight: Style.bigIconSize * 4
property int minimumWidth: Style.iconSize * 10 property int minimumWidth: Style.iconSize * 10
property int minimumHeight: Style.iconSize * 2.5 property int minimumHeight: Style.iconSize * 2.5
ProgressButton { ProgressButton {
anchors.centerIn: parent anchors.centerIn: parent
mode: "highlight"
visible: root.isImpulseBased visible: root.isImpulseBased
longpressEnabled: false longpressEnabled: false
size: Style.bigIconSize
imageSource: "../images/closable-move.svg" imageSource: "../images/closable-move.svg"
busy: busyTimer.running
onClicked: { onClicked: {
var actionTypeId = root.thing.thingClass.actionTypes.findByName("triggerImpulse").id var actionTypeId = root.thing.thingClass.actionTypes.findByName("triggerImpulse").id
print("Triggering impulse", actionTypeId) print("Triggering impulse", actionTypeId)
engine.thingManager.executeAction(root.thing.id, actionTypeId) engine.thingManager.executeAction(root.thing.id, actionTypeId)
busyTimer.start();
}
Timer {
id: busyTimer
interval: 5000
} }
} }
@ -132,7 +124,6 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
backgroundEnabled: true backgroundEnabled: true
size: Style.bigIconSize size: Style.bigIconSize
spacing: (parent.width - Style.iconSize*2*children.length) / (children.length - 1)
visible: !root.isImpulseBased visible: !root.isImpulseBased
} }
} }

View File

@ -90,9 +90,9 @@ Item {
if (targetTempStep === step) { if (targetTempStep === step) {
if (currentTempStep && currentTempStep < targetTempStep) { if (currentTempStep && currentTempStep < targetTempStep) {
ctx.strokeStyle = "red"; ctx.strokeStyle = app.interfaceToColor("heating");
} else if (currentTempStep && currentTempStep > targetTempStep) { } else if (currentTempStep && currentTempStep > targetTempStep) {
ctx.strokeStyle = "dodgerblue"; ctx.strokeStyle = app.interfaceToColor("cooling");
} else { } else {
ctx.strokeStyle = Style.accentColor; ctx.strokeStyle = Style.accentColor;
} }
@ -100,16 +100,16 @@ Item {
ctx.lineWidth = 4; ctx.lineWidth = 4;
} else if (currentTempStep && currentTempStep === step) { } else if (currentTempStep && currentTempStep === step) {
if (currentTempStep < targetTempStep) { if (currentTempStep < targetTempStep) {
ctx.strokeStyle = "red"; ctx.strokeStyle = app.interfaceToColor("heating");
} else { } else {
ctx.strokeStyle = "dodgerblue"; ctx.strokeStyle = app.interfaceToColor("cooling");
} }
ctx.lineWidth = 3; ctx.lineWidth = 3;
} else if (currentTempStep && currentTempStep < step && step < targetTempStep) { } else if (currentTempStep && currentTempStep < step && step < targetTempStep) {
ctx.strokeStyle = "red"; ctx.strokeStyle = app.interfaceToColor("heating");
ctx.lineWidth = 2; ctx.lineWidth = 2;
} else if (currentTempStep && currentTempStep > step && step > targetTempStep) { } else if (currentTempStep && currentTempStep > step && step > targetTempStep) {
ctx.strokeStyle = "dodgerblue"; ctx.strokeStyle = app.interfaceToColor("cooling");
ctx.lineWidth = 2; ctx.lineWidth = 2;
} else { } else {
ctx.strokeStyle = Style.tileOverlayColor; ctx.strokeStyle = Style.tileOverlayColor;
@ -127,8 +127,8 @@ Item {
var outerPointX = center.x + (outerRadius * c); var outerPointX = center.x + (outerRadius * c);
var outerPointY = center.x + (outerRadius * s); var outerPointY = center.x + (outerRadius * s);
context.moveTo(innerPointX, innerPointY); ctx.moveTo(innerPointX, innerPointY);
context.lineTo(outerPointX, outerPointY); ctx.lineTo(outerPointX, outerPointY);
ctx.stroke(); ctx.stroke();
ctx.closePath(); ctx.closePath();
} }
@ -166,16 +166,16 @@ Item {
ColorIcon { ColorIcon {
width: Style.largeIconSize width: Style.largeIconSize
height: width 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 name: root.heatingOnState && root.heatingOnState.value === true
? "../images/thermostat/heating.svg" ? "../images/thermostat/heating.svg"
: root.coolingOnState && root.coolingOnState.value === true : root.coolingOnState && root.coolingOnState.value === true
? "../images/thermostat/cooling.svg" ? "../images/thermostat/cooling.svg"
: "" : ""
color: root.heatingOnState && root.heatingOnState.value === true color: root.heatingOnState && root.heatingOnState.value === true
? "red" ? app.interfaceToColor("heating")
: root.coolingOnState && root.coolingOnState.value === true : root.coolingOnState && root.coolingOnState.value === true
? "dodgerblue" ? app.interfaceToColor("cooling")
: Style.iconColor : Style.iconColor
} }

View File

@ -74,6 +74,7 @@ MainPageTile {
// Open interface specific things list // Open interface specific things list
switch (iface.name) { switch (iface.name) {
case "heating": case "heating":
case "cooling":
case "sensor": case "sensor":
page = "SensorsDeviceListPage.qml" page = "SensorsDeviceListPage.qml"
break; break;
@ -172,6 +173,7 @@ MainPageTile {
case "extendedsmartmeterconsumer": case "extendedsmartmeterconsumer":
case "extendedsmartmeterproducer": case "extendedsmartmeterproducer":
case "heating": case "heating":
case "cooling":
case "thermostat": case "thermostat":
return sensorComponent; return sensorComponent;
// return labelComponent; // return labelComponent;
@ -209,7 +211,7 @@ MainPageTile {
case "light": case "light":
var group = engine.thingManager.createGroup(Interfaces.findByName("colorlight"), thingsProxy); var group = engine.thingManager.createGroup(Interfaces.findByName("colorlight"), thingsProxy);
print("opening lights page for group", group) print("opening lights page for group", group)
pageStack.push("../devicepages/LightDevicePage.qml", {thing: group}) pageStack.push("../devicepages/LightThingPage.qml", {thing: group})
} }
} }
} }

View File

@ -143,6 +143,8 @@ ThingsListPageBase {
ShutterControls { ShutterControls {
id: shutterControls id: shutterControls
Layout.fillWidth: false Layout.fillWidth: false
Layout.preferredWidth: Style.iconSize * 5
Layout.preferredHeight: Style.iconSize
height: parent.height height: parent.height
thing: itemDelegate.thing thing: itemDelegate.thing
invert: root.invertControls invert: root.invertControls

View File

@ -108,6 +108,26 @@ ThingsListPageBase {
ThingStatusIcons { ThingStatusIcons {
thing: itemDelegate.thing 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
}
} }
} }
} }

View File

@ -107,7 +107,7 @@ ThingsListPageBase {
property bool colorInverted: tileColored && NymeaUtils.isDark(Style.foregroundColor) === NymeaUtils.isDark(colorState.value) property bool colorInverted: tileColored && NymeaUtils.isDark(Style.foregroundColor) === NymeaUtils.isDark(colorState.value)
onClicked: { onClicked: {
if (isEnabled && (colorState || colorTemperatureState)) { if (isEnabled /*&& (colorState || colorTemperatureState)*/) {
root.enterPage(index) root.enterPage(index)
} else { } else {
itemDelegate.wobble() itemDelegate.wobble()

View File

@ -30,11 +30,12 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.2 import QtGraphicalEffects 1.0
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../customviews" import "../customviews"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
@ -48,75 +49,71 @@ ThingPageBase {
anchors.fill: parent anchors.fill: parent
columns: root.landscape ? 2 : 1 columns: root.landscape ? 2 : 1
Item { CircleBackground {
id: shutterImage id: background
Layout.preferredWidth: root.landscape ? Layout.fillHeight: true
Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) Layout.fillWidth: true
: Math.min(Math.min(500, parent.width), parent.height - shutterControlsContainer.minimumHeight) Layout.margins: Style.hugeMargins
Layout.preferredHeight: width 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 anchors.centerIn: parent
size: Math.min(parent.height, parent.width) - Style.hugeMargins * 2 width: background.contentItem.width
name: root.isExtended ? height: background.contentItem.height
"../images/awning/awning-" + app.pad(Math.round(root.percentageState.value / 10) * 10, 3) + ".svg" property real draggedProgress: mouseY / height
: "../images/awning/awning-100.svg" onMouseYChanged: print("mouseY", mouseY, draggedProgress)
onReleased: {
actionQueue.sendValue(draggedProgress * 100)
}
} }
} }
Item {
id: shutterControlsContainer ShutterControls {
id: shutterControls
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.minimumWidth: implicitWidth
Layout.margins: app.margins * 2 Layout.preferredHeight: implicitHeight
property int minimumWidth: Style.iconSize * 2.7 * 3
property int minimumHeight: Style.iconSize * 4.5
ColumnLayout { thing: root.thing
anchors.centerIn: parent size: Style.bigIconSize
width: parent.width backgroundEnabled: true
spacing: app.margins invert: true
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
}
}
} }
} }
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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)
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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")
}
}

View File

@ -34,58 +34,26 @@ import QtQuick.Layouts 1.1
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
readonly property State powerState: thing.stateByName("power") 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.fill: parent
anchors.margins: app.margins anchors.margins: Style.hugeMargins
columns: app.landscape ? 2 : 1 iconSource: "thermostat/heating"
rowSpacing: app.margins onColor: app.interfaceToColor("heating")
columnSpacing: app.margins on: actionQueue.pendingValue || root.powerState.value
Layout.alignment: Qt.AlignCenter onClicked: actionQueue.sendValue(!root.powerState.value)
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);
}
}
}
} }
} }

View File

@ -34,6 +34,7 @@ import QtQuick.Layouts 1.1
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
@ -149,54 +150,36 @@ ThingPageBase {
filterValue: root.thing.id filterValue: root.thing.id
} }
ActionQueue {
id: actionQueue
thing: root.thing
stateName: "power"
}
GridLayout { GridLayout {
id: mainGrid id: mainGrid
anchors.fill: parent anchors.fill: parent
anchors.margins: app.margins
columns: app.landscape ? 2 : 1 columns: app.landscape ? 2 : 1
Item { CircleBackground {
Layout.preferredWidth: app.landscape ? parent.width * .4 : parent.width Layout.fillWidth: true
Layout.preferredHeight: app.landscape ? parent.height : parent.height *.4 Layout.fillHeight: true
Layout.leftMargin: Style.hugeMargins; Layout.rightMargin: Style.hugeMargins; Layout.topMargin: Style.hugeMargins
AbstractButton { iconSource: "irrigation"
height: Math.min(Math.min(parent.height, parent.width), Style.iconSize * 5) onColor: app.interfaceToColor("irrigation")
width: height on: actionQueue.pendingValue || root.powerState.value
anchors.centerIn: parent onClicked: actionQueue.sendValue(!root.powerState.value)
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)
}
}
} }
ColumnLayout { ColumnLayout {
Layout.preferredWidth: app.landscape ? parent.width * .6 : parent.width Layout.fillWidth: true
Layout.preferredHeight: app.landscape ? parent.height : parent.height * .6 Layout.fillHeight: true
Layout.margins: Style.margins
Item { Layout.fillWidth: true; Layout.fillHeight: true } Item { Layout.fillWidth: true; Layout.fillHeight: true }
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: root.isOn ? qsTr("Watering since") text: root.isOn ? qsTr("Watering since")
: history.lastWatering ? qsTr("Last watering") : history.lastWatering ? qsTr("Last watering")
@ -205,7 +188,7 @@ ThingPageBase {
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: history.lastWatering ? Qt.formatDateTime(history.lastWatering) : "" text: history.lastWatering ? Qt.formatDateTime(history.lastWatering) : ""
font.pixelSize: app.largeFont font.pixelSize: app.largeFont
@ -213,7 +196,7 @@ ThingPageBase {
} }
Label { Label {
Layout.fillWidth: true 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 horizontalAlignment: Text.AlignHCenter
font.pixelSize: app.smallFont font.pixelSize: app.smallFont
text: { text: {
@ -252,7 +235,7 @@ ThingPageBase {
Label { Label {
Layout.fillWidth: true 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 horizontalAlignment: Text.AlignHCenter
text: tagsProxy.count > 0 ? text: tagsProxy.count > 0 ?
//: Irrigation will be turned of at, e.g. 09:00 //: Irrigation will be turned of at, e.g. 09:00
@ -299,7 +282,7 @@ ThingPageBase {
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins Layout.leftMargin: Style.margins; Layout.rightMargin: Style.margins
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
visible: tagsProxy.count > 0 visible: tagsProxy.count > 0
font.pixelSize: app.largeFont font.pixelSize: app.largeFont
@ -308,7 +291,7 @@ ThingPageBase {
} }
Label { Label {
Layout.fillWidth: true 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 horizontalAlignment: Text.AlignHCenter
visible: tagsProxy.count > 0 visible: tagsProxy.count > 0
font.pixelSize: app.smallFont font.pixelSize: app.smallFont

View File

@ -1,188 +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 <https://www.gnu.org/licenses/>.
*
* 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.1
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: app.margins
columns: app.landscape ? root.statesCount : 1
rowSpacing: app.margins
columnSpacing: app.margins
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: Item {
Layout.preferredWidth: Style.hugeIconSize
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"
}
}
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 {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumHeight: width
thing: root.thing
visible: root.thing.thingClass.stateTypes.findByName("color") !== null
}
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
}
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
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}])
}
}
}
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
}
}
}
}

View File

@ -0,0 +1,308 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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.hugeMargins
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(400, 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
ActionQueue {
id: actionQueue
thing: powerController.thing
stateType: thing.thingClass.stateTypes.findByName("power")
}
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)
}
}
}
}
}
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)
}
}
}
}

View File

@ -31,61 +31,31 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls.Material 2.1 import QtGraphicalEffects 1.0
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
readonly property State powerState: thing.stateByName("power") 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.fill: parent
anchors.margins: app.margins anchors.margins: Style.hugeMargins
columns: app.landscape ? 2 : 1 iconSource: "../images/powersocket.svg"
rowSpacing: app.margins onColor: app.interfaceToColor("powersocket")
columnSpacing: app.margins on: (actionQueue.pendingValue || powerState.value) === true
Layout.alignment: Qt.AlignCenter onClicked: {
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
Item { actionQueue.sendValue(!root.powerState.value)
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);
}
}
} }
} }
} }

View File

@ -36,6 +36,7 @@ import QtGraphicalEffects 1.0
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../customviews" import "../customviews"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
@ -52,259 +53,391 @@ ThingPageBase {
readonly property bool moving: movingState ? movingState.value === true : false 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 { GridLayout {
anchors.fill: parent anchors.fill: parent
columns: root.isVenetian ? // columns: root.isVenetian ?
root.landscape ? 3 : 2 // root.landscape ? 3 : 2
: root.landscape ? 2 : 1 // : root.landscape ? 2 : 1
columns: root.landscape ? 2 : 1
Item { CircleBackground {
id: window id: background
Layout.fillHeight: true
Layout.preferredWidth: root.landscape ? Layout.fillWidth: true
Math.min(parent.width *.4, parent.height) Layout.margins: Style.hugeMargins
: 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 { Item {
id: blind
anchors.fill: parent anchors.fill: parent
Item { Rectangle {
anchors { fill: parent; topMargin: parent.height * .09; bottomMargin: parent.height * 0.09; leftMargin: app.margins * 2; rightMargin: app.margins * 2 } anchors.centerIn: parent
height: parent.height
width: 2
color: Style.accentColor
visible: root.angleState
}
Repeater { Canvas {
model: 10 id: canvas
Item { anchors.centerIn: parent
width: parent.height * .1 anchors.horizontalCenterOffset: root.angleState ? -parent.width / 4 - Style.smallMargins: 0
height: width width: background.contentItem.width / (root.angleState ? 2 : 1)
y: parent.height / 10 * index height: background.contentItem.height
Rectangle { property real progress: root.percentageState ?
anchors.centerIn: parent percentageDragArea.pressed ? percentageDragArea.draggedProgress : root.percentageState.value / 100
width: parent.width : .5
height: width / 4
rotation: root.angle
color: "#808080"
}
}
}
Item { anchors.verticalCenterOffset: -height * (1 - progress)
id: angleMovable
anchors.fill: parent
property int angle: 0
visible: false
Repeater { onPaint: {
model: 10 var ctx = getContext("2d");
Item { ctx.reset();
width: parent.height * .1
height: width
y: parent.height / 10 * index
Rectangle { ctx.fillStyle = Style.tileForegroundColor
anchors.centerIn: parent var segments = 10;
width: parent.width var segmentHeight = height / segments
height: width / 4 var barHeight = segmentHeight - Style.smallMargins
rotation: angleMovable.angle for (var i = 0; i < segments; i++) {
color: Style.foregroundColor ctx.fillRect(0, i * segmentHeight + (segmentHeight - barHeight) / 2, width, barHeight)
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);
}
} }
} }
} }
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 { // Layout.preferredWidth: root.landscape ?
id: shutterControlsContainer // Math.min(parent.width *.4, parent.height)
Layout.columnSpan: root.isVenetian && !root.landscape ? 2 : 1 // : 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.fillWidth: true
Layout.maximumWidth: 500 size: Style.bigIconSize
// Layout.preferredWidth: root.landscape ? Math.max(parent.width / 2, shutterControls.implicitWidth) : parent.width backgroundEnabled: true
Layout.margins: app.margins * 2 thing: root.thing
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 = ""
}
}
}
} }
} }
} }

View File

@ -35,6 +35,7 @@ import QtQuick.Layouts 1.1
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../customviews" import "../customviews"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
@ -58,48 +59,32 @@ ThingPageBase {
anchors.margins: app.margins anchors.margins: app.margins
columns: app.landscape ? 2 : 1 columns: app.landscape ? 2 : 1
ThermostatController { CircleBackground {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: 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 { ProgressButton {
Layout.preferredWidth: app.landscape ? parent.width / 2 : parent.width Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: 50 Layout.margins: Style.bigMargins
visible: root.boostStateType size: Style.largeIconSize
border.color: boostMouseArea.pressed || root.boostStateType && root.boostState.value === true ? Style.accentColor : Style.foregroundColor imageSource: "thermostat/heating"
border.width: 1 backgroundColor: app.interfaceToColor("heating")
radius: height / 2 visible: root.boostState
color: root.boostStateType && root.boostState.value === true ? Style.accentColor : "transparent" busy: actionQueue.pendingValue ? actionQueue.pendingValue : (root.boostState && root.boostState.value === true)
onClicked: actionQueue.sendValue(!root.boostState.value)
Row { ActionQueue {
anchors.centerIn: parent id: actionQueue
spacing: app.margins / 2 thing: root.thing
ColorIcon { stateName: "boost"
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);
}
} }
} }
} }

View File

@ -31,73 +31,55 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Controls.Material 2.1
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../utils"
ThingPageBase { ThingPageBase {
id: root id: root
readonly property State powerState: thing.stateByName("power") 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.fill: parent
anchors.margins: app.margins anchors.margins: Style.hugeMargins
columns: app.landscape ? 2 : 1 iconSource: "ventilation"
rowSpacing: app.margins onColor: app.interfaceToColor("ventilation")
columnSpacing: app.margins on: (actionQueue.pendingValue || powerState.value) === true
Layout.alignment: Qt.AlignCenter onClicked: {
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
Item { actionQueue.sendValue(!root.powerState.value)
Layout.preferredWidth: Math.max(Style.iconSize * 6, parent.width / 5) }
Layout.preferredHeight: width PropertyAnimation on rotation {
Layout.topMargin: app.margins running: root.powerState.value === true
Layout.bottomMargin: app.landscape ? app.margins : 0 duration: 2000
Layout.alignment: Qt.AlignCenter from: 0
Layout.rowSpan: app.landscape ? 4 : 1 to: 360
Layout.fillHeight: true loops: Animation.Infinite
onDurationChanged: {
AbstractButton { running = false;
height: Math.min(parent.height, parent.width) running = true;
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);
}
} }
} }
} }
Dial {
anchors.centerIn: parent
height: background.contentItem.height
width: background.contentItem.width
visible: root.flowRateState
thing: root.thing
stateName: "flowRate"
color: app.interfaceToColor("ventilation")
}
} }

View File

@ -29,6 +29,8 @@ Item {
page = "WeatherDevicePage.qml"; page = "WeatherDevicePage.qml";
} else if (interfaceList.indexOf("heating") >= 0) { } else if (interfaceList.indexOf("heating") >= 0) {
page = "HeatingDevicePage.qml"; page = "HeatingDevicePage.qml";
} else if (interfaceList.indexOf("cooling") >= 0) {
page = "CoolingThingPage.qml";
} else if (interfaceList.indexOf("thermostat") >= 0) { } else if (interfaceList.indexOf("thermostat") >= 0) {
page = "ThermostatDevicePage.qml"; page = "ThermostatDevicePage.qml";
} else if (interfaceList.indexOf("sensor") >= 0) { } else if (interfaceList.indexOf("sensor") >= 0) {
@ -38,7 +40,7 @@ Item {
} else if (interfaceList.indexOf("garagedoor") >= 0 ) { } else if (interfaceList.indexOf("garagedoor") >= 0 ) {
page = "GarageThingPage.qml"; page = "GarageThingPage.qml";
} else if (interfaceList.indexOf("light") >= 0) { } else if (interfaceList.indexOf("light") >= 0) {
page = "LightDevicePage.qml"; page = "LightThingPage.qml";
} else if (interfaceList.indexOf("shutter") >= 0 || interfaceList.indexOf("blind") >= 0) { } else if (interfaceList.indexOf("shutter") >= 0 || interfaceList.indexOf("blind") >= 0) {
page = "ShutterDevicePage.qml"; page = "ShutterDevicePage.qml";
} else if (interfaceList.indexOf("awning") >= 0) { } else if (interfaceList.indexOf("awning") >= 0) {
@ -47,6 +49,8 @@ Item {
page = "NotificationsDevicePage.qml"; page = "NotificationsDevicePage.qml";
} else if (interfaceList.indexOf("fingerprintreader") >= 0) { } else if (interfaceList.indexOf("fingerprintreader") >= 0) {
page = "FingerprintReaderDevicePage.qml"; page = "FingerprintReaderDevicePage.qml";
} else if (interfaceList.indexOf("evcharger") >= 0) {
page = "EvChargerThingPage.qml"
} else if (interfaceList.indexOf("smartmeter") >= 0) { } else if (interfaceList.indexOf("smartmeter") >= 0) {
page = "SmartMeterDevicePage.qml" page = "SmartMeterDevicePage.qml"
} else if (interfaceList.indexOf("powersocket") >= 0) { } else if (interfaceList.indexOf("powersocket") >= 0) {
@ -120,7 +124,7 @@ Item {
"water": "/ui/images/sensors/water.svg", "water": "/ui/images/sensors/water.svg",
"wind": "/ui/images/sensors/windspeed.svg", "wind": "/ui/images/sensors/windspeed.svg",
"cloud": "/ui/images/weathericons/weather-clouds.svg", "cloud": "/ui/images/weathericons/weather-clouds.svg",
"send": "/ui/images/send.svg" "send": "/ui/images/send.svg",
} }
function namedIcon(name) { function namedIcon(name) {
if (!namedIcons.hasOwnProperty(name)) { if (!namedIcons.hasOwnProperty(name)) {