From f1d29034b0e6bbdd700dd210cdf8f161c80c6fba Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 10 Dec 2018 00:29:14 +0100 Subject: [PATCH] rework the generic device page --- libnymea-app-core/devicemanager.cpp | 8 +- libnymea-app-core/devicemanager.h | 3 +- .../discovery/zeroconfdiscovery.cpp | 2 +- libnymea-app-core/jsonrpc/jsonrpcclient.cpp | 6 +- libnymea-app-core/jsonrpc/jsonrpcclient.h | 4 +- libnymea-app-core/libnymea-app-core.h | 3 + libnymea-app-core/libnymea-app-core.pro | 6 +- libnymea-app-core/models/devicemodel.cpp | 164 +++++ libnymea-app-core/models/devicemodel.h | 77 +++ libnymea-app-core/models/logsmodelng.cpp | 5 + libnymea-common/types/state.h | 2 +- libnymea-common/types/statetypes.cpp | 4 +- nymea-app/resources.qrc | 3 +- nymea-app/ui/NewDeviceWizard.qml | 2 +- nymea-app/ui/RootItem.qml | 5 +- nymea-app/ui/components/ColorPicker.qml | 23 +- nymea-app/ui/components/IconMenuItem.qml | 5 +- nymea-app/ui/components/Led.qml | 18 + nymea-app/ui/customviews/GenericTypeGraph.qml | 28 +- nymea-app/ui/delegates/ActionDelegate.qml | 19 +- nymea-app/ui/delegates/ParamDelegate.qml | 17 +- .../devicelistpages/LightsDeviceListPage.qml | 20 +- nymea-app/ui/devicepages/DeviceLogPage.qml | 291 +++++++++ nymea-app/ui/devicepages/DevicePageBase.qml | 22 +- .../ui/devicepages/GenericDevicePage.qml | 563 ++++++++++++++---- .../GenericDeviceStateDetailsPage.qml | 169 ------ 26 files changed, 1126 insertions(+), 343 deletions(-) create mode 100644 libnymea-app-core/models/devicemodel.cpp create mode 100644 libnymea-app-core/models/devicemodel.h create mode 100644 nymea-app/ui/components/Led.qml create mode 100644 nymea-app/ui/devicepages/DeviceLogPage.qml delete mode 100644 nymea-app/ui/devicepages/GenericDeviceStateDetailsPage.qml diff --git a/libnymea-app-core/devicemanager.cpp b/libnymea-app-core/devicemanager.cpp index 1c569ab8..3347f72f 100644 --- a/libnymea-app-core/devicemanager.cpp +++ b/libnymea-app-core/devicemanager.cpp @@ -239,7 +239,7 @@ void DeviceManager::getConfiguredDevicesResponse(const QVariantMap ¶ms) value.convert(QVariant::Int); } device->setStateValue(stateTypeId, value); - qDebug() << "Set device state value:" << device->stateValue(stateTypeId) << value; +// qDebug() << "Set device state value:" << device->stateValue(stateTypeId) << value; } devices()->addDevice(device); } @@ -297,7 +297,7 @@ void DeviceManager::editDeviceResponse(const QVariantMap ¶ms) void DeviceManager::executeActionResponse(const QVariantMap ¶ms) { qDebug() << "Execute Action response" << params; - emit executeActionReply(params.value("params").toMap()); + emit executeActionReply(params); } void DeviceManager::savePluginConfig(const QUuid &pluginId) @@ -366,7 +366,7 @@ void DeviceManager::editDevice(const QUuid &deviceId, const QString &name) m_jsonClient->sendCommand("Devices.EditDevice", params, this, "editDeviceResponse"); } -void DeviceManager::executeAction(const QUuid &deviceId, const QUuid &actionTypeId, const QVariantList ¶ms) +int DeviceManager::executeAction(const QUuid &deviceId, const QUuid &actionTypeId, const QVariantList ¶ms) { qDebug() << "JsonRpc: execute action " << deviceId.toString() << actionTypeId.toString() << params; QVariantMap p; @@ -377,5 +377,5 @@ void DeviceManager::executeAction(const QUuid &deviceId, const QUuid &actionType } qDebug() << "Params:" << p; - m_jsonClient->sendCommand("Actions.ExecuteAction", p, this, "executeActionResponse"); + return m_jsonClient->sendCommand("Actions.ExecuteAction", p, this, "executeActionResponse"); } diff --git a/libnymea-app-core/devicemanager.h b/libnymea-app-core/devicemanager.h index 25e1991e..a7ecafa8 100644 --- a/libnymea-app-core/devicemanager.h +++ b/libnymea-app-core/devicemanager.h @@ -69,7 +69,7 @@ public: Q_INVOKABLE void confirmPairing(const QUuid &pairingTransactionId, const QString &secret = QString()); Q_INVOKABLE void removeDevice(const QUuid &deviceId, RemovePolicy policy = RemovePolicyNone); Q_INVOKABLE void editDevice(const QUuid &deviceId, const QString &name); - Q_INVOKABLE void executeAction(const QUuid &deviceId, const QUuid &actionTypeId, const QVariantList ¶ms = QVariantList()); + Q_INVOKABLE int executeAction(const QUuid &deviceId, const QUuid &actionTypeId, const QVariantList ¶ms = QVariantList()); private: Q_INVOKABLE void notificationReceived(const QVariantMap &data); @@ -98,6 +98,7 @@ signals: void editDeviceReply(const QVariantMap ¶ms); void executeActionReply(const QVariantMap ¶ms); void fetchingDataChanged(); + void notificationReceived(const QString &deviceId, const QString &eventTypeId, const QVariantList ¶ms); private: Vendors *m_vendors; diff --git a/libnymea-app-core/discovery/zeroconfdiscovery.cpp b/libnymea-app-core/discovery/zeroconfdiscovery.cpp index 39fa8c7b..2f3053e4 100644 --- a/libnymea-app-core/discovery/zeroconfdiscovery.cpp +++ b/libnymea-app-core/discovery/zeroconfdiscovery.cpp @@ -91,7 +91,7 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) version = txtRecord.second; } } -// qDebug() << "avahi service entry added" << serverName << uuid << sslEnabled; + qDebug() << "avahi service entry added" << serverName << uuid << sslEnabled; DiscoveryDevice* device = m_discoveryModel->find(uuid); diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp index 7478668e..e499b1c0 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp @@ -56,15 +56,15 @@ void JsonRpcClient::registerNotificationHandler(JsonHandler *handler, const QStr m_notificationHandlers.insert(handler->nameSpace(), qMakePair(handler, method)); } -void JsonRpcClient::sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller, const QString &callbackMethod) +int JsonRpcClient::sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller, const QString &callbackMethod) { JsonRpcReply *reply = createReply(method, params, caller, callbackMethod); m_replies.insert(reply->commandId(), reply); sendRequest(reply->requestMap()); - + return reply->commandId(); } -void JsonRpcClient::sendCommand(const QString &method, QObject *caller, const QString &callbackMethod) +int JsonRpcClient::sendCommand(const QString &method, QObject *caller, const QString &callbackMethod) { return sendCommand(method, QVariantMap(), caller, callbackMethod); } diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.h b/libnymea-app-core/jsonrpc/jsonrpcclient.h index 7b534da3..0a5a5ac5 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.h +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.h @@ -60,8 +60,8 @@ public: void registerNotificationHandler(JsonHandler *handler, const QString &method); - void sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller = nullptr, const QString &callbackMethod = QString()); - void sendCommand(const QString &method, QObject *caller = nullptr, const QString &callbackMethod = QString()); + int sendCommand(const QString &method, const QVariantMap ¶ms, QObject *caller = nullptr, const QString &callbackMethod = QString()); + int sendCommand(const QString &method, QObject *caller = nullptr, const QString &callbackMethod = QString()); void setConnection(NymeaConnection *connection); bool connected() const; diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index af67aa52..9e0d6744 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -55,6 +55,7 @@ #include "ruletemplates/ruleactiontemplate.h" #include "ruletemplates/ruleactionparamtemplate.h" #include "connection/awsclient.h" +#include "models/devicemodel.h" #include @@ -114,6 +115,8 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "DeviceClassesProxy"); qmlRegisterType(uri, 1, 0, "DeviceDiscovery"); + qmlRegisterType(uri, 1, 0, "DeviceModel"); + qmlRegisterUncreatableType(uri, 1, 0, "RuleManager", "Get it from the Engine"); qmlRegisterUncreatableType(uri, 1, 0, "Rules", "Get it from RuleManager"); qmlRegisterUncreatableType(uri, 1, 0, "Rule", "Get it from Rules"); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index 7ba2784e..271b9531 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -83,7 +83,8 @@ SOURCES += \ configuration/nymeaconfiguration.cpp \ models/mqttpolicies.cpp \ configuration/mqttpolicy.cpp \ - configuration/mqttpolicies.cpp + configuration/mqttpolicies.cpp \ + models/devicemodel.cpp HEADERS += \ engine.h \ @@ -145,7 +146,8 @@ HEADERS += \ configuration/serverconfigurations.h \ configuration/nymeaconfiguration.h \ configuration/mqttpolicy.h \ - configuration/mqttpolicies.h + configuration/mqttpolicies.h \ + models/devicemodel.h unix { target.path = /usr/lib diff --git a/libnymea-app-core/models/devicemodel.cpp b/libnymea-app-core/models/devicemodel.cpp new file mode 100644 index 00000000..285caf13 --- /dev/null +++ b/libnymea-app-core/models/devicemodel.cpp @@ -0,0 +1,164 @@ +#include "devicemodel.h" + +#include "types/statetype.h" + +DeviceModel::DeviceModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +int DeviceModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant DeviceModel::data(const QModelIndex &index, int role) const +{ + if (role == RoleId) { + return m_list.at(index.row()); + } + if (role == RoleType) { + StateType* stateType = m_device->deviceClass()->stateTypes()->getStateType(m_list.at(index.row())); + if (stateType) { + return TypeStateType; + } + ActionType* actionType = m_device->deviceClass()->actionTypes()->getActionType(m_list.at(index.row())); + if (actionType) { + return TypeActionType; + } + EventType* eventType = m_device->deviceClass()->eventTypes()->getEventType(m_list.at(index.row())); + if (eventType) { + return TypeEventType; + } + } + if (role == RoleDisplayName) { + StateType* stateType = m_device->deviceClass()->stateTypes()->getStateType(m_list.at(index.row())); + if (stateType) { + return stateType->displayName(); + } + ActionType* actionType = m_device->deviceClass()->actionTypes()->getActionType(m_list.at(index.row())); + if (actionType) { + return actionType->displayName(); + } + EventType* eventType = m_device->deviceClass()->eventTypes()->getEventType(m_list.at(index.row())); + if (eventType) { + return eventType->displayName(); + } + } + if (role == RoleWritable) { + ActionType* actionType = m_device->deviceClass()->actionTypes()->getActionType(m_list.at(index.row())); + return actionType != nullptr; + } + return QVariant(); +} + +QHash DeviceModel::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleType, "type"); + roles.insert(RoleDisplayName, "displayName"); + roles.insert(RoleWritable, "writable"); + return roles; +} + +QVariant DeviceModel::getData(int index, int role) const +{ + if (index < 0 || index >= m_list.count()) { + return QVariant(); + } + return data(this->index(index), role); +} + +Device *DeviceModel::device() const +{ + return m_device; +} + +void DeviceModel::setDevice(Device *device) +{ + if (m_device != device) { + m_device = device; + emit deviceChanged(); + updateList(); + } +} + +bool DeviceModel::showStates() const +{ + return m_showStates; +} + +void DeviceModel::setShowStates(bool showStates) +{ + if (m_showStates != showStates) { + m_showStates = showStates; + emit showStatesChanged(); + updateList(); + } +} + +bool DeviceModel::showActions() const +{ + return m_showActions; +} + +void DeviceModel::setShowActions(bool showActions) +{ + if (m_showActions != showActions) { + m_showActions = showActions; + emit showActionsChanged(); + updateList(); + } +} + +bool DeviceModel::showEvents() const +{ + return m_showEvents; +} + +void DeviceModel::setShowEvents(bool showEvents) +{ + if (m_showEvents != showEvents) { + m_showEvents = showEvents; + emit showEventsChanged(); + updateList(); + } +} + +void DeviceModel::updateList() +{ + if (!m_device) { + beginResetModel(); + m_list.clear(); + endResetModel(); + emit countChanged(); + return; + } + beginResetModel(); + m_list.clear(); + if (m_showStates) { + for (int i = 0; i < m_device->deviceClass()->stateTypes()->rowCount(); i++) { + m_list.append(m_device->deviceClass()->stateTypes()->get(i)->id()); + } + } + + if (m_showActions) { + for (int i = 0; i < m_device->deviceClass()->actionTypes()->rowCount(); i++) { + if (!m_list.contains(m_device->deviceClass()->actionTypes()->get(i)->id())) { + m_list.append(m_device->deviceClass()->actionTypes()->get(i)->id()); + } + } + } + if (m_showEvents) { + for (int i = 0; i < m_device->deviceClass()->eventTypes()->rowCount(); i++) { + if (!m_list.contains(m_device->deviceClass()->eventTypes()->get(i)->id())) { + m_list.append(m_device->deviceClass()->eventTypes()->get(i)->id()); + } + } + } + + endResetModel(); + emit countChanged(); +} diff --git a/libnymea-app-core/models/devicemodel.h b/libnymea-app-core/models/devicemodel.h new file mode 100644 index 00000000..e7029f5a --- /dev/null +++ b/libnymea-app-core/models/devicemodel.h @@ -0,0 +1,77 @@ +#ifndef DEVICEMODEL_H +#define DEVICEMODEL_H + +#include + +#include "types/device.h" +#include "types/deviceclass.h" + +class DeviceModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + + Q_PROPERTY(Device* device READ device WRITE setDevice NOTIFY deviceChanged) + + Q_PROPERTY(bool showStates READ showStates WRITE setShowStates NOTIFY showStatesChanged) + Q_PROPERTY(bool showActions READ showActions WRITE setShowActions NOTIFY showActionsChanged) + Q_PROPERTY(bool showEvents READ showEvents WRITE setShowEvents NOTIFY showEventsChanged) + +public: + enum Roles { + RoleId, + RoleType, + RoleDisplayName, + RoleWritable + }; + Q_ENUM(Roles) + enum Type { + TypeStateType, + TypeActionType, + TypeEventType + }; + Q_ENUM(Type) + + explicit DeviceModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + Q_INVOKABLE QVariant getData(int index, int role) const; + + Device* device() const; + void setDevice(Device *device); + + bool showStates() const; + void setShowStates(bool showStates); + + bool showActions() const; + void setShowActions(bool showActions); + + bool showEvents() const; + void setShowEvents(bool showEvents); + +signals: + void deviceChanged(); + + void countChanged(); + + bool showStatesChanged(); + bool showActionsChanged(); + bool showEventsChanged(); + +private: + void updateList(); + +private: + Device *m_device = nullptr; + + bool m_showStates = true; + bool m_showActions = true; + bool m_showEvents = true; + + QList m_list; +}; + +#endif // DEVICEMODEL_H diff --git a/libnymea-app-core/models/logsmodelng.cpp b/libnymea-app-core/models/logsmodelng.cpp index 4258f407..cde7a236 100644 --- a/libnymea-app-core/models/logsmodelng.cpp +++ b/libnymea-app-core/models/logsmodelng.cpp @@ -104,6 +104,11 @@ void LogsModelNg::setTypeIds(const QStringList &typeIds) if (m_typeIds != typeIds) { m_typeIds = typeIds; emit typeIdsChanged(); + beginResetModel(); + qDeleteAll(m_list); + m_list.clear(); + endResetModel(); + fetchMore(); } } diff --git a/libnymea-common/types/state.h b/libnymea-common/types/state.h index 6884cf2d..ed005a8b 100644 --- a/libnymea-common/types/state.h +++ b/libnymea-common/types/state.h @@ -35,7 +35,7 @@ class State : public QObject Q_PROPERTY(QVariant value READ value NOTIFY valueChanged) public: - explicit State(const QUuid &deviceId, const QUuid &stateTypeId, const QVariant &value, QObject *parent = 0); + explicit State(const QUuid &deviceId, const QUuid &stateTypeId, const QVariant &value, QObject *parent = nullptr); QUuid deviceId() const; QUuid stateTypeId() const; diff --git a/libnymea-common/types/statetypes.cpp b/libnymea-common/types/statetypes.cpp index b80fc9fe..7bf0c274 100644 --- a/libnymea-common/types/statetypes.cpp +++ b/libnymea-common/types/statetypes.cpp @@ -36,7 +36,9 @@ QList StateTypes::stateTypes() StateType *StateTypes::get(int index) const { - qDebug() << "returning" << m_stateTypes.at(index); + if (index < 0 || index >= m_stateTypes.count()) { + return nullptr; + } return m_stateTypes.at(index); } diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 278f854f..500749b6 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -60,7 +60,6 @@ ui/customviews/ExtendedVolumeController.qml ui/devicepages/MediaDevicePage.qml ui/devicepages/ButtonDevicePage.qml - ui/devicepages/GenericDeviceStateDetailsPage.qml ui/devicepages/GenericDevicePage.qml ui/devicepages/WeatherDevicePagePre110.qml ui/devicepages/WeatherDevicePagePost110.qml @@ -137,5 +136,7 @@ ui/system/MqttBrokerSettingsPage.qml ui/system/ServerConfigurationDialog.qml ui/system/MqttPolicyPage.qml + ui/devicepages/DeviceLogPage.qml + ui/components/Led.qml diff --git a/nymea-app/ui/NewDeviceWizard.qml b/nymea-app/ui/NewDeviceWizard.qml index 6f860f84..67c2c8bb 100644 --- a/nymea-app/ui/NewDeviceWizard.qml +++ b/nymea-app/ui/NewDeviceWizard.qml @@ -123,7 +123,7 @@ Page { print("should setup", deviceClass.name, deviceClass.setupMethod, deviceClass.createMethods, deviceClass["discoveryParamTypes"].count) } - swipe.enabled: true// deviceClass.createMethods.indexOf("CreateMethodUser") !== -1 + swipe.enabled: deviceClass.createMethods.indexOf("CreateMethodUser") !== -1 swipe.right: MouseArea { height: deviceClassDelegate.height width: height diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index f7b8b3f0..eba85d1c 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -74,9 +74,10 @@ Item { } Engine { - id: engine + id: engineObject } - property alias _engine: engine + readonly property Engine engine: engineObject + readonly property Engine _engine: engineObject // In case a child cannot use "engine" property int connectionTabIndex: index onConnectionTabIndexChanged: tabSettings.lastConnectedHost = engine.connection.url diff --git a/nymea-app/ui/components/ColorPicker.qml b/nymea-app/ui/components/ColorPicker.qml index 0f87e147..30a383fe 100644 --- a/nymea-app/ui/components/ColorPicker.qml +++ b/nymea-app/ui/components/ColorPicker.qml @@ -4,13 +4,28 @@ Item { id: root property color color: actionState - property Component touchDelegate: null property bool pressed: mouseArea.pressed property bool hovered: mouseArea.containsMouse - property variant lights property bool active: true + property Component touchDelegate: Rectangle { + height: 15 + width: height + radius: height / 2 + color: app.accentColor + + + Rectangle { + color: root.hovered || root.pressed ? "#11000000" : "transparent" + anchors.centerIn: parent + height: 30 + width: height + radius: width / 2 + Behavior on color { ColorAnimation { duration: 200 } } + } + } + function calculateXy(color) { if (!color.hasOwnProperty("r")) { // Qt 4.8's color doesn't have r,g,b properties @@ -189,7 +204,7 @@ Item { x: item ? Math.max(0, Math.min(point.x - width * .5, parent.width - item.width)) : 0 y: item ? Math.max(0, Math.min(point.y - height * .5, parent.height - item.height)) : 0 sourceComponent: root.touchDelegate - visible: mouseArea.draggedItem != touchDelegateLoader && root.active + visible: mouseArea.draggedItem !== touchDelegateLoader && root.active // Behavior on x { // enabled: !mouseArea.pressed // NumberAnimation {} @@ -199,6 +214,6 @@ Item { Loader { id: dndItem sourceComponent: root.touchDelegate - visible: mouseArea.draggedItem != undefined && root.active + visible: mouseArea.draggedItem !== undefined && root.active } } diff --git a/nymea-app/ui/components/IconMenuItem.qml b/nymea-app/ui/components/IconMenuItem.qml index 3c57eb63..de70a7c3 100644 --- a/nymea-app/ui/components/IconMenuItem.qml +++ b/nymea-app/ui/components/IconMenuItem.qml @@ -5,13 +5,14 @@ import QtQuick.Layouts 1.3 MenuItem { id: root property alias iconSource: icon.name + implicitWidth: 200 contentItem: RowLayout { spacing: app.margins ColorIcon { id: icon - height: parent.height - width: height + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height } Label { id: label diff --git a/nymea-app/ui/components/Led.qml b/nymea-app/ui/components/Led.qml new file mode 100644 index 00000000..7ca8b7ae --- /dev/null +++ b/nymea-app/ui/components/Led.qml @@ -0,0 +1,18 @@ +import QtQuick 2.9 + +Item { + id: root + implicitHeight: app.iconSize * .8 + implicitWidth: height + + property bool on: false + + Rectangle { + height: Math.min(parent.height, parent.height) + width: height + radius: width / 2 + color: root.on ? "lightgreen" : "lightgray" + border.width: 1 + border.color: app.foregroundColor + } +} diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml index 8137884e..d1b9e64b 100644 --- a/nymea-app/ui/customviews/GenericTypeGraph.qml +++ b/nymea-app/ui/customviews/GenericTypeGraph.qml @@ -11,8 +11,8 @@ Item { id: root implicitHeight: width * .6 - property var device: null - property var stateType: null + property Device device: null + property StateType stateType: null property int roundTo: 2 readonly property var valueState: device.states.getState(stateType.id) readonly property var deviceClass: engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); @@ -55,12 +55,20 @@ Item { color: root.color } + Led { + visible: root.stateType.type.toLowerCase() === "bool" + on: root.valueState.value === true + } + Label { Layout.fillWidth: true - text: 1.0 * Math.round(root.valueState.value * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + root.stateType.unitString + text: root.stateType.type.toLowerCase() === "bool" + ? root.stateType.displayName + : 1.0 * Math.round(root.valueState.value * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + root.stateType.unitString font.pixelSize: app.largeFont } + HeaderButton { imageSource: "../images/zoom-out.svg" onClicked: { @@ -96,11 +104,19 @@ Item { ValueAxis { id: yAxis - max: logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05) - min: logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05) + max: Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05)) + min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) labelsFont.pixelSize: app.smallFont + labelFormat: { + switch (root.stateType.type.toLowerCase()) { + case "bool": + return "x"; + default: + return "%d"; + } + } labelsColor: app.foregroundColor - tickCount: chartView.height / 40 + tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .2) gridLineColor: color } diff --git a/nymea-app/ui/delegates/ActionDelegate.qml b/nymea-app/ui/delegates/ActionDelegate.qml index fd239d59..d52019e3 100644 --- a/nymea-app/ui/delegates/ActionDelegate.qml +++ b/nymea-app/ui/delegates/ActionDelegate.qml @@ -8,7 +8,7 @@ import "../components" ItemDelegate { id: root - property ActionType actionType: null + property var actionType: null property var actionState: null signal executeAction(var params) @@ -294,23 +294,6 @@ ItemDelegate { root.executeAction(params) } } - - touchDelegate: Rectangle { - height: 15 - width: height - radius: height / 2 - color: Material.accent - - - Rectangle { - color: colorPicker.hovered || colorPicker.pressed ? "#11000000" : "transparent" - anchors.centerIn: parent - height: 30 - width: height - radius: width / 2 - Behavior on color { ColorAnimation { duration: 200 } } - } - } } } diff --git a/nymea-app/ui/delegates/ParamDelegate.qml b/nymea-app/ui/delegates/ParamDelegate.qml index 3c0d7f7e..8d33a4d1 100644 --- a/nymea-app/ui/delegates/ParamDelegate.qml +++ b/nymea-app/ui/delegates/ParamDelegate.qml @@ -143,14 +143,20 @@ ItemDelegate { Component { id: spinnerComponent SpinBox { - value: root.param.value - from: root.paramType.minValue - to: root.paramType.maxValue + value: root.param.value ? root.param.value : 0 + from: root.paramType.minValue ? root.paramType.minValue : -999999999 + to: root.paramType.maxValue ? root.paramType.maxValue : 999999999 editable: true + width: 150 onValueModified: root.param.value = value textFromValue: function(value) { return value } + Component.onCompleted: { + if (root.value === undefined) { + root.value = value + } + } } } @@ -173,6 +179,11 @@ ItemDelegate { root.param.value = root.paramType.allowedValues[index] print("setting value to", root.param.value) } + Component.onCompleted: { + if (root.value === undefined) { + root.value = model[0] + } + } } } diff --git a/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml b/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml index c828b9a8..facc4476 100644 --- a/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml @@ -78,9 +78,7 @@ DeviceListPageBase { Rectangle { anchors { left: parent.left; top: parent.top; bottom: parent.bottom } width: app.margins / 2 - color: itemDelegate.connectedState !== null && itemDelegate.connectedState.value === false ? - "red" - : itemDelegate.colorStateType ? itemDelegate.colorState.value : "#00000000" + color: itemDelegate.colorStateType ? itemDelegate.colorState.value : "#00000000" } contentItem: ColumnLayout { @@ -105,22 +103,12 @@ DeviceListPageBase { // source: icon // } - Glow { - anchors.fill: icon - radius: 1 - samples: 17 - color: app.backgroundColor - source: icon - } - ColorIcon { id: icon anchors.fill: parent - color: app.accentColor -// anchors.margins: app.margins / 4 -// color: itemDelegate.connectedState !== null && itemDelegate.connectedState.value === false ? -// "red" -// : itemDelegate.colorStateType ? itemDelegate.colorState.value : "#00000000" + color: itemDelegate.connectedState !== null && itemDelegate.connectedState.value === false + ? "red" + : app.accentColor name: itemDelegate.connectedState !== null && itemDelegate.connectedState.value === false ? "../images/dialog-warning-symbolic.svg" : itemDelegate.powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg" diff --git a/nymea-app/ui/devicepages/DeviceLogPage.qml b/nymea-app/ui/devicepages/DeviceLogPage.qml new file mode 100644 index 00000000..8481e246 --- /dev/null +++ b/nymea-app/ui/devicepages/DeviceLogPage.qml @@ -0,0 +1,291 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../customviews" + +Page { + id: root + + property Device device: null + property var filterTypeIds: [] + + header: GuhHeader { + text: qsTr("History for %1").arg(root.device.name) + onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: "../images/filters.svg" + color: logsModelNg.filterEnabled ? app.accentColor : keyColor + onClicked: logsModelNg.filterEnabled = !logsModelNg.filterEnabled + visible: root.filterTypeIds.length === 0 + } + } + + LogsModelNg { + id: logsModelNg + engine: _engine + deviceId: root.device.id + typeIds: root.filterTypeIds.length > 0 + ? root.filterTypeIds + : filterEnabled + ? [filterDeviceModel.getData(filterComboBox.currentIndex, DeviceModel.RoleId)] + : [] + live: true + + property bool filterEnabled: false + } + + DeviceModel { + id: filterDeviceModel + device: root.device + } + + Pane { + id: filterPane + anchors { left: parent.left; top: parent.top; right: parent.right } + Behavior on height { NumberAnimation { duration: 120; easing.type: Easing.InOutQuad } } + + height: logsModelNg.filterEnabled ? implicitHeight + app.margins * 2 : 0 + Material.elevation: 1 + + leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0 + contentItem: Rectangle { + color: app.primaryColor + clip: true + RowLayout { + anchors.fill: parent + anchors.margins: app.margins + spacing: app.margins + Label { + text: qsTr("Filter by") + } + + ComboBox { + id: filterComboBox + Layout.fillWidth: true + textRole: "displayName" + model: filterDeviceModel + } + } + } + } + + Loader { + id: graphLoader + anchors { + left: parent.left + top: filterPane.bottom + right: parent.right + } + + readonly property StateType stateType: root.device.deviceClass.stateTypes.getStateType(root.filterTypeIds[0]) + + readonly property bool canShowGraph: { + if (stateType === null) { + return false + } + + if (stateType.unit === Types.UnitUnixTime) { + return false; + } + + switch (stateType.type) { + case "Int": + case "Double": + return true; + case "Bool": + return engine.jsonRpcClient.ensureServerVersion("1.10") + } + print("not showing graph for", root.stateType.type) + return false; + } + + Component.onCompleted: { + if (root.filterTypeIds.length === 0) { + return; + } + if (!canShowGraph) { + return; + } + + var source; + if (engine.jsonRpcClient.ensureServerVersion("1.10")) { + source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml"); + } else { + source = Qt.resolvedUrl("../customviews/GenericTypeGraphPre110.qml"); + } + setSource(source, {device: root.device, stateType: stateType}) + } + } + + + ListView { + anchors { left: parent.left; top: graphLoader.bottom; right: parent.right; bottom: parent.bottom } + clip: true + model: logsModelNg + ScrollBar.vertical: ScrollBar {} + + BusyIndicator { + anchors.centerIn: parent + visible: logsModelNg.busy + } + + delegate: ItemDelegate { + id: entryDelegate + width: parent.width + + property StateType stateType: model.source === LogEntry.LoggingSourceStates ? root.device.deviceClass.stateTypes.getStateType(model.typeId) : null + property EventType eventType: model.source === LogEntry.LoggingSourceEvents || model.source === LogEntry.LoggingSourceStates ? root.device.deviceClass.eventTypes.getEventType(model.typeId) : null + property ActionType actionType: model.source === LogEntry.LoggingSourceActions ? root.device.deviceClass.actionTypes.getActionType(model.typeId) : null + + contentItem: RowLayout { + ColorIcon { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: width + Layout.alignment: Qt.AlignVCenter + color: { + switch (model.source) { + case LogEntry.LoggingSourceStates: + case LogEntry.LoggingSourceSystem: + case LogEntry.LoggingSourceActions: + case LogEntry.LoggingSourceEvents: + return app.accentColor + case LogEntry.LoggingSourceRules: + if (model.loggingEventType === LogEntry.LoggingEventTypeActiveChange) { + return model.value === true ? "green" : keyColor + } + return app.accentColor + } + } + name: { + switch (model.source) { + case LogEntry.LoggingSourceStates: + return "../images/state.svg" + case LogEntry.LoggingSourceSystem: + return "../images/system-shutdown.svg" + case LogEntry.LoggingSourceActions: + return "../images/action.svg" + case LogEntry.LoggingSourceEvents: + return "../images/event.svg" + case LogEntry.LoggingSourceRules: + return "../images/magic.svg" + } + } + } + ColumnLayout { + RowLayout { + Label { + text: { + switch (model.source) { + case LogEntry.LoggingSourceStates: +// return entryDelegate.stateType.displayName + case LogEntry.LoggingSourceEvents: + return entryDelegate.eventType.displayName + case LogEntry.LoggingSourceActions: + return entryDelegate.actionType.displayName + } + } + Layout.fillWidth: true + elide: Text.ElideRight + } + Label { + text: Qt.formatDateTime(model.timestamp,"dd.MM.yy hh:mm:ss") + elide: Text.ElideRight + font.pixelSize: app.smallFont + enabled: false + } + } + + RowLayout { + Loader { + id: valueLoader + Layout.fillWidth: true + sourceComponent: { + switch (model.source) { + case LogEntry.LoggingSourceStates: + switch (entryDelegate.stateType.type.toLowerCase()) { + case "bool": + return boolComponent; + case "color": + return colorComponent + default: + if (entryDelegate.stateType.unit == Types.UnitUnixTime) { + return dateTimeComponent + } + + return labelComponent + + } + case LogEntry.LoggingSourceActions: + + break; + case LogEntry.LoggingSourceEvents: + + break; + } + + return labelComponent + } + Binding { target: valueLoader.item; property: "value"; value: model.value } + Binding { + target: entryDelegate.stateType && valueLoader.item.hasOwnProperty("unitString") ? valueLoader.item : null; + property: "unitString" + value: entryDelegate.stateType ? entryDelegate.stateType.unitString : "" + } + } + } + } + } + } + } + + Component { + id: labelComponent + Label { + property var value + property string unitString + text: value + " " + unitString + font.pixelSize: app.smallFont + elide: Text.ElideRight + } + } + + Component { + id: dateTimeComponent + Label { + property var value + text: Qt.formatDateTime(new Date(value * 1000), Qt.DefaultLocaleShortDate) + } + } + + Component { + id: boolComponent + Item { + id: boolLed + property var value + Led { + implicitHeight: app.smallFont + on: boolLed.value === "true" + } + } + } + + Component { + id: colorComponent + Item { + property var value + implicitHeight: app.smallFont + Rectangle { + height: parent.height + width: height * 2 + color: parent.value + // radius: width / 2 + border.color: app.foregroundColor + border.width: 1 + } + } + } +} diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index c59821b1..7a142f68 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -6,8 +6,11 @@ import "../components" Page { id: root - property var device: null - readonly property var deviceClass: engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) + property Device device: null + readonly property DeviceClass deviceClass: engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) + + property bool showLogsButton: true + property bool showDetailsButton: true default property alias data: contentItem.data @@ -35,7 +38,13 @@ Page { Component.onCompleted: { thingMenu.addItem(menuEntryComponent.createObject(thingMenu, {text: qsTr("Magic"), iconSource: "../images/magic.svg", functionName: "openDeviceMagicPage"})) - thingMenu.addItem(menuEntryComponent.createObject(thingMenu, {text: qsTr("Thing details"), iconSource: "../images/info.svg", functionName: "openDeviceInfoPage"})) + if (root.showDetailsButton) { + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, {text: qsTr("Thing details"), iconSource: "../images/configure.svg", functionName: "openGenericDevicePage"})) + } + if (root.showLogsButton) { + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, {text: qsTr("Thing logs"), iconSource: "../images/logs.svg", functionName: "openDeviceLogPage"})) + } + if (engine.jsonRpcClient.ensureServerVersion(1.6)) { thingMenu.addItem(menuEntryComponent.createObject(thingMenu, { @@ -48,8 +57,8 @@ Page { function openDeviceMagicPage() { pageStack.push(Qt.resolvedUrl("../magic/DeviceRulesPage.qml"), {device: root.device}) } - function openDeviceInfoPage() { - pageStack.push(Qt.resolvedUrl("GenericDeviceStateDetailsPage.qml"), {device: root.device}) + function openGenericDevicePage() { + pageStack.push(Qt.resolvedUrl("GenericDevicePage.qml"), {device: root.device}) } function toggleFavorite() { if (favoritesProxy.count === 0) { @@ -58,6 +67,9 @@ Page { engine.tagsManager.untagDevice(root.device.id, "favorites") } } + function openDeviceLogPage() { + pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {device: root.device }); + } Component { id: menuEntryComponent diff --git a/nymea-app/ui/devicepages/GenericDevicePage.qml b/nymea-app/ui/devicepages/GenericDevicePage.qml index 6511cb5a..7bf37dba 100644 --- a/nymea-app/ui/devicepages/GenericDevicePage.qml +++ b/nymea-app/ui/devicepages/GenericDevicePage.qml @@ -1,134 +1,487 @@ import QtQuick 2.5 -import QtQuick.Controls 2.1 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtGraphicalEffects 1.0 import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" +import "../delegates" DevicePageBase { id: root + showDetailsButton: false - Flickable { + function executeAction(actionTypeId, params) { + return engine.deviceManager.executeAction(root.device.id, actionTypeId, params) + } + + ListView { id: flickable anchors.fill: parent - contentHeight: contentColumn.height - contentWidth: parent.width - clip: true - property var d: root.device - property var dc: root.deviceClass + section.property: "type" + section.delegate: ListSectionHeader { + text: { + switch (parseInt(section)) { + case DeviceModel.TypeStateType: + return qsTr("States") + case DeviceModel.TypeActionType: + return qsTr("Actions") + case DeviceModel.TypeEventType: + return qsTr("Events") + } + } + } - ColumnLayout { - id: contentColumn + model: DeviceModel { + device: root.device + } + delegate: SwipeDelegate { + id: delegate width: parent.width -// spacing: app.margins - Repeater { - id: interfaceViewsRepeater - property bool unhandledInterface: false + readonly property StateType stateType: model.type === DeviceModel.TypeStateType ? root.deviceClass.stateTypes.getStateType(model.id) : null + readonly property ActionType actionType: model.writable ? root.deviceClass.actionTypes.getActionType(model.id) : null + readonly property EventType eventType: model.type === DeviceModel.TypeEventType ? root.deviceClass.eventTypes.getEventType(model.id) : null -// model: deviceClass.interfaces - delegate: Loader { - id: stateViewLoader - Layout.fillWidth: true - source: { - var src = ""; - var options = [] - switch (modelData) { - case "weather": - src = "WeatherView.qml"; - break; - case "mediacontroller": - src = "MediaControllerView.qml" - break; - case "extendedvolumecontroller": - src = "ExtendedVolumeController.qml" - break; - case "temperaturesensor": - case "humiditysensor": - case "moisturesensor": - case "lightsensor": - case "conductivitysensor": - case "pressuresensor": - case "noisesensor": - case "co2sensor": - src = "SensorView.qml" - options.interfaceName = modelData; - break; - case "notifications": - src = "NotificationsView.qml" - break; - case "battery": - case "connectable": - // Handled by our base class - break; - case "sensor": - case "media": - // Ignore interfaces without any states/actions - break; - default: - print("WARNING: Unhandled interface", modelData) - interfaceViewsRepeater.unhandledInterface = true + Layout.fillWidth: true + topPadding: model.type === DeviceModel.TypeActionType ? app.margins / 2 : 0 + bottomPadding: 0 + contentItem: Loader { + id: inlineLoader + sourceComponent: { + switch (model.type) { + case DeviceModel.TypeStateType: + return stateComponent; + case DeviceModel.TypeActionType: + return actionComponent; + case DeviceModel.TypeEventType: + return eventComponent; + } + } + + Binding { + target: inlineLoader.item + when: model.type === DeviceModel.TypeStateType + property: "stateType" + value: delegate.stateType + } + Binding { + target: inlineLoader.item + when: model.type === DeviceModel.TypeActionType + property: "actionType" + value: delegate.actionType + } + Binding { + target: inlineLoader.item + when: model.type === DeviceModel.TypeEventType + property: "eventType" + value: delegate.eventType + } + } + + onClicked: pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {device: root.device, filterTypeIds: [model.id]}) + + swipe.right: RowLayout { + height: delegate.height + anchors.right: parent.right + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/logs.svg" + } + onClicked: { + pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {device: root.device, filterTypeIds: [model.id]}) + } + } + } + } + } + + Component { + id: stateComponent + + RowLayout { + id: stateDelegate + property StateType stateType: null + readonly property State deviceState: stateType ? root.device.states.getState(stateType.id) : null + readonly property bool writable: root.deviceClass.actionTypes.getActionType(stateType.id) !== null + + Label { + Layout.fillWidth: true + text: stateDelegate.stateType.displayName + elide: Text.ElideRight + } + Loader { + id: stateDelegateLoader + sourceComponent: { + switch (stateType.type.toLowerCase()) { + case "string": + if (stateDelegate.writable) { + if (stateDelegate.stateType.allowedValues !== undefined) { + return comboBoxComponent + } + return textFieldComponent; + } else { + return labelComponent; + } + case "stringlist": + return listComponent; + case "bool": + if (stateDelegate.writable) { + return switchComponent; + } else { + return ledComponent; + } + case "int": + case "double": + if (stateDelegate.stateType.unit === Types.UnitUnixTime) { + return dateTimeComponent; } - return Qt.resolvedUrl("../customviews/" + src); + if (stateDelegate.writable) { + return spinBoxComponent; + } + return numberLabelComponent; + case "color": + return colorComponent; + default: + print("Unhandled state type", stateType.displayName, stateType.type) } - Binding { - target: stateViewLoader.item ? stateViewLoader.item : null - property: "device" - value: device + + print("GenericDevicePage: unhandled entry", stateDelegate.stateType.displayName) + } + } + + Label { + visible: stateDelegate.stateType.unit !== Types.UnitUnixTime && stateDelegate.stateType.unitString.length > 0 + text: stateDelegate.stateType.unitString + } + + Binding { + target: stateDelegateLoader.item + property: "value" + value: root.device.states.getState(stateDelegate.stateType.id).value + } + Binding { + target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("possibleValues") ? stateDelegateLoader.item : null + property: "possibleValues" + value: stateDelegate.stateType.allowedValues + } + Binding { + target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("from") ? stateDelegateLoader.item : null + property: "from" + value: stateDelegate.stateType.minValue !== undefined ? stateDelegate.stateType.minValue : -999999999999 + } + Binding { + target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("to") ? stateDelegateLoader.item : null + property: "to" + value: stateDelegate.stateType.maxValue !== undefined ? stateDelegate.stateType.maxValue : 999999999999 + } + Binding { + target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("actionTypeId") ? stateDelegateLoader.item : null + property: "actionTypeId" + value: stateDelegate.stateType.id + } + Connections { + target: stateDelegateLoader.item && stateDelegateLoader.item.hasOwnProperty("changed") ? stateDelegateLoader.item : null + onChanged: { + var params = [] + var param1 = {} + param1["paramTypeId"] = stateDelegate.stateType.id + param1["value"] = value; + params.push(param1) + root.executeAction(stateDelegate.stateType.id, params); + } + } + } + } + + Component { + id: actionComponent + + RowLayout { + id: actionDelegate + + property ActionType actionType: null + property int pendingActionId: -1 + property bool lastSuccess: false + + Connections { + target: engine.deviceManager + onExecuteActionReply: { + if (params["id"] === actionDelegate.pendingActionId) { + print("blubb!") + pendingTimer.start(); + actionDelegate.lastSuccess = params["params"]["deviceError"] === "DeviceErrorNoError" + actionDelegate.pendingActionId = -1 } - Binding { - target: stateViewLoader.item ? stateViewLoader.item : null - property: "deviceClass" - value: deviceClass + } + } + Timer { id: pendingTimer; interval: 1000; repeat: false; running: false } + + Button { + text: actionType.displayName + Layout.fillWidth: true + + + onClicked: { + if (actionDelegate.actionType.paramTypes.count === 0) { + actionDelegate.pendingActionId = root.executeAction(actionDelegate.actionType.id, []) + } else { + var dialog = paramsDialogComponent.createObject(root, { actionType: actionDelegate.actionType }) + dialog.open() } - Binding { - target: stateViewLoader.item ? stateViewLoader.item : null - property: "interfaceName" - value: modelData + } + + Component { + id: paramsDialogComponent + Dialog { + id: paramsDialog + modal: true + width: parent.width - app.margins * 2 + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + padding: 0 + + property ActionType actionType: null + + contentItem: ColumnLayout { + Repeater { + id: paramsRepeater + model: paramsDialog.actionType.paramTypes + delegate: ParamDelegate { + Layout.fillWidth: true + paramType: paramsDialog.actionType.paramTypes.get(index) + } + } + RowLayout { + Layout.margins: app.margins + spacing: app.margins + Button { + text: qsTr("Cancel") + Layout.fillWidth: true + onClicked: paramsDialog.close() + } + Button { + text: qsTr("OK") + Layout.fillWidth: true + onClicked: { + var params = [] + for (var i = 0; i < paramsRepeater.count; i++) { + var param = {} + param["paramTypeId"] = paramsRepeater.itemAt(i).paramType.id + param["value"] = paramsRepeater.itemAt(i).value + params.push(param) + } + actionDelegate.pendingActionId = root.executeAction(paramsDialog.actionType.id, params); + paramsDialog.close(); + } + } + } + } } } } + Item { + Layout.preferredHeight: preferredSize + Layout.preferredWidth: preferredSize + property int preferredSize: actionDelegate.pendingActionId !== -1 || pendingTimer.running ? app.iconSize : 0 + Behavior on preferredSize { NumberAnimation { duration: 100 } } - Repeater { - model: interfaceViewsRepeater.count == 0 || interfaceViewsRepeater.unhandledInterface ? deviceClass.actionTypes : null - delegate: ItemDelegate { - Layout.fillWidth: true - Layout.preferredHeight: delegateLoader.height + BusyIndicator { + anchors.fill: parent + visible: actionDelegate.pendingActionId !== -1 + } - Loader { - id: delegateLoader - width: parent.width - property var actionType: deviceClass.actionTypes.get(index) - property var actionValue: device.hasState(actionType.id) ? device.states.getState(actionType.id).value : null - source: Qt.resolvedUrl("../delegates/ActionDelegate.qml") + ColorIcon { + anchors.fill: parent + visible: actionDelegate.pendingActionId === -1 + name: actionDelegate.lastSuccess ? "../images/tick.svg" : "../images/close.svg" + color: actionDelegate.lastSuccess ? "green" : "red" + } + } + } + } - Binding { - target: delegateLoader.item ? delegateLoader.item : null - property: "actionType" - value: delegateLoader.actionType - } - Binding { - target: delegateLoader.item ? delegateLoader.item : null - property: "actionState" - value: delegateLoader.actionValue + Component { + id: eventComponent + RowLayout { + id: eventComponentItem + property EventType eventType: null + + + Label { + Layout.fillWidth: true + text: eventComponentItem.eventType.displayName + } + Rectangle { + id: flashlight + Layout.preferredHeight: app.iconSize * .8 + Layout.preferredWidth: height + color: "lightgray" + radius: width / 2 + border.color: app.foregroundColor + border.width: 1 + + SequentialAnimation on color { + id: flashlightAnimation + running: false + ColorAnimation { to: "lightgreen"; duration: 100 } + ColorAnimation { to: "lightgray"; duration: 500 } + } + } + LogsModelNg { + engine: _engine + live: true + deviceId: root.device.id + typeIds: eventComponentItem.eventType.id + onCountChanged: { + flashlightAnimation.start() + } + } + } + } + + Component { + id: ledComponent + Led { + property bool value + on: value === true + } + } + + Component { + id: labelComponent + Label { + property var value + text: value + } + } + Component { + id: numberLabelComponent + Label { + property var value + text: Math.round(value * 100) / 100 + } + } + Component { + id: textFieldComponent + TextField { + property var value + text: value + } + } + + Component { + id: listComponent + Label { + property var value + text: value.join(", ") + } + } + + Component { + id: checkBoxComponent + CheckBox { + property var value + checked: value === true + enabled: false + } + } + + Component { + id: switchComponent + Switch { + property var value + signal changed(var value) + checked: value === true + onClicked: { + changed(checked) + } + } + } + + Component { + id: spinBoxComponent + SpinBox { + width: 150 + signal changed(var value) + stepSize: (to - from) / 10 + editable: true + onValueModified: { + changed(value) + } + } + } + + Component { + id: comboBoxComponent + ComboBox { + property var value + property var possibleValues + + signal changed(var value) + model: possibleValues + onActivated: changed(model[index]) + } + } + + Component { + id: colorComponent + Item { + id: colorComponentItem + implicitWidth: app.iconSize * 2 + implicitHeight: app.iconSize + property var value + signal changed(var value) + + Pane { + anchors.fill: parent + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + Material.elevation: 1 + contentItem: Rectangle { + color: colorComponentItem.value + + MouseArea { + anchors.fill: parent + onClicked: { + var pos = colorComponentItem.mapToItem(root, 0, colorComponentItem.height) + print("opening", colorComponentItem.value) + var colorPicker = colorPickerComponent.createObject(root, {preferredY: pos.y, colorValue: colorComponentItem.value }) + colorPicker.open() } + } + } + } - property int commandId: 0 - Connections { - target: delegateLoader.item ? delegateLoader.item : null - onExecuteAction: { - engine.deviceManager.executeAction(root.device.id, model.id, params) - } - } - Connections { - target: engine.jsonRpcClient - onResponseReceived: { - if (commandId == delegateLoader.commandId) { - print("response:", response["error"]) - } + Component { + id: colorPickerComponent + Dialog { + id: colorPickerDialog + modal: true + x: (parent.width - width) / 2 + y: Math.min(preferredY, parent.height - height) + width: parent.width - app.margins * 2 + height: 200 + padding: app.margins + property var colorValue + property int preferredY: 0 + contentItem: ColorPicker { + color: colorPickerDialog.colorValue + property var lastSentTime: new Date() + onColorChanged: { + var currentTime = new Date(); + if (pressed && currentTime - lastSentTime > 200) { + colorComponentItem.changed(color); } } } @@ -136,4 +489,12 @@ DevicePageBase { } } } + + Component { + id: dateTimeComponent + Label { + property var value + text: Qt.formatDateTime(new Date(value * 1000), Qt.DefaultLocaleShortDate) + } + } } diff --git a/nymea-app/ui/devicepages/GenericDeviceStateDetailsPage.qml b/nymea-app/ui/devicepages/GenericDeviceStateDetailsPage.qml deleted file mode 100644 index 7b90124c..00000000 --- a/nymea-app/ui/devicepages/GenericDeviceStateDetailsPage.qml +++ /dev/null @@ -1,169 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.1 -import Nymea 1.0 -import "../components" - -Page { - id: root - - property var device - readonly property var deviceClass: engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) - property bool readOnly: true - - header: GuhHeader { - text: "Details for " + root.device.name - onBackPressed: pageStack.pop() - } - Flickable { - anchors.fill: parent - contentHeight: statesColumn.height + app.margins*2 - - ColumnLayout { - id: statesColumn - anchors { left: parent.left; top: parent.top; right: parent.right; margins: app.margins } - spacing: app.margins - - Repeater { - model: deviceClass.stateTypes - delegate: RowLayout { - width: parent.width - height: app.largeFont - - property var stateType: deviceClass.stateTypes.get(index) - - Label { - id: stateLabel - Layout.preferredWidth: parent.width / 2 - text: displayName - elide: Text.ElideRight - } - - Loader { - id: placeHolder - Layout.fillWidth: true - sourceComponent: { - var writable = deviceClass.actionTypes.getActionType(id) !== null; - if (root.readOnly || !writable) { - return labelComponent; - } - - switch (stateType.type) { - case "Bool": - return boolComponent; - case "Int": - case "Double": - if (stateType.minValue !== undefined && stateType.maxValue !== undefined) { - return sliderComponent; - } - return textFieldComponent; - case "String": - return textFieldComponent; - case "Color": - return colorPreviewComponent; - } - console.warn("DeviceStateDetailsPage: Type delegate not implemented", stateType.type) - return null; - } - } - - ColorIcon { - Layout.fillHeight: true - Layout.preferredWidth: height - name: "../images/info.svg" - - MouseArea { - anchors.fill: parent - anchors.margins: -app.margins / 2 - onClicked: { - pageStack.push(Qt.resolvedUrl("StateLogPage.qml"), - {device: root.device, stateType: stateType}) - } - } - } - - Binding { - target: placeHolder.item - when: placeHolder.item - property: "value" - value: device.states.getState(id).value - } -// Binding { -// target: placeHolder.item -// when: placeHolder.item -// property: "enabled" -// value: deviceClass.actionTypes.getActionType(id) !== null -// } - Binding { - target: placeHolder.item - when: placeHolder.item - property: "stateTypeId" - value: id - } - - // Label { - // id: valueLable - // Layout.fillWidth: true - // text: device.states.getState(id).value + " " + deviceClass.stateTypes.getStateType(id).unitString - // } - } - } - } - } - - - Component { - id: labelComponent - Label { - property var value: "" - property var stateTypeId: null - property var unitString: deviceClass.stateTypes.getStateType(stateTypeId).unitString - text: unitString === "datetime" ? Qt.formatDateTime(new Date(value * 1000), Qt.DefaultLocaleShortDate) : value + " " + unitString - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - } - - Component { - id: textFieldComponent - TextField { - property var value: "" - property var stateTypeId: null - text: value - onEditingFinished: { - executeAction(stateTypeId, text) - } - } - } - - Component { - id: boolComponent - Switch { - property var value: false - property var stateTypeId: null - checked: value - onClicked: executeAction(stateTypeId, checked) - } - } - - Component { - id: colorPreviewComponent - Rectangle { - property var value: "blue" - property var stateTypeId: null - color: value - implicitHeight: app.mediumFont - implicitWidth: height - radius: height / 4 - } - } - - function executeAction(stateTypeId, value) { - var paramList = [] - var muteParam = {} - muteParam["paramTypeId"] = stateTypeId; - muteParam["value"] = value; - paramList.push(muteParam) - engine.deviceManager.executeAction(root.device.id, stateTypeId, paramList) - } -}