From 37397e2c659d8f9139bc19e288236f46ad2c0acf Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 29 Nov 2019 17:44:05 +0100 Subject: [PATCH] initial stab on grouping things --- libnymea-app-core/devicesproxy.cpp | 50 ++- libnymea-app-core/devicesproxy.h | 12 + libnymea-app-core/libnymea-app-core.h | 2 + libnymea-app-core/libnymea-app-core.pro | 2 + libnymea-app-core/models/interfacesproxy.cpp | 23 +- libnymea-app-core/models/interfacesproxy.h | 10 + libnymea-app-core/models/taglistmodel.cpp | 100 ++++++ libnymea-app-core/models/taglistmodel.h | 44 +++ libnymea-app-core/models/tagsproxymodel.cpp | 9 +- libnymea-app-core/models/tagsproxymodel.h | 1 + libnymea-app-core/tagsmanager.cpp | 38 +- libnymea-app-core/tagsmanager.h | 9 +- libnymea-common/types/interfaces.cpp | 5 + libnymea-common/types/tag.cpp | 8 +- libnymea-common/types/tag.h | 17 +- libnymea-common/types/tags.cpp | 15 +- libnymea-common/types/tags.h | 3 +- nymea-app/images.qrc | 1 + nymea-app/resources.qrc | 2 + nymea-app/ui/MainPage.qml | 11 + nymea-app/ui/components/ColorIcon.qml | 4 +- nymea-app/ui/devicepages/DevicePageBase.qml | 72 ++++ nymea-app/ui/grouping/GroupPage.qml | 38 ++ nymea-app/ui/images/view-grid-symbolic.svg | 179 ++++++++++ nymea-app/ui/mainviews/GroupsView.qml | 354 +++++++++++++++++++ 25 files changed, 971 insertions(+), 38 deletions(-) create mode 100644 libnymea-app-core/models/taglistmodel.cpp create mode 100644 libnymea-app-core/models/taglistmodel.h create mode 100644 nymea-app/ui/grouping/GroupPage.qml create mode 100644 nymea-app/ui/images/view-grid-symbolic.svg create mode 100644 nymea-app/ui/mainviews/GroupsView.qml diff --git a/libnymea-app-core/devicesproxy.cpp b/libnymea-app-core/devicesproxy.cpp index 6e7d0761..6fb2aa6c 100644 --- a/libnymea-app-core/devicesproxy.cpp +++ b/libnymea-app-core/devicesproxy.cpp @@ -23,6 +23,7 @@ #include "devicesproxy.h" #include "engine.h" #include "tagsmanager.h" +#include "types/tag.h" DevicesProxy::DevicesProxy(QObject *parent) : QSortFilterProxyModel(parent) @@ -93,7 +94,7 @@ QString DevicesProxy::filterTagId() const void DevicesProxy::setFilterTagId(const QString &filterTag) { - if (m_filterTagId != filterTagId()) { + if (m_filterTagId != filterTag) { m_filterTagId = filterTag; emit filterTagIdChanged(); invalidateFilter(); @@ -101,6 +102,21 @@ void DevicesProxy::setFilterTagId(const QString &filterTag) } } +QString DevicesProxy::filterTagValue() const +{ + return m_filterTagValue; +} + +void DevicesProxy::setFilterTagValue(const QString &tagValue) +{ + if (m_filterTagValue != tagValue) { + m_filterTagValue = tagValue; + emit filterTagValueChanged(); + invalidateFilter(); + emit countChanged(); + } +} + QString DevicesProxy::filterDeviceClassId() const { return m_filterDeviceClassId; @@ -112,6 +128,22 @@ void DevicesProxy::setFilterDeviceClassId(const QString &filterDeviceClassId) m_filterDeviceClassId = filterDeviceClassId; emit filterDeviceClassIdChanged(); invalidateFilter(); + emit countChanged(); + } +} + +QString DevicesProxy::filterDeviceId() const +{ + return m_filterDeviceId; +} + +void DevicesProxy::setFilterDeviceId(const QString &filterDeviceId) +{ + if (m_filterDeviceId != filterDeviceId) { + m_filterDeviceId = filterDeviceId; + emit filterDeviceIdChanged(); + invalidateFilter(); + emit countChanged(); } } @@ -156,7 +188,7 @@ void DevicesProxy::setNameFilter(const QString &nameFilter) m_nameFilter = nameFilter; emit nameFilterChanged(); invalidateFilter(); - countChanged(); + emit countChanged(); } } @@ -201,6 +233,7 @@ void DevicesProxy::setGroupByInterface(bool groupByInterface) m_groupByInterface = groupByInterface; emit groupByInterfaceChanged(); invalidate(); + emit countChanged(); } } @@ -254,12 +287,21 @@ bool DevicesProxy::filterAcceptsRow(int source_row, const QModelIndex &source_pa { Device *device = getInternal(source_row); if (!m_filterTagId.isEmpty()) { - if (!m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId)) { + Tag *tag = m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId); + if (!tag) { + return false; + } + if (!m_filterTagValue.isEmpty() && tag->value() != m_filterTagValue) { return false; } } if (!m_filterDeviceClassId.isEmpty()) { - if (device->deviceClassId() != m_filterDeviceClassId) { + if (device->deviceClassId() != QUuid(m_filterDeviceClassId)) { + return false; + } + } + if (!m_filterDeviceId.isEmpty()) { + if (device->id() != QUuid(m_filterDeviceId)) { return false; } } diff --git a/libnymea-app-core/devicesproxy.h b/libnymea-app-core/devicesproxy.h index b57ba33b..673303ce 100644 --- a/libnymea-app-core/devicesproxy.h +++ b/libnymea-app-core/devicesproxy.h @@ -38,7 +38,9 @@ class DevicesProxy : public QSortFilterProxyModel Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) Q_PROPERTY(DevicesProxy *parentProxy READ parentProxy WRITE setParentProxy NOTIFY parentProxyChanged) Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged) + Q_PROPERTY(QString filterTagValue READ filterTagValue WRITE setFilterTagValue NOTIFY filterTagValueChanged) Q_PROPERTY(QString filterDeviceClassId READ filterDeviceClassId WRITE setFilterDeviceClassId NOTIFY filterDeviceClassIdChanged) + Q_PROPERTY(QString filterDeviceId READ filterDeviceId WRITE setFilterDeviceId NOTIFY filterDeviceIdChanged) Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) Q_PROPERTY(QStringList hiddenInterfaces READ hiddenInterfaces WRITE setHiddenInterfaces NOTIFY hiddenInterfacesChanged) Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged) @@ -63,9 +65,15 @@ public: QString filterTagId() const; void setFilterTagId(const QString &filterTag); + QString filterTagValue() const; + void setFilterTagValue(const QString &tagValue); + QString filterDeviceClassId() const; void setFilterDeviceClassId(const QString &filterDeviceClassId); + QString filterDeviceId() const; + void setFilterDeviceId(const QString &filterDeviceId); + QStringList shownInterfaces() const; void setShownInterfaces(const QStringList &shownInterfaces); @@ -91,7 +99,9 @@ signals: void engineChanged(); void parentProxyChanged(); void filterTagIdChanged(); + void filterTagValueChanged(); void filterDeviceClassIdChanged(); + void filterDeviceIdChanged(); void shownInterfacesChanged(); void hiddenInterfacesChanged(); void nameFilterChanged(); @@ -106,7 +116,9 @@ private: Engine *m_engine = nullptr; DevicesProxy *m_parentProxy = nullptr; QString m_filterTagId; + QString m_filterTagValue; QString m_filterDeviceClassId; + QString m_filterDeviceId; QStringList m_shownInterfaces; QStringList m_hiddenInterfaces; QString m_nameFilter; diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index 75f76421..de616f3b 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -48,6 +48,7 @@ #include "models/wirelessaccesspointsproxy.h" #include "tagsmanager.h" #include "models/tagsproxymodel.h" +#include "models/taglistmodel.h" #include "types/tag.h" #include "ruletemplates/ruletemplates.h" #include "ruletemplates/ruletemplate.h" @@ -189,6 +190,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "Tags", "Get it from TagsManager"); qmlRegisterUncreatableType(uri, 1, 0, "Tag", "Get it from Tags"); qmlRegisterType(uri, 1, 0, "TagsProxyModel"); + qmlRegisterType(uri, 1, 0, "TagListModel"); qmlRegisterType(uri, 1, 0, "NetworkManagerController"); qmlRegisterType(uri, 1, 0, "BluetoothDiscovery"); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index 9cfbcbd5..309dcbe8 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -47,6 +47,7 @@ SOURCES += \ deviceclassesproxy.cpp \ devicediscovery.cpp \ models/packagesfiltermodel.cpp \ + models/taglistmodel.cpp \ vendorsproxy.cpp \ pluginsproxy.cpp \ interfacesmodel.cpp \ @@ -109,6 +110,7 @@ HEADERS += \ deviceclassesproxy.h \ devicediscovery.h \ models/packagesfiltermodel.h \ + models/taglistmodel.h \ vendorsproxy.h \ pluginsproxy.h \ interfacesmodel.h \ diff --git a/libnymea-app-core/models/interfacesproxy.cpp b/libnymea-app-core/models/interfacesproxy.cpp index 708b17fc..a6e6109c 100644 --- a/libnymea-app-core/models/interfacesproxy.cpp +++ b/libnymea-app-core/models/interfacesproxy.cpp @@ -5,6 +5,7 @@ #include "types/device.h" #include "devices.h" +#include "devicesproxy.h" InterfacesProxy::InterfacesProxy(QObject *parent): QSortFilterProxyModel(parent) { @@ -23,6 +24,7 @@ void InterfacesProxy::setShowEvents(bool showEvents) m_showEvents = showEvents; emit showEventsChanged(); invalidateFilter(); + void countChanged(); } } @@ -37,6 +39,7 @@ void InterfacesProxy::setShowActions(bool showActions) m_showActions = showActions; emit showActionsChanged(); invalidateFilter(); + void countChanged(); } } @@ -51,6 +54,7 @@ void InterfacesProxy::setShowStates(bool showStates) m_showStates = showStates; emit showStatesChanged(); invalidateFilter(); + void countChanged(); } } @@ -58,7 +62,6 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source { Q_UNUSED(source_parent) QString interfaceName = m_interfaces->get(source_row)->name(); - qDebug() << "filterAcceptsRow" << interfaceName << m_shownInterfaces; if (!m_shownInterfaces.isEmpty()) { if (!m_shownInterfaces.contains(interfaceName)) { return false; @@ -83,6 +86,24 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source return false; } } + if (m_devicesProxyFilter != nullptr) { + // TODO: This could be improved *a lot* by caching interfaces in the devices model... + bool found = false; + for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) { + Device *d = m_devicesProxyFilter->get(i); + if (!d->deviceClass()) { + qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name(); + return false; + } + if (d->deviceClass()->interfaces().contains(interfaceName)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } Interface* iface = m_interfaces->get(source_row); if (m_showEvents) { diff --git a/libnymea-app-core/models/interfacesproxy.h b/libnymea-app-core/models/interfacesproxy.h index 2ae59058..9151cb2f 100644 --- a/libnymea-app-core/models/interfacesproxy.h +++ b/libnymea-app-core/models/interfacesproxy.h @@ -4,15 +4,18 @@ #include class Devices; +class DevicesProxy; class Interface; class Interfaces; class InterfacesProxy: public QSortFilterProxyModel { Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) Q_PROPERTY(Devices* devicesFilter READ devicesFilter WRITE setDevicesFilter NOTIFY devicesFilterChanged) + Q_PROPERTY(DevicesProxy* devicesProxyFilter READ devicesProxyFilter WRITE setDevicesProxyFilter NOTIFY devicesProxyFilterChanged) Q_PROPERTY(bool showEvents READ showEvents WRITE setShowEvents NOTIFY showEventsChanged) Q_PROPERTY(bool showActions READ showActions WRITE setShowActions NOTIFY showActionsChanged) Q_PROPERTY(bool showStates READ showStates WRITE setShowStates NOTIFY showStatesChanged) @@ -26,6 +29,9 @@ public: Devices* devicesFilter() const { return m_devicesFilter; } void setDevicesFilter(Devices *devices) { m_devicesFilter = devices; emit devicesFilterChanged(); invalidateFilter(); } + DevicesProxy* devicesProxyFilter() const { return m_devicesProxyFilter; } + void setDevicesProxyFilter(DevicesProxy *devicesProxy) { m_devicesProxyFilter = devicesProxy; emit devicesProxyFilterChanged(); invalidateFilter(); } + bool showEvents() const; void setShowEvents(bool showEvents); @@ -42,14 +48,18 @@ public: signals: void shownInterfacesChanged(); void devicesFilterChanged(); + void devicesProxyFilterChanged(); void showEventsChanged(); void showActionsChanged(); void showStatesChanged(); + void countChanged(); + private: Interfaces *m_interfaces = nullptr; QStringList m_shownInterfaces; Devices* m_devicesFilter = nullptr; + DevicesProxy* m_devicesProxyFilter = nullptr; bool m_showEvents = false; bool m_showActions = false; bool m_showStates = false; diff --git a/libnymea-app-core/models/taglistmodel.cpp b/libnymea-app-core/models/taglistmodel.cpp new file mode 100644 index 00000000..34f249a1 --- /dev/null +++ b/libnymea-app-core/models/taglistmodel.cpp @@ -0,0 +1,100 @@ +#include "taglistmodel.h" +#include "tagsproxymodel.h" +#include "types/tag.h" + +#include + +TagListModel::TagListModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +TagsProxyModel *TagListModel::tagsProxy() const +{ + return m_tagsProxy; +} + +void TagListModel::setTagsProxy(TagsProxyModel *tagsProxy) +{ + if (m_tagsProxy != tagsProxy) { + m_tagsProxy = tagsProxy; + emit tagsProxyChanged(); + + connect(tagsProxy, &TagsProxyModel::countChanged, this, &TagListModel::update); + + update(); + } +} + +int TagListModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant TagListModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleTagId: + return m_list.at(index.row())->tagId(); + case RoleValue: + return m_list.at(index.row())->value(); + } + + return QVariant(); +} + +QHash TagListModel::roleNames() const +{ + QHash roles; + roles.insert(RoleTagId, "tagId"); + roles.insert(RoleValue, "value"); + return roles; +} + +bool TagListModel::containsId(const QString &tagId) +{ + foreach (Tag* t, m_list) { + if (t->tagId() == tagId) { + return true; + } + } + return false; +} + +bool TagListModel::containsValue(const QString &tagValue) +{ + foreach (Tag* t, m_list) { + if (t->value() == tagValue) { + return true; + } + } + return false; +} + +void TagListModel::update() +{ + beginResetModel(); + qDeleteAll(m_list); + m_list.clear(); + + for (int i = 0; i < m_tagsProxy->rowCount(); i++) { + Tag *tag = m_tagsProxy->get(i); + + bool found = false; + foreach (Tag* existingTag, m_list) { + if (tag->tagId() == existingTag->tagId() && tag->value() == existingTag->value()) { + found = true; + break; + } + } + if (!found) { + Tag *t = new Tag(tag->tagId(), tag->value(), this); + m_list.append(t); + } + } + + qDebug() << "Model populated" << m_list.count() << this; + endResetModel(); + emit countChanged(); +} diff --git a/libnymea-app-core/models/taglistmodel.h b/libnymea-app-core/models/taglistmodel.h new file mode 100644 index 00000000..cd312b68 --- /dev/null +++ b/libnymea-app-core/models/taglistmodel.h @@ -0,0 +1,44 @@ +#ifndef TAGLISTMODEL_H +#define TAGLISTMODEL_H + +#include + +class TagsProxyModel; +class Tag; + +class TagListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(TagsProxyModel* tagsProxy READ tagsProxy WRITE setTagsProxy NOTIFY tagsProxyChanged) +public: + enum Roles { + RoleTagId, + RoleValue + }; + explicit TagListModel(QObject *parent = nullptr); + + TagsProxyModel* tagsProxy() const; + void setTagsProxy(TagsProxyModel* tagsProxy); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + Q_INVOKABLE bool containsId(const QString &tagId); + Q_INVOKABLE bool containsValue(const QString &tagValue); + +signals: + void countChanged(); + void tagsProxyChanged(); + +private slots: + void update(); + +private: + TagsProxyModel *m_tagsProxy = nullptr; + + QList m_list; +}; + +#endif // TAGLISTMODEL_H diff --git a/libnymea-app-core/models/tagsproxymodel.cpp b/libnymea-app-core/models/tagsproxymodel.cpp index c21df726..3c9f3118 100644 --- a/libnymea-app-core/models/tagsproxymodel.cpp +++ b/libnymea-app-core/models/tagsproxymodel.cpp @@ -94,17 +94,18 @@ bool TagsProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_ Q_UNUSED(source_parent) Tag *tag = m_tags->get(source_row); if (!m_filterTagId.isEmpty()) { - if (tag->tagId() != m_filterTagId) { + QRegExp exp(m_filterTagId); + if (!exp.exactMatch(tag->tagId())) { return false; } } if (!m_filterDeviceId.isEmpty()) { - if (tag->deviceId() != m_filterDeviceId) { + if (QUuid(tag->deviceId()) != QUuid(m_filterDeviceId)) { return false; } } if (!m_filterRuleId.isEmpty()) { - if (tag->ruleId() != m_filterRuleId) { + if (QUuid(tag->ruleId()) != QUuid(m_filterRuleId)) { return false; } } @@ -115,7 +116,7 @@ bool TagsProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex { QString leftValue = m_tags->get(source_left.row())->value(); QString rightValue = m_tags->get(source_right.row())->value(); - bool okLeft, okRight;; + bool okLeft, okRight; qlonglong leftAsNumber = leftValue.toLongLong(&okLeft); qlonglong rightAsNumber = rightValue.toLongLong(&okRight); if (okLeft && okRight) { diff --git a/libnymea-app-core/models/tagsproxymodel.h b/libnymea-app-core/models/tagsproxymodel.h index 4d1205f5..feb7f029 100644 --- a/libnymea-app-core/models/tagsproxymodel.h +++ b/libnymea-app-core/models/tagsproxymodel.h @@ -42,6 +42,7 @@ signals: void filterTagIdChanged(); void filterDeviceIdChanged(); void filterRuleIdChanged(); + void groupSameTagsChanged(); void countChanged(); private: diff --git a/libnymea-app-core/tagsmanager.cpp b/libnymea-app-core/tagsmanager.cpp index e859f824..0912937b 100644 --- a/libnymea-app-core/tagsmanager.cpp +++ b/libnymea-app-core/tagsmanager.cpp @@ -17,10 +17,17 @@ QString TagsManager::nameSpace() const void TagsManager::init() { + m_busy = true; + emit busyChanged(); m_tags->clear(); m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply"); } +bool TagsManager::busy() const +{ + return m_busy; +} + Tags *TagsManager::tags() const { return m_tags; @@ -83,13 +90,16 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) QString notification = params.value("notification").toString(); if (notification == "Tags.TagAdded") { - addTagInternal(tagMap); + Tag *tag = unpackTag(tagMap); + if (tag) { + m_tags->addTag(tag); + } } else if (notification == "Tags.TagRemoved") { for (int i = 0; i < m_tags->rowCount(); i++) { Tag* tag = m_tags->get(i); - if (tagMap.value("deviceId").toString() == tag->deviceId() && - tagMap.value("ruleId").toString() == tag->ruleId() && + if (tagMap.value("deviceId").toUuid() == tag->deviceId() && + tagMap.value("ruleId").toUuid() == tag->ruleId() && tagMap.value("tagId").toString() == tag->tagId()) { m_tags->removeTag(tag); return; @@ -99,8 +109,8 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) qDebug() << "tag value changed"; for (int i = 0; i < m_tags->rowCount(); i++) { Tag* tag = m_tags->get(i); - if (tagMap.value("deviceId").toString() == tag->deviceId() && - tagMap.value("ruleId").toString() == tag->ruleId() && + if (tagMap.value("deviceId").toUuid() == tag->deviceId() && + tagMap.value("ruleId").toUuid() == tag->ruleId() && tagMap.value("tagId").toString() == tag->tagId()) { qDebug() << "Found tag"; tag->setValue(tagMap.value("value").toString()); @@ -111,10 +121,18 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) void TagsManager::getTagsReply(const QVariantMap ¶ms) { + QList tags; foreach (const QVariant &tagVariant, params.value("params").toMap().value("tags").toList()) { - addTagInternal(tagVariant.toMap()); + qDebug() << "aDDING TAG"; + Tag *tag = unpackTag(tagVariant.toMap()); + if (tag) { + tags.append(tag); + } } - emit tagsChanged(); + m_tags->addTags(tags); + + m_busy = false; + emit busyChanged(); } void TagsManager::addTagReply(const QVariantMap ¶ms) @@ -127,7 +145,7 @@ void TagsManager::removeTagReply(const QVariantMap ¶ms) qDebug() << "RemoveTag reply" << params; } -void TagsManager::addTagInternal(const QVariantMap &tagMap) +Tag* TagsManager::unpackTag(const QVariantMap &tagMap) { QString deviceId = tagMap.value("deviceId").toString(); QString ruleId = tagMap.value("ruleId").toString(); @@ -143,8 +161,8 @@ void TagsManager::addTagInternal(const QVariantMap &tagMap) } else { qWarning() << "Invalid tag. Neither deviceId nor ruleId are set. Skipping..."; tag->deleteLater(); - return; + return nullptr; } // qDebug() << "adding tag" << tag->tagId() << tag->value(); - m_tags->addTag(tag); + return tag; } diff --git a/libnymea-app-core/tagsmanager.h b/libnymea-app-core/tagsmanager.h index fefef407..28b7cdcd 100644 --- a/libnymea-app-core/tagsmanager.h +++ b/libnymea-app-core/tagsmanager.h @@ -9,13 +9,15 @@ class TagsManager : public JsonHandler { Q_OBJECT - Q_PROPERTY(Tags* tags READ tags NOTIFY tagsChanged) + Q_PROPERTY(Tags* tags READ tags CONSTANT) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); QString nameSpace() const override; void init(); + bool busy() const; Tags* tags() const; @@ -25,7 +27,7 @@ public: Q_INVOKABLE void untagRule(const QString &ruleId, const QString &tagId); signals: - void tagsChanged(); + void busyChanged(); private slots: void handleTagsNotification(const QVariantMap ¶ms); @@ -34,11 +36,12 @@ private slots: void removeTagReply(const QVariantMap ¶ms); private: - void addTagInternal(const QVariantMap &tagMap); + Tag *unpackTag(const QVariantMap &tagMap); JsonRpcClient *m_jsonClient = nullptr; Tags *m_tags = nullptr; + bool m_busy = false; }; #endif // TAGSMANAGER_H diff --git a/libnymea-common/types/interfaces.cpp b/libnymea-common/types/interfaces.cpp index 9916fc0c..d0d3f41a 100644 --- a/libnymea-common/types/interfaces.cpp +++ b/libnymea-common/types/interfaces.cpp @@ -89,6 +89,11 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent) tr("Daylight"), tr("Daylight changed")); + addInterface("lightsensor", tr("Light intensity sensors")); + addStateType("lightsensor", "lightIntensity", QVariant::Bool, false, + tr("Light intensity"), + tr("Light intensity changed")); + addInterface("evcharger", tr("EV charger")); addStateType("evcharger", "power", QVariant::Bool, true, tr("Charging"), diff --git a/libnymea-common/types/tag.cpp b/libnymea-common/types/tag.cpp index 3ea5e823..9d4750b5 100644 --- a/libnymea-common/types/tag.cpp +++ b/libnymea-common/types/tag.cpp @@ -10,22 +10,22 @@ Tag::Tag(const QString &tagId, const QString &value, QObject *parent): } -QString Tag::deviceId() const +QUuid Tag::deviceId() const { return m_deviceId; } -void Tag::setDeviceId(const QString &deviceId) +void Tag::setDeviceId(const QUuid &deviceId) { m_deviceId = deviceId; } -QString Tag::ruleId() const +QUuid Tag::ruleId() const { return m_ruleId; } -void Tag::setRuleId(const QString &ruleId) +void Tag::setRuleId(const QUuid &ruleId) { m_ruleId = ruleId; } diff --git a/libnymea-common/types/tag.h b/libnymea-common/types/tag.h index e0be291b..eb57579b 100644 --- a/libnymea-common/types/tag.h +++ b/libnymea-common/types/tag.h @@ -2,23 +2,24 @@ #define TAG_H #include +#include class Tag : public QObject { Q_OBJECT - Q_PROPERTY(QString deviceId READ deviceId CONSTANT) - Q_PROPERTY(QString ruleId READ ruleId CONSTANT) + Q_PROPERTY(QUuid deviceId READ deviceId CONSTANT) + Q_PROPERTY(QUuid ruleId READ ruleId CONSTANT) Q_PROPERTY(QString tagId READ tagId CONSTANT) Q_PROPERTY(QString value READ value NOTIFY valueChanged) public: explicit Tag(const QString &tagId, const QString &value, QObject *parent = nullptr); - QString deviceId() const; - void setDeviceId(const QString &deviceId); + QUuid deviceId() const; + void setDeviceId(const QUuid &deviceId); - QString ruleId() const; - void setRuleId(const QString &ruleId); + QUuid ruleId() const; + void setRuleId(const QUuid &ruleId); QString tagId() const; @@ -29,8 +30,8 @@ signals: void valueChanged(); private: - QString m_deviceId; - QString m_ruleId; + QUuid m_deviceId; + QUuid m_ruleId; QString m_tagId; QString m_value; }; diff --git a/libnymea-common/types/tags.cpp b/libnymea-common/types/tags.cpp index 58ef1cce..9d861dea 100644 --- a/libnymea-common/types/tags.cpp +++ b/libnymea-common/types/tags.cpp @@ -46,6 +46,19 @@ void Tags::addTag(Tag *tag) beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); m_list.append(tag); endInsertRows(); + qDebug() << "tags count changed"; + emit countChanged(); +} + +void Tags::addTags(QList tags) +{ + beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + tags.count() - 1); + foreach (Tag *tag, tags) { + tag->setParent(this); + connect(tag, &Tag::valueChanged, this, &Tags::tagValueChanged); + } + m_list.append(tags); + endInsertRows(); emit countChanged(); } @@ -68,7 +81,7 @@ Tag *Tags::get(int index) const return m_list.at(index); } -Tag *Tags::findDeviceTag(const QString &deviceId, const QString &tagId) const +Tag *Tags::findDeviceTag(const QUuid &deviceId, const QString &tagId) const { foreach (Tag *tag, m_list) { if (tag->deviceId() == deviceId && tag->tagId() == tagId) { diff --git a/libnymea-common/types/tags.h b/libnymea-common/types/tags.h index 00373b69..9e624be8 100644 --- a/libnymea-common/types/tags.h +++ b/libnymea-common/types/tags.h @@ -25,11 +25,12 @@ public: QHash roleNames() const override; void addTag(Tag *tag); + void addTags(QList tags); void removeTag(Tag *tag); Tag* get(int index) const; - Q_INVOKABLE Tag* findDeviceTag(const QString &deviceId, const QString &tagId) const; + Q_INVOKABLE Tag* findDeviceTag(const QUuid &deviceId, const QString &tagId) const; Q_INVOKABLE Tag* findRuleTag(const QString &ruleId, const QString &tagId) const; void clear(); diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 0d413d49..85fe2d1f 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -212,5 +212,6 @@ ui/images/browser/MediaBrowserIconNapster.svg ui/images/browser/MediaBrowserIconSoundCloud.svg ui/images/browser/MediaBrowserIconDeezer.svg + ui/images/view-grid-symbolic.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 7a3e5967..8f386481 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -197,5 +197,7 @@ ui/magic/NewMagicPage.qml ui/components/WebViewWrapper.qml ui/magic/NewScenePage.qml + ui/mainviews/GroupsView.qml + ui/grouping/GroupPage.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 4eadf7cf..499ce009 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -79,6 +79,10 @@ Page { } tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++}) tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++}) + if (engine.jsonRpcClient.ensureServerVersion(1.6)) { + tabEntryComponent.createObject(tabBar, {text: qsTr("Groups"), iconSource: "../images/view-grid-symbolic.svg", pageIndex: pi++}) + } + root.tabsReady = true } @@ -292,6 +296,13 @@ Page { } } } + + GroupsView { + id: groupsView + property string title: qsTr("My groups" + count); + width: swipeView.width + height: swipeView.height + } } ColumnLayout { diff --git a/nymea-app/ui/components/ColorIcon.qml b/nymea-app/ui/components/ColorIcon.qml index 16e61690..40cccf73 100644 --- a/nymea-app/ui/components/ColorIcon.qml +++ b/nymea-app/ui/components/ColorIcon.qml @@ -86,7 +86,7 @@ Item { Component.onCompleted: ready = true anchors.fill: parent - anchors.margins: parent.margins + anchors.margins: parent ? parent.margins : 0 source: ready && width > 0 && height > 0 && icon.name ? icon.name : "" sourceSize { width: width @@ -100,7 +100,7 @@ Item { id: colorizedImage anchors.fill: parent - anchors.margins: parent.margins + anchors.margins: parent ? parent.margins : 0 visible: active && image.status == Image.Ready // Whether or not a color has been set. diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 016c8360..f9eda37f 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -70,6 +70,13 @@ Page { iconSource: Qt.binding(function() { return favoritesProxy.count === 0 ? "../images/starred.svg" : "../images/non-starred.svg"}), functionName: "toggleFavorite" })) + + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, + { + text: qsTr("Grouping"), + iconSource: "../images/view-grid-symbolic.svg", + functionName: "addToGroup" + })) } } function openDeviceMagicPage() { @@ -85,6 +92,12 @@ Page { engine.tagsManager.untagDevice(root.device.id, "favorites") } } + function addToGroup() { +// engine.tagsManager.tagDevice(root.device.id, "group", "My group 1") + var dialog = addToGroupDialog.createObject(root) + dialog.open(); + } + function openThingSettingsPage() { pageStack.push(Qt.resolvedUrl("../thingconfiguration/ConfigureThingPage.qml"), {device: root.device}) } @@ -101,6 +114,65 @@ Page { onTriggered: thingMenu[functionName]() } } + + Component { + id: addToGroupDialog + MeaDialog { + title: qsTr("Configure groups") + headerIcon: "../images/view-grid-symbolic.svg" + + RowLayout { + TextField { + id: newGroupdTextField + Layout.fillWidth: true + placeholderText: qsTr("New group") + } + Button { + text: qsTr("OK") + enabled: newGroupdTextField.displayText.length > 0 && !groupTags.containsId("group-" + newGroupdTextField.displayText) + onClicked: { + engine.tagsManager.tagDevice(root.device.id, "group-" + newGroupdTextField.text, 1000) + newGroupdTextField.text = "" + } + } + } + + + ListView { + Layout.fillWidth: true + height: 200 + + model: TagListModel { + id: groupTags + tagsProxy: TagsProxyModel { + tags: engine.tagsManager.tags + filterTagId: "group-.*" + } + } + + delegate: CheckDelegate { + width: parent.width + text: model.tagId.substring(6) + checked: innerProxy.count > 0 + onClicked: { + if (innerProxy.count == 0) { + engine.tagsManager.tagDevice(root.device.id, model.tagId, 1000) + } else { + engine.tagsManager.untagDevice(root.device.id, model.tagId, model.value) + } + } + + DevicesProxy { + id: innerProxy + engine: _engine + filterTagId: model.tagId + filterDeviceId: root.device.id + } + } + } + } + + } } Rectangle { diff --git a/nymea-app/ui/grouping/GroupPage.qml b/nymea-app/ui/grouping/GroupPage.qml new file mode 100644 index 00000000..d1053f0b --- /dev/null +++ b/nymea-app/ui/grouping/GroupPage.qml @@ -0,0 +1,38 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +Page { + id: root + header: NymeaHeader { + text: root.groupTag.substring(6) + onBackPressed: pageStack.pop() + } + + property string groupTag + + + DevicesProxy { + id: devicesInGroup + engine: _engine + filterTagId: root.groupTag + } + + InterfacesProxy { + id: interfacesInGroup + devicesProxyFilter: devicesInGroup + showStates: true + } + + ListView { + anchors.fill: parent + model: devicesInGroup + delegate: Label { + text: model.name + } + } + +} diff --git a/nymea-app/ui/images/view-grid-symbolic.svg b/nymea-app/ui/images/view-grid-symbolic.svg new file mode 100644 index 00000000..bc246a32 --- /dev/null +++ b/nymea-app/ui/images/view-grid-symbolic.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml new file mode 100644 index 00000000..a2762a0e --- /dev/null +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -0,0 +1,354 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import Nymea 1.0 +import QtQuick.Controls.Material 2.2 +import "../components" + +Item { + id: root + + readonly property int count: groupsGridView.count + + GridView { + id: groupsGridView + anchors.fill: parent + anchors.margins: app.margins / 2 + + readonly property int minTileWidth: 180 + readonly property int minTileHeight: 180 + readonly property int tilesPerRow: root.width / minTileWidth + + model: TagListModel { + tagsProxy: TagsProxyModel { + tags: engine.tagsManager.tags + filterTagId: "group-.*" + } + } + cellWidth: width / tilesPerRow + cellHeight: cellWidth + + delegate: Item { + id: groupDelegate + width: groupsGridView.cellWidth + height: groupsGridView.cellHeight + + Pane { + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 2 + padding: 0 + + DevicesProxy { + id: devicesInGroup + engine: _engine + filterTagId: model.tagId + filterTagValue: model.value + } + + InterfacesProxy { + id: controlsInGroup + shownInterfaces: ["light", "simpleclosable"] + devicesProxyFilter: devicesInGroup + showStates: true + showActions: true + } + InterfacesProxy { + id: sensorsInGroup + shownInterfaces: ["temperaturesensor", "lightsensor", "presencesensor"] + devicesProxyFilter: devicesInGroup + showStates: true + } + + contentItem: ItemDelegate { + padding: 0 + + onClicked: { + pageStack.push(Qt.resolvedUrl("../grouping/GroupPage.qml"), {groupTag: model.tagId}) + } + + contentItem: ColumnLayout { + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 30 + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .05) + Label { + anchors.fill: parent + anchors { leftMargin: app.margins; rightMargin: app.margins } + text: model.tagId.substring(6) + elide: Text.ElideRight + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + + Repeater { + model: controlsInGroup + delegate: Loader { + id: controlLoader + Layout.fillWidth: true + Layout.leftMargin: app.margins / 2 + Layout.rightMargin: app.margins / 2 + sourceComponent: { + switch (model.name) { + case "simpleclosable": + return closableDelegate + case "light": + return lightDelegate + } + } + Binding { + target: controlLoader.item + property: "devices" + value: devicesInGroup + } + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize * 1.2 + Layout.alignment: Qt.AlignRight + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, 0.05) + + RowLayout { + anchors.fill: parent + + Repeater { + model: sensorsInGroup + delegate: Row { + ColorIcon { + height: app.iconSize * .8 + width: height + name: app.interfaceToIcon(model.name) + color: app.interfaceToColor(model.name) + } + DevicesProxy { + id: innerProxy + engine: _engine + parentProxy: devicesInGroup + shownInterfaces: [model.name] + } + + Led { + visible: ["presencesensor"].indexOf(model.name) >= 0 + state: { + var stateName = null + switch (model.name) { + case "presencesensor": + stateName = "isPresent" + break; + } + if (!stateName) { + return "off"; + } + var ret = false; + for (var i = 0; i < innerProxy.count; i++) { + ret |= innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value + } + return ret ? "on" : "off"; + } + } + + Label { + text: { + var stateName = null; + switch (model.name) { + case "temperaturesensor": + stateName = "temperature"; + break; + case "lightsensor": + stateName = "lightIntensity" + break; + } + if (!stateName) { + return ""; + } + + var ret = 0 + for (var i = 0; i < innerProxy.count; i++) { + ret += innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value + } + return (ret / innerProxy.count).toFixed(1) + } + } + } + } + } + } + } + } + } + } + } + } + } + + Component { + id: lightDelegate + RowLayout { + property var devices + + DevicesProxy { + id: lights + engine: _engine + parentProxy: devices + shownInterfaces: ["light"] + } + + DevicesProxy { + id: dimmableLights + engine: _engine + parentProxy: devices + shownInterfaces: ["dimmablelight"] + } + + Label { + text: qsTr("Lighting") + Layout.fillWidth: true + Layout.preferredHeight: slider.height + verticalAlignment: Text.AlignVCenter + visible: dimmableLights.count == 0 + } + + Slider { + id: slider + from: 0 + to: 100 + visible: dimmableLights.count > 0 + value: { + var median = 0 + var count = 0; + for (var i = 0; i < dimmableLights.count; i++) { + var device = dimmableLights.get(i); + var brightnessId = device.deviceClass.stateTypes.findByName("brightness").id + median += device.states.getState(brightnessId).value + count++ + } + return median / count; + } + + Layout.fillWidth: true + onPressedChanged: { + for (var i = 0; i < dimmableLights.count; i++) { + var device = dimmableLights.get(i); + var brightnessId = device.deviceClass.actionTypes.findByName("brightness").id + engine.deviceManager.executeAction(device.id, brightnessId, [{paramTypeId: brightnessId, value: value}]); + } + } + } + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: isOn ? "../images/light-on.svg" : "../images/light-off.svg" + color: isOn ? app.accentColor : keyColor + + property bool isOn: { + for (var i = 0; i < lights.count; i++) { + var device = lights.get(i) + var powerId = device.deviceClass.stateTypes.findByName("power").id + if (device.states.getState(powerId).value === true) { + return true + } + } + return false; + } + + MouseArea { + anchors.fill: parent + onClicked: { + for (var i = 0; i < lights.count; i++) { + var device = lights.get(i) + var powerId = device.deviceClass.stateTypes.findByName("power").id + engine.deviceManager.executeAction(device.id, powerId, [{paramTypeId: powerId, value: !parent.isOn}]) + } + } + } + } + } + } + + Component { + id: closableDelegate + + RowLayout { + + property var devices: null + DevicesProxy { + id: simpleClosables + engine: _engine + parentProxy: devices + shownInterfaces: ["simpleclosable"] + } + DevicesProxy { + id: closables + engine: _engine + parentProxy: devices + shownInterfaces: ["closable"] + } + + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + + ColorIcon { + height: parent.height + width: height + anchors.centerIn: parent + name: Qt.resolvedUrl("../images/up.svg") + } + onClicked: { + for (var i = 0; i < simpleClosables.count; i++) { + var device = simpleClosables.get(i) + var openId = device.deviceClass.actionTypes.findByName("open").id + engine.deviceManager.executeAction(device.id, openId) + } + } + } + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + visible: closables.count > 0 + + ColorIcon { + height: parent.height + width: height + anchors.centerIn: parent + name: Qt.resolvedUrl("../images/media-playback-stop.svg") + } + onClicked: { + for (var i = 0; i < closables.count; i++) { + var device = closables.get(i) + var openId = device.deviceClass.actionTypes.findByName("stop").id + engine.deviceManager.executeAction(device.id, openId) + } + } + } + ItemDelegate { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + + ColorIcon { + height: parent.height + width: height + anchors.centerIn: parent + name: Qt.resolvedUrl("../images/down.svg") + } + onClicked: { + for (var i = 0; i < simpleClosables.count; i++) { + var device = simpleClosables.get(i) + var closeId = device.deviceClass.actionTypes.findByName("close").id + engine.deviceManager.executeAction(device.id, closeId) + } + } + } + } + } +}