diff --git a/libnymea-app-core/devicesproxy.cpp b/libnymea-app-core/devicesproxy.cpp index cf1bfe20..3ae851ed 100644 --- a/libnymea-app-core/devicesproxy.cpp +++ b/libnymea-app-core/devicesproxy.cpp @@ -22,11 +22,12 @@ #include "devicesproxy.h" #include "engine.h" +#include "tagsmanager.h" DevicesProxy::DevicesProxy(QObject *parent) : QSortFilterProxyModel(parent) { - + connect(Engine::instance()->tagsManager()->tags(), &Tags::countChanged, this, &DevicesProxy::invalidateFilter); } Devices *DevicesProxy::devices() const @@ -47,31 +48,46 @@ void DevicesProxy::setDevices(Devices *devices) } } -DeviceClass::BasicTag DevicesProxy::filterTag() const +QString DevicesProxy::filterTagId() const { - return m_filterTag; + return m_filterTagId; } -void DevicesProxy::setFilterTag(DeviceClass::BasicTag filterTag) +void DevicesProxy::setFilterTagId(const QString &filterTag) { - if (m_filterTag != filterTag) { - m_filterTag = filterTag; - emit filterTagChanged(); + if (m_filterTagId != filterTagId()) { + m_filterTagId = filterTag; + emit filterTagIdChanged(); invalidateFilter(); emit countChanged(); } } -QString DevicesProxy::filterInterface() const +QStringList DevicesProxy::shownInterfaces() const { - return m_filterInterface; + return m_shownInterfaces; } -void DevicesProxy::setFilterInterface(const QString &filterInterface) +void DevicesProxy::setShownInterfaces(const QStringList &shownInterfaces) { - if (m_filterInterface != filterInterface) { - m_filterInterface = filterInterface; - emit filterInterfaceChanged(); + if (m_shownInterfaces != shownInterfaces) { + m_shownInterfaces = shownInterfaces; + emit shownInterfacesChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +QStringList DevicesProxy::hiddenInterfaces() const +{ + return m_hiddenInterfaces; +} + +void DevicesProxy::setHiddenInterfaces(const QStringList &hiddenInterfaces) +{ + if (m_hiddenInterfaces != hiddenInterfaces) { + m_hiddenInterfaces = hiddenInterfaces; + emit hiddenInterfacesChanged(); invalidateFilter(); emit countChanged(); } @@ -92,17 +108,33 @@ bool DevicesProxy::lessThan(const QModelIndex &left, const QModelIndex &right) c bool DevicesProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { - if (m_filterTag != DeviceClass::BasicTagNone) { - QList tags = Engine::instance()->deviceManager()->deviceClasses()->getDeviceClass(m_devices->get(source_row)->deviceClassId())->basicTags(); - if (!tags.contains(m_filterTag)) { + Device *device = m_devices->get(source_row); + if (!m_filterTagId.isEmpty()) { + if (!Engine::instance()->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId)) { return false; } } - if (!m_filterInterface.isEmpty()) { + if (!m_shownInterfaces.isEmpty()) { QStringList interfaces = Engine::instance()->deviceManager()->deviceClasses()->getDeviceClass(m_devices->get(source_row)->deviceClassId())->interfaces(); - if (!interfaces.contains(m_filterInterface)) { + bool foundMatch = false; + foreach (const QString &filterInterface, m_shownInterfaces) { + if (interfaces.contains(filterInterface)) { + foundMatch = true; + continue; + } + } + if (!foundMatch) { return false; } } + + if (!m_hiddenInterfaces.isEmpty()) { + QStringList interfaces = Engine::instance()->deviceManager()->deviceClasses()->getDeviceClass(m_devices->get(source_row)->deviceClassId())->interfaces(); + foreach (const QString &filterInterface, m_hiddenInterfaces) { + if (interfaces.contains(filterInterface)) { + return false; + } + } + } return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } diff --git a/libnymea-app-core/devicesproxy.h b/libnymea-app-core/devicesproxy.h index e4b9e1f3..fcd41c5c 100644 --- a/libnymea-app-core/devicesproxy.h +++ b/libnymea-app-core/devicesproxy.h @@ -34,8 +34,9 @@ class DevicesProxy : public QSortFilterProxyModel Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(Devices *devices READ devices WRITE setDevices NOTIFY devicesChanged) - Q_PROPERTY(DeviceClass::BasicTag filterTag READ filterTag WRITE setFilterTag NOTIFY filterTagChanged) - Q_PROPERTY(QString filterInterface READ filterInterface WRITE setFilterInterface NOTIFY filterInterfaceChanged) + Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged) + Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) + Q_PROPERTY(QStringList hiddenInterfaces READ hiddenInterfaces WRITE setHiddenInterfaces NOTIFY hiddenInterfacesChanged) public: explicit DevicesProxy(QObject *parent = 0); @@ -43,24 +44,29 @@ public: Devices *devices() const; void setDevices(Devices *devices); - DeviceClass::BasicTag filterTag() const; - void setFilterTag(DeviceClass::BasicTag filterTag); + QString filterTagId() const; + void setFilterTagId(const QString &filterTag); - QString filterInterface() const; - void setFilterInterface(const QString &filterInterface); + QStringList shownInterfaces() const; + void setShownInterfaces(const QStringList &shownInterfaces); + + QStringList hiddenInterfaces() const; + void setHiddenInterfaces(const QStringList &hiddenInterfaces); Q_INVOKABLE Device *get(int index) const; signals: void devicesChanged(); - void filterTagChanged(); - void filterInterfaceChanged(); + void filterTagIdChanged(); + void shownInterfacesChanged(); + void hiddenInterfacesChanged(); void countChanged(); private: Devices *m_devices = nullptr; - DeviceClass::BasicTag m_filterTag = DeviceClass::BasicTagNone; - QString m_filterInterface; + QString m_filterTagId; + QStringList m_shownInterfaces; + QStringList m_hiddenInterfaces; protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; diff --git a/libnymea-app-core/discovery/zeroconfdiscovery.cpp b/libnymea-app-core/discovery/zeroconfdiscovery.cpp index 841e345a..1b808e60 100644 --- a/libnymea-app-core/discovery/zeroconfdiscovery.cpp +++ b/libnymea-app-core/discovery/zeroconfdiscovery.cpp @@ -44,7 +44,7 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) if (!entry.name().startsWith("nymea") || entry.ip().isNull()) { return; } - qDebug() << "zeroconf service discovered" << entry << entry.txt() << entry.type(); +// qDebug() << "zeroconf service discovered" << entry << entry.txt() << entry.type(); QString uuid; bool sslEnabled = false; @@ -65,14 +65,14 @@ 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); if (!device) { device = new DiscoveryDevice(m_discoveryModel); device->setUuid(uuid); - qDebug() << "Adding new host to model"; +// qDebug() << "Adding new host to model"; m_discoveryModel->addDevice(device); } device->setHostAddress(entry.ip()); @@ -80,7 +80,7 @@ void ZeroconfDiscovery::serviceEntryAdded(const QZeroConfService &entry) device->setVersion(version); PortConfig *portConfig = device->portConfigs()->find(entry.port()); if (!portConfig) { - qDebug() << "Adding new port config"; +// qDebug() << "Adding new port config"; portConfig = new PortConfig(entry.port()); device->portConfigs()->insert(portConfig); } diff --git a/libnymea-app-core/engine.cpp b/libnymea-app-core/engine.cpp index 9ed4510d..4cb99f14 100644 --- a/libnymea-app-core/engine.cpp +++ b/libnymea-app-core/engine.cpp @@ -23,6 +23,7 @@ #include "tcpsocketinterface.h" #include "rulemanager.h" #include "logmanager.h" +#include "tagsmanager.h" #include "basicconfiguration.h" Engine* Engine::s_instance = 0; @@ -45,6 +46,11 @@ RuleManager *Engine::ruleManager() const return m_ruleManager; } +TagsManager *Engine::tagsManager() const +{ + return m_tagsManager; +} + JsonRpcClient *Engine::jsonRpcClient() const { return m_jsonRpcClient; @@ -77,11 +83,14 @@ Engine::Engine(QObject *parent) : m_deviceManager(new DeviceManager(m_jsonRpcClient, this)), m_ruleManager(new RuleManager(m_jsonRpcClient, this)), m_logManager(new LogManager(m_jsonRpcClient, this)), + m_tagsManager(new TagsManager(m_jsonRpcClient, this)), m_basicConfiguration(new BasicConfiguration(m_jsonRpcClient, this)), m_bluetoothDiscovery(new BluetoothDiscovery(this)) { connect(m_jsonRpcClient, &JsonRpcClient::connectedChanged, this, &Engine::onConnectedChanged); connect(m_jsonRpcClient, &JsonRpcClient::authenticationRequiredChanged, this, &Engine::onConnectedChanged); + + connect(m_deviceManager, &DeviceManager::fetchingDataChanged, this, &Engine::onDeviceManagerFetchingChanged); } void Engine::onConnectedChanged() @@ -98,3 +107,10 @@ void Engine::onConnectedChanged() } } } + +void Engine::onDeviceManagerFetchingChanged() +{ + if (!m_deviceManager->fetchingData()) { + m_tagsManager->init(); + } +} diff --git a/libnymea-app-core/engine.h b/libnymea-app-core/engine.h index 9162b0f5..d74fe3d7 100644 --- a/libnymea-app-core/engine.h +++ b/libnymea-app-core/engine.h @@ -30,6 +30,7 @@ class RuleManager; class LogManager; +class TagsManager; class BasicConfiguration; class Engine : public QObject @@ -38,6 +39,7 @@ class Engine : public QObject Q_PROPERTY(NymeaConnection* connection READ connection CONSTANT) Q_PROPERTY(DeviceManager* deviceManager READ deviceManager CONSTANT) Q_PROPERTY(RuleManager* ruleManager READ ruleManager CONSTANT) + Q_PROPERTY(TagsManager* tagsManager READ tagsManager CONSTANT) Q_PROPERTY(JsonRpcClient* jsonRpcClient READ jsonRpcClient CONSTANT) Q_PROPERTY(BasicConfiguration* basicConfiguration READ basicConfiguration CONSTANT) Q_PROPERTY(BluetoothDiscovery* bluetoothDiscovery READ bluetoothDiscovery CONSTANT) @@ -51,6 +53,7 @@ public: NymeaConnection *connection() const; DeviceManager *deviceManager() const; RuleManager *ruleManager() const; + TagsManager *tagsManager() const; JsonRpcClient *jsonRpcClient() const; LogManager *logManager() const; BasicConfiguration *basicConfiguration() const; @@ -65,11 +68,13 @@ private: DeviceManager *m_deviceManager; RuleManager *m_ruleManager; LogManager *m_logManager; + TagsManager *m_tagsManager; BasicConfiguration *m_basicConfiguration; BluetoothDiscovery *m_bluetoothDiscovery; private slots: void onConnectedChanged(); + void onDeviceManagerFetchingChanged(); }; diff --git a/libnymea-app-core/interfacesmodel.cpp b/libnymea-app-core/interfacesmodel.cpp index e060ce91..29d4e2d7 100644 --- a/libnymea-app-core/interfacesmodel.cpp +++ b/libnymea-app-core/interfacesmodel.cpp @@ -61,6 +61,20 @@ void InterfacesModel::setShownInterfaces(const QStringList &shownInterfaces) } } +bool InterfacesModel::showUncategorized() const +{ + return m_showUncategorized; +} + +void InterfacesModel::setShowUncategorized(bool showUncategorized) +{ + if (m_showUncategorized != showUncategorized) { + m_showUncategorized = showUncategorized; + emit showUncategorizedChanged(); + syncInterfaces(); + } +} + void InterfacesModel::syncInterfaces() { if (!m_devices) { @@ -72,6 +86,7 @@ void InterfacesModel::syncInterfaces() DeviceClass *dc = Engine::instance()->deviceManager()->deviceClasses()->getDeviceClass(m_devices->get(i)->deviceClassId()); // qDebug() << "device" <name() << "has interfaces" << dc->interfaces(); + bool isInShownIfaces = false; foreach (const QString &interface, dc->interfaces()) { if (!m_shownInterfaces.contains(interface)) { continue; @@ -80,6 +95,10 @@ void InterfacesModel::syncInterfaces() if (!interfacesInSource.contains(interface)) { interfacesInSource.append(interface); } + isInShownIfaces = true; + } + if (!isInShownIfaces && !interfacesInSource.contains("uncategorized")) { + interfacesInSource.append("uncategorized"); } } QStringList interfacesToAdd = interfacesInSource; @@ -113,3 +132,38 @@ void InterfacesModel::rowsChanged(const QModelIndex &index, int first, int last) syncInterfaces(); } + +InterfacesSortModel::InterfacesSortModel(QObject *parent): + QSortFilterProxyModel(parent) +{ +} + +InterfacesModel *InterfacesSortModel::interfacesModel() const +{ + return m_interfacesModel; +} + +void InterfacesSortModel::setInterfacesModel(InterfacesModel *interfacesModel) +{ + if (m_interfacesModel != interfacesModel) { + m_interfacesModel = interfacesModel; + setSourceModel(interfacesModel); + setSortRole(Devices::RoleName); + sort(0); + emit interfacesModelChanged(); + } +} + +bool InterfacesSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + QVariant leftName = sourceModel()->data(left, InterfacesModel::RoleName); + QVariant rightName = sourceModel()->data(right, InterfacesModel::RoleName); + + if (leftName == "uncategorized") { + return false; + } + if (rightName == "uncategorized") { + return true; + } + return m_interfacesModel->shownInterfaces().indexOf(leftName.toString()) < m_interfacesModel->shownInterfaces().indexOf(rightName.toString()); +} diff --git a/libnymea-app-core/interfacesmodel.h b/libnymea-app-core/interfacesmodel.h index b216816d..13a560c8 100644 --- a/libnymea-app-core/interfacesmodel.h +++ b/libnymea-app-core/interfacesmodel.h @@ -12,6 +12,7 @@ class InterfacesModel : public QAbstractListModel Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(Devices* devices READ devices WRITE setDevices NOTIFY devicesChanged) Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged) + Q_PROPERTY(bool showUncategorized READ showUncategorized WRITE setShowUncategorized NOTIFY showUncategorizedChanged) public: enum Roles { @@ -31,10 +32,14 @@ public: QStringList shownInterfaces() const; void setShownInterfaces(const QStringList &shownInterfaces); + bool showUncategorized() const; + void setShowUncategorized(bool showUncategorized); + signals: void countChanged(); void devicesChanged(); void shownInterfacesChanged(); + void showUncategorizedChanged(); private slots: void syncInterfaces(); @@ -45,6 +50,27 @@ private: QStringList m_interfaces; QStringList m_shownInterfaces; + bool m_showUncategorized = false; +}; + +class InterfacesSortModel: public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(InterfacesModel* interfacesModel READ interfacesModel WRITE setInterfacesModel NOTIFY interfacesModelChanged) + +public: + InterfacesSortModel(QObject *parent = nullptr); + + InterfacesModel* interfacesModel() const; + void setInterfacesModel(InterfacesModel* interfacesModel); + + bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; + +signals: + void interfacesModelChanged(); + +private: + InterfacesModel* m_interfacesModel = nullptr; }; #endif // INTERFACESMODEL_H diff --git a/libnymea-app-core/jsonrpc/jsontypes.cpp b/libnymea-app-core/jsonrpc/jsontypes.cpp index e80a99a4..bb620de4 100644 --- a/libnymea-app-core/jsonrpc/jsontypes.cpp +++ b/libnymea-app-core/jsonrpc/jsontypes.cpp @@ -239,6 +239,7 @@ QVariantMap JsonTypes::packRule(Rule *rule) } ret.insert("name", rule->name()); ret.insert("enabled", rule->enabled()); + ret.insert("executable", rule->executable()); if (rule->actions()->rowCount() > 0) { ret.insert("actions", packRuleActions(rule->actions())); diff --git a/libnymea-app-core/libnymea-app-core.h b/libnymea-app-core/libnymea-app-core.h index e0eab502..44611ca8 100644 --- a/libnymea-app-core/libnymea-app-core.h +++ b/libnymea-app-core/libnymea-app-core.h @@ -37,6 +37,9 @@ #include "models/interfacesproxy.h" #include "basicconfiguration.h" #include "wifisetup/networkmanagercontroler.h" +#include "tagsmanager.h" +#include "models/tagsproxymodel.h" +#include "types/tag.h" static QObject* interfacesModel_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { @@ -87,6 +90,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "Devices", "Can't create this in QML. Get it from the DeviceManager."); qmlRegisterType(uri, 1, 0, "DevicesProxy"); qmlRegisterType(uri, 1, 0, "InterfacesModel"); + qmlRegisterType(uri, 1, 0, "InterfacesSortModel"); qmlRegisterUncreatableType(uri, 1, 0, "DeviceClass", "Can't create this in QML. Get it from the DeviceClasses."); qmlRegisterUncreatableType(uri, 1, 0, "DeviceClasses", "Can't create this in QML. Get it from the DeviceManager."); @@ -141,6 +145,11 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "ValueLogsProxyModel"); qmlRegisterUncreatableType(uri, 1, 0, "LogEntry", "Get them from LogsModel"); + qmlRegisterUncreatableType(uri, 1, 0, "TagsManager", "Get it from Engine"); + 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, "NetworkManagerControler"); qmlRegisterUncreatableType(uri, 1, 0, "BluetoothDiscovery", "Can't create this in QML. Get it from the Engine instance."); qmlRegisterUncreatableType(uri, 1, 0, "BluetoothDeviceInfo", "Can't create this in QML. Get it from the DeviceInfos."); diff --git a/libnymea-app-core/libnymea-app-core.pro b/libnymea-app-core/libnymea-app-core.pro index 50675ea6..ec07d2ca 100644 --- a/libnymea-app-core/libnymea-app-core.pro +++ b/libnymea-app-core/libnymea-app-core.pro @@ -12,7 +12,7 @@ include(../config.pri) } QT -= gui -QT += websockets bluetooth +QT += network websockets bluetooth INCLUDEPATH += $$top_srcdir/libnymea-common $$top_srcdir/QtZeroConf @@ -57,7 +57,9 @@ SOURCES += \ wifisetup/wirelesssetupmanager.cpp \ wifisetup/networkmanagercontroler.cpp \ models/logsmodelng.cpp \ - models/interfacesproxy.cpp + models/interfacesproxy.cpp \ + tagsmanager.cpp \ + models/tagsproxymodel.cpp HEADERS += \ engine.h \ @@ -101,7 +103,9 @@ HEADERS += \ wifisetup/networkmanagercontroler.h \ libnymea-app-core.h \ models/logsmodelng.h \ - models/interfacesproxy.h + models/interfacesproxy.h \ + tagsmanager.h \ + models/tagsproxymodel.h unix { target.path = /usr/lib diff --git a/libnymea-app-core/models/rulesfiltermodel.cpp b/libnymea-app-core/models/rulesfiltermodel.cpp index 97ab2b7c..3b0c54d9 100644 --- a/libnymea-app-core/models/rulesfiltermodel.cpp +++ b/libnymea-app-core/models/rulesfiltermodel.cpp @@ -11,7 +11,7 @@ RulesFilterModel::RulesFilterModel(QObject *parent) : QSortFilterProxyModel(parent) { - + setSortRole(Rules::RoleName); } Rules *RulesFilterModel::rules() const @@ -27,6 +27,7 @@ void RulesFilterModel::setRules(Rules *rules) emit rulesChanged(); invalidateFilter(); emit countChanged(); + sort(0); } } @@ -45,6 +46,21 @@ void RulesFilterModel::setFilterDeviceId(const QString &filterDeviceId) } } +bool RulesFilterModel::filterExecutable() const +{ + return m_filterExecutable; +} + +void RulesFilterModel::setFilterExecutable(bool filterExecutable) +{ + if (m_filterExecutable != filterExecutable) { + m_filterExecutable = filterExecutable; + emit filterExecutableChanged(); + invalidateFilter(); + emit countChanged(); + } +} + Rule *RulesFilterModel::get(int index) const { return m_rules->get(mapToSource(this->index(index, 0)).row()); @@ -53,9 +69,12 @@ Rule *RulesFilterModel::get(int index) const bool RulesFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_parent) + Rule* rule = m_rules->get(source_row); + if (m_filterExecutable && !rule->executable()) { + return false; + } bool found = true; if (!m_filterDeviceId.isNull()) { - Rule* rule = m_rules->get(source_row); found = false; for (int i = 0; i < rule->eventDescriptors()->rowCount(); i++) { EventDescriptor *ed = rule->eventDescriptors()->get(i); diff --git a/libnymea-app-core/models/rulesfiltermodel.h b/libnymea-app-core/models/rulesfiltermodel.h index acae1e1d..76daaa23 100644 --- a/libnymea-app-core/models/rulesfiltermodel.h +++ b/libnymea-app-core/models/rulesfiltermodel.h @@ -13,6 +13,7 @@ class RulesFilterModel : public QSortFilterProxyModel Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(Rules* rules READ rules WRITE setRules NOTIFY rulesChanged) Q_PROPERTY(QString filterDeviceId READ filterDeviceId WRITE setFilterDeviceId NOTIFY filterDeviceIdChanged) + Q_PROPERTY(bool filterExecutable READ filterExecutable WRITE setFilterExecutable NOTIFY filterExecutableChanged) public: explicit RulesFilterModel(QObject *parent = nullptr); @@ -23,11 +24,15 @@ public: QString filterDeviceId() const; void setFilterDeviceId(const QString &filterDeviceId); + bool filterExecutable() const; + void setFilterExecutable(bool filterExecutable); + Q_INVOKABLE Rule* get(int index) const; signals: void rulesChanged(); void filterDeviceIdChanged(); + void filterExecutableChanged(); void countChanged(); protected: @@ -36,6 +41,7 @@ protected: private: Rules *m_rules = nullptr; QString m_filterDeviceId; + bool m_filterExecutable = false; }; #endif // RULESFILTERMODEL_H diff --git a/libnymea-app-core/models/tagsproxymodel.cpp b/libnymea-app-core/models/tagsproxymodel.cpp new file mode 100644 index 00000000..0522e8a0 --- /dev/null +++ b/libnymea-app-core/models/tagsproxymodel.cpp @@ -0,0 +1,111 @@ +#include "tagsproxymodel.h" +#include "engine.h" +#include "tagsmanager.h" +#include "types/tag.h" + +TagsProxyModel::TagsProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + setSourceModel(Engine::instance()->tagsManager()->tags()); + connect(Engine::instance()->tagsManager()->tags(), &Tags::countChanged, this, &TagsProxyModel::countChanged, Qt::QueuedConnection); + setSortRole(Tags::RoleValue); + sort(0); +} + +QString TagsProxyModel::filterTagId() const +{ + return m_filterTagId; +} + +void TagsProxyModel::setFilterTagId(const QString &filterTagId) +{ + if (m_filterTagId != filterTagId) { + m_filterTagId = filterTagId; + emit filterTagIdChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +QString TagsProxyModel::filterDeviceId() const +{ + return m_filterDeviceId; +} + +void TagsProxyModel::setFilterDeviceId(const QString &filterDeviceId) +{ + if (m_filterDeviceId != filterDeviceId) { + m_filterDeviceId = filterDeviceId; + emit filterDeviceIdChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +QString TagsProxyModel::filterRuleId() const +{ + return m_filterRuleId; +} + +void TagsProxyModel::setFilterRuleId(const QString &filterRuleId) +{ + if (m_filterRuleId != filterRuleId) { + m_filterRuleId = filterRuleId; + emit filterRuleIdChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +Tag *TagsProxyModel::get(int index) const +{ + if (index < 0 || index > rowCount()) { + return nullptr; + } + return Engine::instance()->tagsManager()->tags()->get(mapToSource(this->index(index, 0)).row()); +} + +Tag *TagsProxyModel::findTag(const QString &tagId) const +{ + for (int i = 0; i < rowCount(); i++) { + Tag *tag = Engine::instance()->tagsManager()->tags()->get(mapToSource(index(i, 0)).row()); + if (tag->tagId() == tagId) { + return tag; + } + } + return nullptr; +} + +bool TagsProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + Q_UNUSED(source_parent) + Tag *tag = Engine::instance()->tagsManager()->tags()->get(source_row); + if (!m_filterTagId.isEmpty()) { + if (tag->tagId() != m_filterTagId) { + return false; + } + } + if (!m_filterDeviceId.isEmpty()) { + if (tag->deviceId() != m_filterDeviceId) { + return false; + } + } + if (!m_filterRuleId.isEmpty()) { + if (tag->ruleId() != m_filterRuleId) { + return false; + } + } + return true; +} + +bool TagsProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + QString leftValue = Engine::instance()->tagsManager()->tags()->get(source_left.row())->value(); + QString rightValue = Engine::instance()->tagsManager()->tags()->get(source_right.row())->value(); + bool okLeft, okRight;; + qlonglong leftAsNumber = leftValue.toLongLong(&okLeft); + qlonglong rightAsNumber = rightValue.toLongLong(&okRight); + if (okLeft && okRight) { + return leftAsNumber < rightAsNumber; + } + return leftValue < rightValue; +} diff --git a/libnymea-app-core/models/tagsproxymodel.h b/libnymea-app-core/models/tagsproxymodel.h new file mode 100644 index 00000000..81769800 --- /dev/null +++ b/libnymea-app-core/models/tagsproxymodel.h @@ -0,0 +1,47 @@ +#ifndef TAGSPROXYMODEL_H +#define TAGSPROXYMODEL_H + +#include + +class Tag; + +class TagsProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged) + Q_PROPERTY(QString filterDeviceId READ filterDeviceId WRITE setFilterDeviceId NOTIFY filterDeviceIdChanged) + Q_PROPERTY(QString filterRuleId READ filterRuleId WRITE setFilterRuleId NOTIFY filterRuleIdChanged) + +public: + explicit TagsProxyModel(QObject *parent = nullptr); + + QString filterTagId() const; + void setFilterTagId(const QString &filterTagId); + + QString filterDeviceId() const; + void setFilterDeviceId(const QString &filterDeviceId); + + QString filterRuleId() const; + void setFilterRuleId(const QString &filterRuleId); + + Q_INVOKABLE Tag* get(int index) const; + Q_INVOKABLE Tag* findTag(const QString &tagId) const; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + +signals: + void filterTagIdChanged(); + void filterDeviceIdChanged(); + void filterRuleIdChanged(); + void countChanged(); + +private: + QString m_filterTagId; + QString m_filterDeviceId; + QString m_filterRuleId; +}; + +#endif // TAGSPROXYMODEL_H diff --git a/libnymea-app-core/nymeaconnection.cpp b/libnymea-app-core/nymeaconnection.cpp index 0f2fdd86..4ef00727 100644 --- a/libnymea-app-core/nymeaconnection.cpp +++ b/libnymea-app-core/nymeaconnection.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "nymeainterface.h" #include "tcpsocketinterface.h" @@ -133,8 +134,8 @@ void NymeaConnection::onSslErrors(const QList &errors) void NymeaConnection::onError(QAbstractSocket::SocketError error) { - qWarning() << "Socket error" << error; - emit connectionError(); + QMetaEnum errorEnum = QMetaEnum::fromType(); + emit connectionError(errorEnum.valueToKey(error)); } void NymeaConnection::onConnected() diff --git a/libnymea-app-core/nymeaconnection.h b/libnymea-app-core/nymeaconnection.h index abdc0aa8..f913cf55 100644 --- a/libnymea-app-core/nymeaconnection.h +++ b/libnymea-app-core/nymeaconnection.h @@ -34,7 +34,7 @@ public: signals: void verifyConnectionCertificate(const QString &url, const QStringList &issuerInfo, const QByteArray &fingerprint); void connectedChanged(bool connected); - void connectionError(); + void connectionError(const QString &error); void dataAvailable(const QByteArray &data); private slots: diff --git a/libnymea-app-core/rulemanager.cpp b/libnymea-app-core/rulemanager.cpp index bbad1c58..feb16a5a 100644 --- a/libnymea-app-core/rulemanager.cpp +++ b/libnymea-app-core/rulemanager.cpp @@ -80,9 +80,16 @@ void RuleManager::editRule(Rule *rule) } +void RuleManager::executeActions(const QString &ruleId) +{ + QVariantMap params; + params.insert("ruleId", ruleId); + m_jsonClient->sendCommand("Rules.ExecuteActions", params, this, "onExecuteRuleActionsReply"); +} + void RuleManager::handleRulesNotification(const QVariantMap ¶ms) { -// qDebug() << "rules notification received" << params; + qDebug() << "rules notification received" << params; if (params.value("notification").toString() == "Rules.RuleAdded") { QVariantMap ruleMap = params.value("params").toMap().value("rule").toMap(); m_rules->insert(parseRule(ruleMap)); @@ -124,11 +131,13 @@ void RuleManager::getRulesReply(const QVariantMap ¶ms) QString name = ruleDescriptionVariant.toMap().value("name").toString(); bool enabled = ruleDescriptionVariant.toMap().value("enabled").toBool(); bool active = ruleDescriptionVariant.toMap().value("active").toBool(); + bool executable = ruleDescriptionVariant.toMap().value("executable").toBool(); Rule *rule = new Rule(ruleId, m_rules); rule->setName(name); rule->setEnabled(enabled); rule->setActive(active); + rule->setExecutable(executable); m_rules->insert(rule); QVariantMap requestParams; @@ -156,7 +165,7 @@ void RuleManager::getRuleDetailsReply(const QVariantMap ¶ms) void RuleManager::onAddRuleReply(const QVariantMap ¶ms) { qDebug() << "Add rule reply" << params; - emit addRuleReply(params.value("params").toMap().value("ruleError").toString()); + emit addRuleReply(params.value("params").toMap().value("ruleError").toString(), params.value("params").toMap().value("ruleId").toString()); } void RuleManager::removeRuleReply(const QVariantMap ¶ms) @@ -171,16 +180,23 @@ void RuleManager::onEditRuleReply(const QVariantMap ¶ms) emit editRuleReply(params.value("params").toMap().value("ruleError").toString()); } +void RuleManager::onExecuteRuleActionsReply(const QVariantMap ¶ms) +{ + qDebug() << "Execute rule actions reply:" << params; +} + Rule *RuleManager::parseRule(const QVariantMap &ruleMap) { QUuid ruleId = ruleMap.value("id").toUuid(); QString name = ruleMap.value("name").toString(); bool enabled = ruleMap.value("enabled").toBool(); bool active = ruleMap.value("active").toBool(); + bool executable = ruleMap.value("executable").toBool(); Rule* rule = new Rule(ruleId); rule->setName(name); rule->setEnabled(enabled); rule->setActive(active); + rule->setExecutable(executable); parseEventDescriptors(ruleMap.value("eventDescriptors").toList(), rule); parseRuleActions(ruleMap.value("actions").toList(), rule); parseRuleExitActions(ruleMap.value("exitActions").toList(), rule); diff --git a/libnymea-app-core/rulemanager.h b/libnymea-app-core/rulemanager.h index fad4a8ff..83018492 100644 --- a/libnymea-app-core/rulemanager.h +++ b/libnymea-app-core/rulemanager.h @@ -31,6 +31,7 @@ public: Q_INVOKABLE void addRule(Rule *rule); Q_INVOKABLE void removeRule(const QUuid &ruleId); Q_INVOKABLE void editRule(Rule *rule); + Q_INVOKABLE void executeActions(const QString &ruleId); private slots: void handleRulesNotification(const QVariantMap ¶ms); @@ -39,6 +40,7 @@ private slots: void onAddRuleReply(const QVariantMap ¶ms); void removeRuleReply(const QVariantMap ¶ms); void onEditRuleReply(const QVariantMap ¶ms); + void onExecuteRuleActionsReply(const QVariantMap ¶ms); private: Rule *parseRule(const QVariantMap &ruleMap); @@ -50,7 +52,7 @@ private: void parseTimeDescriptor(const QVariantMap &timeDescriptor, Rule *rule); signals: - void addRuleReply(const QString &ruleError); + void addRuleReply(const QString &ruleError, const QString &ruleId); void editRuleReply(const QString &ruleError); private: diff --git a/libnymea-app-core/tagsmanager.cpp b/libnymea-app-core/tagsmanager.cpp new file mode 100644 index 00000000..a0a3fa2c --- /dev/null +++ b/libnymea-app-core/tagsmanager.cpp @@ -0,0 +1,151 @@ +#include "tagsmanager.h" +#include "types/tag.h" +#include "engine.h" + +TagsManager::TagsManager(JsonRpcClient *jsonClient, QObject *parent): + JsonHandler(parent), + m_jsonClient(jsonClient), + m_tags(new Tags(this)) +{ + jsonClient->registerNotificationHandler(this, "handleTagsNotification"); +} + +QString TagsManager::nameSpace() const +{ + return "Tags"; +} + +void TagsManager::init() +{ + m_tags->clear(); + m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply"); +} + +Tags *TagsManager::tags() const +{ + return m_tags; +} + +void TagsManager::tagDevice(const QString &deviceId, const QString &tagId, const QString &value) +{ + QVariantMap params; + QVariantMap tag; + tag.insert("deviceId", deviceId); + tag.insert("appId", "nymea:app"); + tag.insert("tagId", tagId); + tag.insert("value", value); + params.insert("tag", tag); + m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagReply"); +} + +void TagsManager::untagDevice(const QString &deviceId, const QString &tagId) +{ + QVariantMap params; + QVariantMap tag; + tag.insert("deviceId", deviceId); + tag.insert("appId", "nymea:app"); + tag.insert("tagId", tagId); + params.insert("tag", tag); + m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagReply"); +} + +void TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QString &value) +{ + QVariantMap params; + QVariantMap tag; + tag.insert("ruleId", ruleId); + tag.insert("appId", "nymea:app"); + tag.insert("tagId", tagId); + tag.insert("value", value); + params.insert("tag", tag); + m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagReply"); +} + +void TagsManager::untagRule(const QString &ruleId, const QString &tagId) +{ + QVariantMap params; + QVariantMap tag; + tag.insert("ruleId", ruleId); + tag.insert("appId", "nymea:app"); + tag.insert("tagId", tagId); + params.insert("tag", tag); + m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagReply"); +} + +void TagsManager::handleTagsNotification(const QVariantMap ¶ms) +{ + qDebug() << "Have tags notification" << params; + + QVariantMap tagMap = params.value("params").toMap().value("tag").toMap(); + if (tagMap.value("appId").toString() != "nymea:app") { + return; // not for us + } + + QString notification = params.value("notification").toString(); + if (notification == "Tags.TagAdded") { + addTagInternal(tagMap); + + } 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() && + tagMap.value("tagId").toString() == tag->tagId()) { + m_tags->removeTag(tag); + return; + } + } + } else if (notification == "Tags.TagValueChanged") { + 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() && + tagMap.value("tagId").toString() == tag->tagId()) { + qDebug() << "Found tag"; + tag->setValue(tagMap.value("value").toString()); + } + } + } +} + +void TagsManager::getTagsReply(const QVariantMap ¶ms) +{ + qDebug() << "Have tags" << params; + foreach (const QVariant &tagVariant, params.value("params").toMap().value("tags").toList()) { + addTagInternal(tagVariant.toMap()); + } + emit tagsChanged(); +} + +void TagsManager::addTagReply(const QVariantMap ¶ms) +{ + qDebug() << "AddTag reply" << params; +} + +void TagsManager::removeTagReply(const QVariantMap ¶ms) +{ + qDebug() << "RemoveTag reply" << params; +} + +void TagsManager::addTagInternal(const QVariantMap &tagMap) +{ + QString deviceId = tagMap.value("deviceId").toString(); + QString ruleId = tagMap.value("ruleId").toString(); + QString tagId = tagMap.value("tagId").toString(); + QString value = tagMap.value("value").toString(); + Tag *tag = nullptr; + if (!deviceId.isEmpty()) { + tag = new Tag(tagId, value); + tag->setDeviceId(deviceId); + } else if (!ruleId.isEmpty()) { + tag = new Tag(tagId, value); + tag->setRuleId(ruleId); + } else { + qWarning() << "Invalid tag. Neither deviceId nor ruleId are set. Skipping..."; + tag->deleteLater(); + return; + } + qDebug() << "adding tag" << tag->tagId() << tag->value(); + m_tags->addTag(tag); +} diff --git a/libnymea-app-core/tagsmanager.h b/libnymea-app-core/tagsmanager.h new file mode 100644 index 00000000..fefef407 --- /dev/null +++ b/libnymea-app-core/tagsmanager.h @@ -0,0 +1,44 @@ +#ifndef TAGSMANAGER_H +#define TAGSMANAGER_H + +#include "jsonrpc/jsonhandler.h" +#include "jsonrpc/jsonrpcclient.h" + +#include "types/tags.h" + +class TagsManager : public JsonHandler +{ + Q_OBJECT + Q_PROPERTY(Tags* tags READ tags NOTIFY tagsChanged) + +public: + explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); + QString nameSpace() const override; + + void init(); + + Tags* tags() const; + + Q_INVOKABLE void tagDevice(const QString &deviceId, const QString &tagId, const QString &value); + Q_INVOKABLE void untagDevice(const QString &deviceId, const QString &tagId); + Q_INVOKABLE void tagRule(const QString &ruleId, const QString &tagId, const QString &value); + Q_INVOKABLE void untagRule(const QString &ruleId, const QString &tagId); + +signals: + void tagsChanged(); + +private slots: + void handleTagsNotification(const QVariantMap ¶ms); + void getTagsReply(const QVariantMap ¶ms); + void addTagReply(const QVariantMap ¶ms); + void removeTagReply(const QVariantMap ¶ms); + +private: + void addTagInternal(const QVariantMap &tagMap); + + JsonRpcClient *m_jsonClient = nullptr; + + Tags *m_tags = nullptr; +}; + +#endif // TAGSMANAGER_H diff --git a/libnymea-common/libnymea-common.pro b/libnymea-common/libnymea-common.pro index 5574cf14..cfe3fcb7 100644 --- a/libnymea-common/libnymea-common.pro +++ b/libnymea-common/libnymea-common.pro @@ -52,7 +52,9 @@ HEADERS += \ types/calendaritem.h \ types/timeeventitems.h \ types/calendaritems.h \ - types/repeatingoption.h + types/repeatingoption.h \ + types/tag.h \ + types/tags.h SOURCES += \ types/vendor.cpp \ @@ -95,7 +97,9 @@ SOURCES += \ types/calendaritem.cpp \ types/timeeventitems.cpp \ types/calendaritems.cpp \ - types/repeatingoption.cpp + types/repeatingoption.cpp \ + types/tag.cpp \ + types/tags.cpp # install header file with relative subdirectory for(header, HEADERS) { diff --git a/libnymea-common/types/rule.cpp b/libnymea-common/types/rule.cpp index 039847a7..c94e2022 100644 --- a/libnymea-common/types/rule.cpp +++ b/libnymea-common/types/rule.cpp @@ -24,7 +24,7 @@ Rule::Rule(const QUuid &id, QObject *parent) : m_exitActions(new RuleActions(this)), m_timeDescriptor(new TimeDescriptor(this)) { - qDebug() << "### Creating rule" << this; +// qDebug() << "### Creating rule" << this; } Rule::~Rule() @@ -76,6 +76,19 @@ void Rule::setActive(bool active) } } +bool Rule::executable() const +{ + return m_executable; +} + +void Rule::setExecutable(bool executable) +{ + if (m_executable != executable) { + m_executable = executable; + emit executableChanged(); + } +} + EventDescriptors *Rule::eventDescriptors() const { return m_eventDescriptors; @@ -123,6 +136,7 @@ Rule *Rule::clone() const Rule *ret = new Rule(this->id()); ret->setName(this->name()); ret->setEnabled(this->enabled()); + ret->setExecutable(this->executable()); for (int i = 0; i < this->eventDescriptors()->rowCount(); i++) { ret->eventDescriptors()->addEventDescriptor(this->eventDescriptors()->get(i)->clone()); } diff --git a/libnymea-common/types/rule.h b/libnymea-common/types/rule.h index a44472dd..89a90a29 100644 --- a/libnymea-common/types/rule.h +++ b/libnymea-common/types/rule.h @@ -16,6 +16,7 @@ class Rule : public QObject Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) Q_PROPERTY(bool active READ active NOTIFY activeChanged) + Q_PROPERTY(bool executable READ executable WRITE setExecutable NOTIFY executableChanged) Q_PROPERTY(EventDescriptors* eventDescriptors READ eventDescriptors CONSTANT) Q_PROPERTY(StateEvaluator* stateEvaluator READ stateEvaluator WRITE setStateEvaluator NOTIFY stateEvaluatorChanged) Q_PROPERTY(RuleActions* actions READ actions CONSTANT) @@ -36,6 +37,9 @@ public: bool active() const; void setActive(bool active); + bool executable() const; + void setExecutable(bool executable); + EventDescriptors* eventDescriptors() const; StateEvaluator *stateEvaluator() const; RuleActions* actions() const; @@ -52,6 +56,7 @@ signals: void nameChanged(); void enabledChanged(); void activeChanged(); + void executableChanged(); void stateEvaluatorChanged(); private: @@ -59,6 +64,7 @@ private: QString m_name; bool m_enabled = true; bool m_active = false; + bool m_executable = false; EventDescriptors *m_eventDescriptors = nullptr; StateEvaluator *m_stateEvaluator = nullptr; RuleActions *m_actions = nullptr; diff --git a/libnymea-common/types/rules.cpp b/libnymea-common/types/rules.cpp index d2982acf..b6f96f92 100644 --- a/libnymea-common/types/rules.cpp +++ b/libnymea-common/types/rules.cpp @@ -34,6 +34,8 @@ QVariant Rules::data(const QModelIndex &index, int role) const return m_list.at(index.row())->enabled(); case RoleActive: return m_list.at(index.row())->active(); + case RoleExecutable: + return m_list.at(index.row())->executable(); } return QVariant(); } @@ -45,6 +47,7 @@ QHash Rules::roleNames() const roles.insert(RoleId, "id"); roles.insert(RoleEnabled, "enabled"); roles.insert(RoleActive, "active"); + roles.insert(RoleExecutable, "executable"); return roles; } @@ -56,6 +59,7 @@ void Rules::insert(Rule *rule) connect(rule, &Rule::enabledChanged, this, &Rules::ruleChanged); connect(rule, &Rule::activeChanged, this, &Rules::ruleChanged); connect(rule, &Rule::nameChanged, this, &Rules::ruleChanged); + connect(rule, &Rule::executableChanged, this, &Rules::ruleChanged); endInsertRows(); emit countChanged(); } @@ -103,5 +107,5 @@ void Rules::ruleChanged() return; } QModelIndex modelIndex = index(idx); - emit dataChanged(modelIndex, modelIndex, {RoleActive, RoleEnabled, RoleName}); + emit dataChanged(modelIndex, modelIndex, {RoleActive, RoleEnabled, RoleName, RoleExecutable}); } diff --git a/libnymea-common/types/rules.h b/libnymea-common/types/rules.h index 60d76aa2..52b3a219 100644 --- a/libnymea-common/types/rules.h +++ b/libnymea-common/types/rules.h @@ -14,7 +14,8 @@ public: RoleName, RoleId, RoleEnabled, - RoleActive + RoleActive, + RoleExecutable }; explicit Rules(QObject *parent = nullptr); diff --git a/libnymea-common/types/tag.cpp b/libnymea-common/types/tag.cpp new file mode 100644 index 00000000..3ea5e823 --- /dev/null +++ b/libnymea-common/types/tag.cpp @@ -0,0 +1,50 @@ +#include "tag.h" + +#include + +Tag::Tag(const QString &tagId, const QString &value, QObject *parent): + QObject(parent), + m_tagId(tagId), + m_value(value) +{ + +} + +QString Tag::deviceId() const +{ + return m_deviceId; +} + +void Tag::setDeviceId(const QString &deviceId) +{ + m_deviceId = deviceId; +} + +QString Tag::ruleId() const +{ + return m_ruleId; +} + +void Tag::setRuleId(const QString &ruleId) +{ + m_ruleId = ruleId; +} + +QString Tag::tagId() const +{ + return m_tagId; +} + +QString Tag::value() const +{ + return m_value; +} + +void Tag::setValue(const QString &value) +{ + if (m_value != value) { + m_value = value; + qDebug() << "tags value changed" << m_deviceId << m_tagId << value; + emit valueChanged(); + } +} diff --git a/libnymea-common/types/tag.h b/libnymea-common/types/tag.h new file mode 100644 index 00000000..e0be291b --- /dev/null +++ b/libnymea-common/types/tag.h @@ -0,0 +1,38 @@ +#ifndef TAG_H +#define TAG_H + +#include + +class Tag : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString deviceId READ deviceId CONSTANT) + Q_PROPERTY(QString 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); + + QString ruleId() const; + void setRuleId(const QString &ruleId); + + QString tagId() const; + + QString value() const; + void setValue(const QString &value); + +signals: + void valueChanged(); + +private: + QString m_deviceId; + QString m_ruleId; + QString m_tagId; + QString m_value; +}; + +#endif // TAG_H diff --git a/libnymea-common/types/tags.cpp b/libnymea-common/types/tags.cpp new file mode 100644 index 00000000..58ef1cce --- /dev/null +++ b/libnymea-common/types/tags.cpp @@ -0,0 +1,106 @@ +#include "tags.h" +#include "tag.h" + +#include + +Tags::Tags(QObject *parent) : QAbstractListModel(parent) +{ + +} + +int Tags::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant Tags::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleDeviceId: + return m_list.at(index.row())->deviceId(); + case RoleRuleId: + return m_list.at(index.row())->ruleId(); + case RoleTagId: + return m_list.at(index.row())->tagId(); + case RoleValue: + return m_list.at(index.row())->value(); + } + return QVariant(); +} + +QHash Tags::roleNames() const +{ + QHash roles; + roles.insert(RoleDeviceId, "deviceId"); + roles.insert(RoleRuleId, "ruleId"); + roles.insert(RoleTagId, "tagId"); + roles.insert(RoleValue, "value"); + return roles; +} + +void Tags::addTag(Tag *tag) +{ + tag->setParent(this); + connect(tag, &Tag::valueChanged, this, &Tags::tagValueChanged); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(tag); + endInsertRows(); + emit countChanged(); +} + +void Tags::removeTag(Tag *tag) +{ + int idx = m_list.indexOf(tag); + if (idx < 0) { + qWarning() << "Don't know this tag. Can't remove"; + return; + } + beginRemoveRows(QModelIndex(), idx, idx); + m_list.removeAt(idx); + endRemoveRows(); + tag->deleteLater(); + emit countChanged(); +} + +Tag *Tags::get(int index) const +{ + return m_list.at(index); +} + +Tag *Tags::findDeviceTag(const QString &deviceId, const QString &tagId) const +{ + foreach (Tag *tag, m_list) { + if (tag->deviceId() == deviceId && tag->tagId() == tagId) { + return tag; + } + } + return nullptr; +} + +Tag *Tags::findRuleTag(const QString &ruleId, const QString &tagId) const +{ + foreach (Tag *tag, m_list) { + if (tag->ruleId() == ruleId && tag->tagId() == tagId) { + return tag; + } + } + return nullptr; +} + +void Tags::clear() +{ + beginResetModel(); + qDeleteAll(m_list); + m_list.clear(); + endResetModel(); + emit countChanged(); +} + +void Tags::tagValueChanged() +{ + qDebug() << "Tag value in mode changed"; + Tag *tag = static_cast(sender()); + int idx = m_list.indexOf(tag); + emit dataChanged(index(idx, 0), index(idx, 0), {RoleValue}); +} diff --git a/libnymea-common/types/tags.h b/libnymea-common/types/tags.h new file mode 100644 index 00000000..00373b69 --- /dev/null +++ b/libnymea-common/types/tags.h @@ -0,0 +1,47 @@ +#ifndef TAGS_H +#define TAGS_H + +#include + +class Tag; + +class Tags: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleDeviceId, + RoleRuleId, + RoleTagId, + RoleValue + }; + Q_ENUM(Roles) + + explicit Tags(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void addTag(Tag *tag); + void removeTag(Tag *tag); + + Tag* get(int index) const; + + Q_INVOKABLE Tag* findDeviceTag(const QString &deviceId, const QString &tagId) const; + Q_INVOKABLE Tag* findRuleTag(const QString &ruleId, const QString &tagId) const; + + void clear(); + +signals: + void countChanged(); + +private slots: + void tagValueChanged(); + +private: + QList m_list; +}; + +#endif // TAGS_H diff --git a/nymea-app.pro b/nymea-app.pro index 6e856a2c..95bd34fd 100644 --- a/nymea-app.pro +++ b/nymea-app.pro @@ -6,6 +6,8 @@ SUBDIRS = libnymea-common libnymea-app-core nymea-app libnymea-app-core.depends = libnymea-common nymea-app.depends = libnymea-app-core +#QML_IMPORT_PATH=/home/micha/Develop/Qt/5.11.0/gcc_64/qml/ + withtests: { SUBDIRS += tests tests.depends = libnymea-app-core diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index e54d4615..252413f0 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -2,7 +2,7 @@ TEMPLATE=app TARGET=nymea-app include(../config.pri) -QT += qml quick quickcontrols2 svg websockets bluetooth +QT += network qml quick quickcontrols2 svg websockets bluetooth INCLUDEPATH += $$top_srcdir/libnymea-common \ $$top_srcdir/libnymea-app-core diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 243d8c16..5de82b1e 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -2,7 +2,7 @@ ui/Nymea.qml ui/ConnectPage.qml - ui/DevicesPage.qml + ui/mainviews/DevicesPage.qml ui/NewDeviceWizard.qml ui/SettingsPage.qml ui/components/GuhHeader.qml @@ -203,5 +203,14 @@ ui/images/rpi-setup.svg ui/images/eye.svg ui/images/private-browsing.svg + ui/images/starred.svg + ui/images/non-starred.svg + ui/images/slideshow.svg + ui/components/MainPageTabButton.qml + ui/mainviews/ScenesView.qml + ui/mainviews/FavoritesView.qml + ui/mainviews/DevicesPageDelegate.qml + ui/components/AutoSizeMenu.qml + ui/components/EmptyViewPlaceholder.qml diff --git a/nymea-app/ui/ConnectPage.qml b/nymea-app/ui/ConnectPage.qml index bac1390f..a70d1f2b 100644 --- a/nymea-app/ui/ConnectPage.qml +++ b/nymea-app/ui/ConnectPage.qml @@ -28,6 +28,20 @@ Page { popup.open(); } onConnectionError: { + var errorMessage; + switch (error) { + case "ConnectionRefusedError": + errorMessage = qsTr("The host has rejected our connection. This probably means that %1 stopped running. Did you unplug your %1 box?").arg(app.systemName); + break; + case "SslInvalidUserDataError": + case "SslHandshakeFailedError": + // silently ignore. They'll be handled by the SSL logic + return; + } + var comp = Qt.createComponent(Qt.resolvedUrl("components/ErrorDialog.qml")) + var popup = comp.createObject(app, {text: errorMessage}) + popup.open() + pageStack.pop(root) pageStack.push(discoveryPage) } diff --git a/nymea-app/ui/DevicesPage.qml b/nymea-app/ui/DevicesPage.qml deleted file mode 100644 index 10570646..00000000 --- a/nymea-app/ui/DevicesPage.qml +++ /dev/null @@ -1,284 +0,0 @@ -import QtQuick 2.8 -import QtQuick.Controls 2.1 -import QtQuick.Controls.Material 2.1 -import QtQuick.Layouts 1.2 -import Nymea 1.0 -import "components" - -Item { - id: root - property alias count: interfacesGridView.count - property alias model: interfacesGridView.model - - GridView { - id: interfacesGridView - anchors.fill: parent - anchors.margins: app.margins / 2 - - readonly property int minTileWidth: 180 - readonly property int minTileHeight: 240 - readonly property int tilesPerRow: root.width / minTileWidth - - model: InterfacesModel { - id: interfacesModel - devices: Engine.deviceManager.devices - } - cellWidth: width / tilesPerRow - cellHeight: Math.max(cellWidth, minTileHeight) - delegate: Item { - width: interfacesGridView.cellWidth - height: interfacesGridView.cellHeight - Pane { - anchors.fill: parent - anchors.margins: app.margins / 2 - Material.elevation: 1 - - Column { - width: parent.width - anchors.centerIn: parent - anchors.verticalCenterOffset: -app.iconSize - spacing: app.margins - ColorIcon { - height: app.iconSize * 2 - width: height - color: app.guhAccent - anchors.horizontalCenter: parent.horizontalCenter - name: interfaceToIcon(model.name) - } - - Label { - text: interfaceToString(model.name).toUpperCase() - width: parent.width - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - var page; - switch (model.name) { - case "light": - page = "LightsDeviceListPage.qml" - break; - default: - page = "GenericDeviceListPage.qml" - } - - pageStack.push(Qt.resolvedUrl("devicelistpages/" + page), {filterInterface: model.name}) - } - } - - DevicesProxy { - id: devicesProxy - devices: Engine.deviceManager.devices - filterInterface: model.name - } - } - - Item { - id: inlineControlPane - anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: app.margins / 2 } - height: app.iconSize + app.margins * 2 - Rectangle { - anchors.fill: parent - // color: app.guhAccent - color: "black" - opacity: .05 - } - - Loader { - id: inlineControlLoader - anchors { - fill: parent - leftMargin: app.margins - rightMargin: app.margins - topMargin: app.margins / 2 - bottomMargin: app.margins / 2 - } - sourceComponent: { - switch (model.name) { - case "sensor": - case "weather": - return labelComponent; - - case "light": - case "media": - case "garagegate": - case "shutter": - case "blind": - return buttonComponent - } - } - } - } - - Component { - id: buttonComponent - MouseArea { - onClicked: { - switch (model.name) { - case "light": - if (devicesProxy.count == 1) { - var device = devicesProxy.get(0); - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var stateType = deviceClass.stateTypes.findByName("power") - var actionType = deviceClass.actionTypes.findByName("power") - var params = []; - var param1 = {}; - param1["paramTypeId"] = actionType.paramTypes.get(0).id; - param1["value"] = !device.states.getState(stateType.id).value; - params.push(param1) - Engine.deviceManager.executeAction(device.id, actionType.id, params) - } else { - for (var i = 0; i < devicesProxy.count; i++) { - var device = devicesProxy.get(i); - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var actionType = deviceClass.actionTypes.findByName("power"); - - var params = []; - var param1 = {}; - param1["paramTypeId"] = actionType.paramTypes.get(0).id; - param1["value"] = false; - params.push(param1) - Engine.deviceManager.executeAction(device.id, actionType.id, params) - } - } - break; - case "media": - var device = devicesProxy.get(0) - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var stateType = deviceClass.stateTypes.findByName("playbackStatus"); - var state = device.states.getState(stateType.id) - - var actionName - switch (state.value) { - case "PLAYING": - actionName = "pause"; - break; - case "PAUSED": - actionName = "play"; - break; - } - var actionTypeId = deviceClass.actionTypes.findByName(actionName).id; - - print("executing", device, device.id, actionTypeId, actionName, deviceClass.actionTypes) - - Engine.deviceManager.executeAction(device.id, actionTypeId) - case "garagegate": - case "shutter": - case "blind": - for (var i = 0; i < devicesProxy.count; i++) { - var device = devicesProxy.get(i); - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var actionType = deviceClass.actionTypes.findByName("close"); - Engine.deviceManager.executeAction(device.id, actionType.id) - } - - } - } - - ColumnLayout { - anchors.fill: parent - - Label { - id: label - Layout.fillWidth: true - text: { - switch (model.name) { - case "media": - return devicesProxy.get(0).name; - case "light": - var count = 0; - for (var i = 0; i < devicesProxy.count; i++) { - var device = devicesProxy.get(i); - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var stateType = deviceClass.stateTypes.findByName("power") - if (device.states.getState(stateType.id).value === true) { - count++; - } - } - return count === 0 ? qsTr("All off") : qsTr("%1 on").arg(count) - case "garagegate": - var count = 0; - for (var i = 0; i < devicesProxy.count; i++) { - var device = devicesProxy.get(i); - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var stateType = deviceClass.stateTypes.findByName("state"); - if (device.states.getState(stateType.id).value !== "closed") { - count++; - } - } - return count === 0 ? qsTr("All closed") : qsTr("%1 open").arg(count) - case "shutter": - return qsTr("%1 installed").arg(devicesProxy.count) - } - console.warn("Unhandled interface", model.name) - } - font.pixelSize: app.smallFont - elide: Text.ElideRight - } - ColorIcon { - id: icon - width: app.largeFont - height: width - color: app.guhAccent - anchors.right: parent.right - name: { - switch (model.name) { - case "media": - var device = devicesProxy.get(0) - var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var stateType = deviceClass.stateTypes.findByName("playbackStatus"); - var state = device.states.getState(stateType.id) - return state.value === "PLAYING" ? "../images/media-playback-pause.svg" : - state.value === "PAUSED" ? "../images/media-playback-start.svg" : - "" - case "light": - return "../images/system-shutdown.svg" - case "garagegate": - case "shutter": - case "blind": - return "../images/down.svg" - } - } - } - } - } - } - - Component { - id: labelComponent - ColumnLayout { - property var device: devicesProxy.get(0) - property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null - property var state: deviceClass ? device.states.getState(deviceClass.stateTypes.findByName("temperature").id) : null - - Label { - text: parent.device.name - font.pixelSize: app.smallFont - Layout.fillWidth: true - elide: Text.ElideRight - } - - Label { - font.pixelSize: app.largeFont - color: app.guhAccent - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - text: { - if (devicesProxy.count > 0) { - var stateName; - // switch (model.name) { - // case "sensor": - // } - return parent.state.value + "°C"; - } - } - } - } - } - } - } -} diff --git a/nymea-app/ui/EditDevicesPage.qml b/nymea-app/ui/EditDevicesPage.qml index 53c67f1f..d430a5d3 100644 --- a/nymea-app/ui/EditDevicesPage.qml +++ b/nymea-app/ui/EditDevicesPage.qml @@ -33,42 +33,14 @@ Page { } } - ColumnLayout { + EmptyViewPlaceholder { anchors { left: parent.left; right: parent.right; margins: app.margins } anchors.verticalCenter: parent.verticalCenter - spacing: app.margins * 2 visible: Engine.deviceManager.devices.count === 0 && !Engine.deviceManager.fetchingData - Label { - text: qsTr("There are no things set up yet.") - font.pixelSize: app.largeFont - Layout.fillWidth: true - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - color: app.guhAccent - } - Label { - text: qsTr("In order for your %1 box to be useful, go ahead and add some things.").arg(app.systemName) - Layout.fillWidth: true - Layout.maximumWidth: 400 - Layout.alignment: Qt.AlignHCenter - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - Image { - source: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) - Layout.preferredWidth: app.iconSize * 5 - Layout.preferredHeight: width - Layout.alignment: Qt.AlignHCenter - sourceSize.width: app.iconSize * 5 - sourceSize.height: app.iconSize * 5 - } - Button { - Layout.fillWidth: true - Layout.maximumWidth: 400 - Layout.alignment: Qt.AlignHCenter - text: qsTr("Add a thing") - onClicked: pageStack.push(Qt.resolvedUrl("NewDeviceWizard.qml")) - } + title: qsTr("There are no things set up yet.") + text: qsTr("In order for your %1 box to be useful, go ahead and add some things.").arg(app.systemName) + imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) + buttonText: qsTr("Add a thing") + onButtonClicked: pageStack.push(Qt.resolvedUrl("NewDeviceWizard.qml")) } - } diff --git a/nymea-app/ui/MagicPage.qml b/nymea-app/ui/MagicPage.qml index 93530d41..5bd11e39 100644 --- a/nymea-app/ui/MagicPage.qml +++ b/nymea-app/ui/MagicPage.qml @@ -43,6 +43,9 @@ Page { onAddRuleReply: { d.editRulePage.busy = false; if (ruleError == "RuleErrorNoError") { + print("should tag rule now:", d.editRulePage.rule.id, d.editRulePage.ruleIcon, d.editRulePage.ruleColor) + Engine.tagsManager.tagRule(ruleId, "color", d.editRulePage.ruleColor) + Engine.tagsManager.tagRule(ruleId, "icon", d.editRulePage.ruleIcon) pageStack.pop(); } else { var popup = errorDialog.createObject(app, {errorCode: ruleError }) @@ -53,6 +56,9 @@ Page { onEditRuleReply: { d.editRulePage.busy = false; if (ruleError == "RuleErrorNoError") { + print("should tag rule now:", d.editRulePage.ruleIcon, d.editRulePage.ruleColor) + Engine.tagsManager.tagRule(d.editRulePage.rule.id, "color", d.editRulePage.ruleColor) + Engine.tagsManager.tagRule(d.editRulePage.rule.id, "icon", d.editRulePage.ruleIcon) pageStack.pop(); } else { var popup = errorDialog.createObject(app, {errorCode: ruleError }) @@ -64,19 +70,32 @@ Page { ListView { anchors.fill: parent - model: Engine.ruleManager.rules + model: RulesFilterModel { + id: rulesProxy + rules: Engine.ruleManager.rules + } delegate: MeaListItemDelegate { id: ruleDelegate width: parent.width - iconName: "../images/magic.svg" - iconColor: !model.enabled ? "red" : (model.active ? app.guhAccent : "grey") + iconName: "../images/" + (model.executable ? (iconTag ? iconTag.value : "slideshow") : "magic") + ".svg" + iconColor: model.executable ? (colorTag ? colorTag.value : app.guhAccent) : !model.enabled ? "red" : (model.active ? app.guhAccent : "grey") text: model.name canDelete: true + property var colorTag: model.executable ? Engine.tagsManager.tags.findRuleTag(model.id, "color") : null + property var iconTag: model.executable ? Engine.tagsManager.tags.findRuleTag(model.id, "icon") : null + Connections { + target: Engine.tagsManager.tags + onCountChanged: { + colorTag = Engine.tagsManager.tags.findRuleTag(model.id, "color") + iconTag = Engine.tagsManager.tags.findRuleTag(model.id, "icon") + } + } + onDeleteClicked: Engine.ruleManager.removeRule(model.id) onClicked: { - var newRule = Engine.ruleManager.rules.get(index).clone(); + var newRule = rulesProxy.get(index).clone(); d.editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: newRule}) d.editRulePage.StackView.onRemoved.connect(function() { newRule.destroy(); @@ -92,37 +111,16 @@ Page { } } - ColumnLayout { + EmptyViewPlaceholder { anchors { left: parent.left; right: parent.right; margins: app.margins } anchors.verticalCenter: parent.verticalCenter - spacing: app.margins * 2 - visible: Engine.ruleManager.rules.count === 0 - Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: qsTr("There is no magic set up yet.") - wrapMode: Text.WordWrap - color: app.guhAccent - font.pixelSize: app.largeFont - } - Label { - text: qsTr("Add some using the wizard stick!") - Layout.fillWidth: true - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - AbstractButton { - Layout.preferredHeight: app.iconSize * 4 - Layout.preferredWidth: height - Layout.alignment: Qt.AlignHCenter - - ColorIcon { - anchors.fill: parent - name: "../images/magic.svg" - } - - onClicked: addRule() - } + visible: Engine.ruleManager.rules.count === 0 + title: qsTr("There is no magic set up yet.") + text: qsTr("Use magic to make your things smart! In a few easy steps you'll have your things wired up and work for you.") + imageSource: "images/magic.svg" + buttonText: qsTr("Add some magic") + onImageClicked: addRule() + onButtonClicked: addRule() } Component { diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 807c504c..a8eb227d 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -1,16 +1,17 @@ import QtQuick 2.8 -import QtQuick.Controls 2.1 +import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 import Nymea 1.0 import "components" import "delegates" +import "mainviews" Page { id: root header: GuhHeader { - text: qsTr("My things") + text: swipeView.currentItem.title backButtonVisible: false menuButtonVisible: true onMenuPressed: mainMenu.open() @@ -36,72 +37,134 @@ Page { // } // } - Menu { + AutoSizeMenu { id: mainMenu - width: implicitWidth + app.margins IconMenuItem { iconSource: "../images/share.svg" text: qsTr("Configure things") + width: parent.width onTriggered: pageStack.push(Qt.resolvedUrl("EditDevicesPage.qml")) } -// MenuSeparator {} IconMenuItem { iconSource: "../images/magic.svg" text: qsTr("Magic") + width: parent.width onTriggered: pageStack.push(Qt.resolvedUrl("MagicPage.qml")) } - MenuSeparator {} + MenuSeparator { width: parent.width } IconMenuItem { iconSource: "../images/settings.svg" text: qsTr("System settings") + width: parent.width onTriggered: pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) } - MenuSeparator {} + MenuSeparator { width: parent.width } IconMenuItem { iconSource: "../images/stock_application.svg" text: qsTr("App settings") + width: parent.width onTriggered: pageStack.push(Qt.resolvedUrl("AppSettingsPage.qml")) } } - InterfacesModel { - id: page1Model - devices: Engine.deviceManager.devices - shownInterfaces: ["light", "weather", "sensor", "media", "garagegate", "shutter", "garagegate"] - property var view: null - onCountChanged: buildView() - } - InterfacesModel { - id: page2Model - devices: Engine.deviceManager.devices - shownInterfaces: ["gateway", "button", "notifications", "inputtrigger", "outputtrigger"] - property var view: null - onCountChanged: buildView() - } + ColumnLayout { + anchors.fill: parent - Component { - id: devicePageComponent - DevicesPage { - width: swipeView.width - height: swipeView.height - visible: count > 0 - } - } + Item { + Layout.fillWidth: true + Layout.fillHeight: true - Component { - id: allDevicesComponent - ListView { - width: swipeView.width - height: swipeView.height - model: DevicesProxy { - id: devicesProxy - devices: Engine.deviceManager.devices - } - delegate: ThingDelegate { - interfaces: model.interfaces - name: model.name - onClicked: { - pageStack.push(Qt.resolvedUrl("devicepages/GenericDevicePage.qml"), {device: devicesProxy.get(index)}) + SwipeView { + id: swipeView + clip: true + anchors.fill: parent + currentIndex: settings.currentMainViewIndex + onCurrentIndexChanged: settings.currentMainViewIndex = currentIndex + opacity: Engine.deviceManager.fetchingData ? 0 : 1 + Behavior on opacity { NumberAnimation { duration: 300 } } + + Component.onCompleted: { + if (Engine.jsonRpcClient.ensureServerVersion(1.6)) { + swipeView.insertItem(0, favoritesViewComponent.createObject(swipeView)) + } else if (settings.currentMainViewIndex === 2) { + settings.currentMainViewIndex = 0; + } + } + + Component { + id: favoritesViewComponent + FavoritesView { + id: favoritesView + width: swipeView.width + height: swipeView.height + property string title: qsTr("My favorites") + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: favoritesView.count === 0 && !Engine.deviceManager.fetchingData + title: qsTr("There are no favorite things yet.") + text: Engine.deviceManager.devices.count === 0 ? + qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") : + qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.") + imageSource: "images/starred.svg" + buttonVisible: Engine.deviceManager.devices.count === 0 + buttonText: qsTr("Add a thing") + onButtonClicked: pageStack.push(Qt.resolvedUrl("NewDeviceWizard.qml")) + } + + } + } + + DevicesPage { + property string title: qsTr("My things"); + width: swipeView.width + height: swipeView.height + model: InterfacesSortModel { + interfacesModel: InterfacesModel { + devices: Engine.deviceManager.devices + shownInterfaces: app.supportedInterfaces + } + } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: Engine.deviceManager.devices.count === 0 && !Engine.deviceManager.fetchingData + title: qsTr("Welcome to %1!").arg(app.systemName) + // Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice + text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 box to be useful, go ahead and add some things.").arg(app.systemName) + imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) + buttonText: qsTr("Add a thing") + onButtonClicked: pageStack.push(Qt.resolvedUrl("NewDeviceWizard.qml")) + } + } + + ScenesView { + id: scenesView + property string title: qsTr("My scenes"); + width: swipeView.width + height: swipeView.height + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: scenesView.count === 0 && !Engine.deviceManager.fetchingData + title: qsTr("There are no scenes set up yet") + text: Engine.deviceManager.devices.count === 0 ? + qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") : + qsTr("Scenes provide a useful way to control your things with just one click.") + imageSource: "images/slideshow.svg" + buttonText: Engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene") + onButtonClicked: { + if (Engine.deviceManager.devices.count === 0) { + pageStack.push(Qt.resolvedUrl("NewDeviceWizard.qml")) + } else { + var page = pageStack.push(Qt.resolvedUrl("MagicPage.qml")) + page.addRule() + } + } + } } } @@ -121,91 +184,36 @@ Page { horizontalAlignment: Text.AlignHCenter } } + } - ColumnLayout { - anchors { left: parent.left; right: parent.right; margins: app.margins } - anchors.verticalCenter: parent.verticalCenter - spacing: app.margins * 2 - visible: Engine.deviceManager.devices.count === 0 && !Engine.deviceManager.fetchingData - Label { - text: qsTr("Welcome to %1!").arg(app.systemName) - font.pixelSize: app.largeFont - Layout.fillWidth: true - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - color: app.guhAccent + TabBar { + id: tabBar + Layout.fillWidth: true + Material.elevation: 3 + currentIndex: settings.currentMainViewIndex + position: TabBar.Footer + Layout.preferredHeight: 70 + // FIXME: All this can go away when we require Controls 2.3 (Qt 5.10) or greater as TabBar got a major rework there. + // Ideally we'd just list the 3 items and set visible to false if the server version isn't good enough but TabBar + // has troubles dealing with that. For now, let's manually fill it and use a timer to initialize the currentIndex. + Component.onCompleted: { + var pi = 0; + if (Engine.jsonRpcClient.ensureServerVersion(1.6)) { + tabEntryComponent.createObject(tabBar, {text: qsTr("Favorites"), iconSource: "../images/starred.svg", pageIndex: pi++}) } - Label { - text: qsTr("There are no things set up yet. In order for your %1 box to be useful, go ahead and add some things.").arg(app.systemName) - Layout.fillWidth: true - Layout.maximumWidth: 400 - Layout.alignment: Qt.AlignHCenter - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - Image { - source: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) - Layout.preferredWidth: app.iconSize * 5 - Layout.preferredHeight: width - Layout.alignment: Qt.AlignHCenter - sourceSize.width: app.iconSize * 5 - sourceSize.height: app.iconSize * 5 - } - Button { - Layout.fillWidth: true - Layout.maximumWidth: 400 - Layout.alignment: Qt.AlignHCenter - text: qsTr("Add a thing") - onClicked: pageStack.push(Qt.resolvedUrl("NewDeviceWizard.qml")) + tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++}) + tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++}) + initTimer.start() + } + Timer { id: initTimer; interval: 1; repeat: false; onTriggered: tabBar.currentIndex = Qt.binding(function() {return settings.currentMainViewIndex;})} + + Component { + id: tabEntryComponent + MainPageTabButton { + property int pageIndex: 0 + onClicked: settings.currentMainViewIndex = pageIndex } } } } - - function buildView() { - var shownViews = [] - if (page1Model.count > 0) { - shownViews.push(0) - } - if (page2Model.count > 0) { - shownViews.push(1) - } - shownViews.push(2) - - if (swipeView.count === shownViews.length) { - return; - } - - while (swipeView.count > 0) { - swipeView.removeItem(0) - } - if (shownViews.indexOf(0) >= 0) { - swipeView.addItem(devicePageComponent.createObject(swipeView, {model: page1Model})) - } - if (shownViews.indexOf(1) >= 0) { - swipeView.addItem(devicePageComponent.createObject(swipeView, {model: page2Model})) - } - swipeView.addItem(allDevicesComponent.createObject(swipeView)) - } - - - ColumnLayout { - anchors.fill: parent - - SwipeView { - id: swipeView - Layout.fillWidth: true - Layout.fillHeight: true - currentIndex: pageIndicator.currentIndex - clip: true - } - - PageIndicator { - id: pageIndicator - Layout.alignment: Qt.AlignHCenter - count: swipeView.count - currentIndex: swipeView.currentIndex - interactive: true - } - } } diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 69d01f12..b40eafda 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -31,6 +31,7 @@ ApplicationWindow { property bool darkTheme: false property string graphStyle: "bars" property string style: "light" + property int currentMainViewIndex: 0 } Component.onCompleted: { @@ -103,6 +104,12 @@ ApplicationWindow { } } + // Workaround flickering on pageStack animations when the white background shines through + Rectangle { + anchors.fill: parent + color: Material.background + } + StackView { id: pageStack objectName: "pageStack" @@ -138,6 +145,7 @@ ApplicationWindow { } } + property var supportedInterfaces: ["light", "weather", "sensor", "media", "garagegate", "shutter", "garagegate", "button", "notifications", "inputtrigger", "outputtrigger", "gateway"] function interfaceToString(name) { switch(name) { case "light": @@ -168,6 +176,8 @@ ApplicationWindow { return qsTr("Blinds"); case "garagegate": return qsTr("Garage gates"); + case "uncategorized": + return qsTr("Uncategorized") } } @@ -222,6 +232,8 @@ ApplicationWindow { return Qt.resolvedUrl("images/shutter-10.svg") case "battery": return Qt.resolvedUrl("images/battery/battery-050.svg") + case "uncategorized": + return Qt.resolvedUrl("images/select-none.svg") } return ""; } @@ -236,6 +248,28 @@ ApplicationWindow { return "grey"; } + function interfaceListToDevicePage(interfaceList) { + var page; + if (interfaceList.indexOf("media") >= 0) { + page = "MediaDevicePage.qml"; + } else if (interfaceList.indexOf("button") >= 0) { + page = "ButtonDevicePage.qml"; + } else if (interfaceList.indexOf("weather") >= 0) { + page = "WeatherDevicePage.qml"; + } else if (interfaceList.indexOf("sensor") >= 0) { + page = "SensorDevicePage.qml"; + } else if (interfaceList.indexOf("inputtrigger") >= 0) { + page = "InputTriggerDevicePage.qml"; + } else if (interfaceList.indexOf("shutter") >= 0 ) { + page = "ShutterDevicePage.qml"; + } else if (interfaceList.indexOf("garagegate") >= 0 ) { + page = "GarageGateDevicePage.qml"; + } else { + page = "GenericDevicePage.qml"; + } + return page; + } + Component { id: invalidVersionComponent Popup { diff --git a/nymea-app/ui/components/AutoSizeMenu.qml b/nymea-app/ui/components/AutoSizeMenu.qml new file mode 100644 index 00000000..203cc973 --- /dev/null +++ b/nymea-app/ui/components/AutoSizeMenu.qml @@ -0,0 +1,17 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 + +Menu { + function calculateWidth() { + var result = 0; + var i = 0; + while (itemAt(i) !== null) { + result = Math.max(itemAt(i).contentItem.implicitWidth + app.margins * 2, result); + i++; + } + width = Math.min(parent.width, result + app.margins * 2); + + } + onAboutToShow: calculateWidth() + +} diff --git a/nymea-app/ui/components/EmptyViewPlaceholder.qml b/nymea-app/ui/components/EmptyViewPlaceholder.qml new file mode 100644 index 00000000..94bb823c --- /dev/null +++ b/nymea-app/ui/components/EmptyViewPlaceholder.qml @@ -0,0 +1,53 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +ColumnLayout { + id: root + spacing: app.margins * 2 + + property alias title: titleLabel.text + property alias text: textLabel.text + property alias imageSource: image.source + property alias buttonText: button.text + property alias buttonVisible: button.visible + + signal imageClicked(); + signal buttonClicked(); + + Label { + id: titleLabel + font.pixelSize: app.largeFont + Layout.fillWidth: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: app.guhAccent + } + Label { + id: textLabel + Layout.fillWidth: true + Layout.maximumWidth: 400 + Layout.alignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + Image { + id: image + Layout.preferredWidth: app.iconSize * 5 + Layout.preferredHeight: width + Layout.alignment: Qt.AlignHCenter + sourceSize.width: app.iconSize * 5 + sourceSize.height: app.iconSize * 5 + MouseArea { + anchors.fill: parent + onClicked: root.imageClicked(); + } + } + Button { + id: button + Layout.fillWidth: true + Layout.maximumWidth: 400 + Layout.alignment: Qt.AlignHCenter + onClicked: root.buttonClicked(); + } +} diff --git a/nymea-app/ui/components/MainPageTabButton.qml b/nymea-app/ui/components/MainPageTabButton.qml new file mode 100644 index 00000000..3156c893 --- /dev/null +++ b/nymea-app/ui/components/MainPageTabButton.qml @@ -0,0 +1,27 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.3 + +TabButton { + id: root + property string iconSource + + contentItem: ColumnLayout { + ColorIcon { + Layout.preferredWidth: app.iconSize + Layout.preferredHeight: app.iconSize + Layout.alignment: Qt.AlignHCenter + name: root.iconSource + color: root.checked ? app.guhAccent : keyColor + } + Label { + Layout.fillWidth: true + text: root.text + horizontalAlignment: Text.AlignHCenter + font.pixelSize: app.smallFont + color: root.checked ? app.guhAccent : Material.foreground + } + } +} + diff --git a/nymea-app/ui/devicelistpages/GenericDeviceListPage.qml b/nymea-app/ui/devicelistpages/GenericDeviceListPage.qml index d3c6c9f3..80df16f3 100644 --- a/nymea-app/ui/devicelistpages/GenericDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/GenericDeviceListPage.qml @@ -7,7 +7,8 @@ import "../delegates" Page { id: subPage - property alias filterInterface: devicesProxy.filterInterface + property alias shownInterfaces: devicesProxy.shownInterfaces + property alias hiddenInterfaces: devicesProxy.hiddenInterfaces Component.onCompleted: { if (devicesProxy.count == 1) { @@ -17,9 +18,12 @@ Page { header: GuhHeader { text: { - if (subPage.filterInterface.length > 0) { + if (subPage.shownInterfaces.length === 1) { return qsTr("My %1 things").arg(interfaceToString(subPage.filterInterface)) + } else if (subPage.shownInterfaces.length > 1) { + return qsTr("My things") } + return qsTr("All my things") } @@ -32,24 +36,7 @@ Page { function enterPage(index, replace) { var device = devicesProxy.get(index); var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); - var page; - if (deviceClass.interfaces.indexOf("media") >= 0) { - page = "MediaDevicePage.qml"; - } else if (deviceClass.interfaces.indexOf("button") >= 0) { - page = "ButtonDevicePage.qml"; - } else if (deviceClass.interfaces.indexOf("weather") >= 0) { - page = "WeatherDevicePage.qml"; - } else if (deviceClass.interfaces.indexOf("sensor") >= 0) { - page = "SensorDevicePage.qml"; - } else if (deviceClass.interfaces.indexOf("inputtrigger") >= 0) { - page = "InputTriggerDevicePage.qml"; - } else if (deviceClass.interfaces.indexOf("shutter") >= 0 ) { - page = "ShutterDevicePage.qml"; - } else if (deviceClass.interfaces.indexOf("garagegate") >= 0 ) { - page = "GarageGateDevicePage.qml"; - } else { - page = "GenericDevicePage.qml"; - } + var page = app.interfaceListToDevicePage(deviceClass.interfaces); if (replace) { pageStack.replace(Qt.resolvedUrl("../devicepages/" + page), {device: devicesProxy.get(index)}) } else { diff --git a/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml b/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml index df18da14..c2e41da3 100644 --- a/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/LightsDeviceListPage.qml @@ -5,7 +5,7 @@ import Nymea 1.0 import "../components" Page { - property alias filterInterface: devicesProxy.filterInterface + property alias shownInterfaces: devicesProxy.shownInterfaces header: GuhHeader { text: qsTr("Lights") onBackPressed: pageStack.pop() @@ -48,6 +48,7 @@ Page { } delegate: ItemDelegate { + id: itemDelegate width: parent.width height: childrenRect.height property var device: devicesProxy.get(index); @@ -67,9 +68,9 @@ Page { ThrottledSlider { id: inlineSlider visible: model.interfaces.indexOf("dimmablelight") >= 0 && parent.width > 350 - property var stateType: deviceClass.stateTypes.findByName("brightness"); - property var actionType: deviceClass.actionTypes.findByName("brightness"); - property var actionState: device.states.getState(stateType.id) + property var stateType: itemDelegate.deviceClass.stateTypes.findByName("brightness"); + property var actionType: itemDelegate.deviceClass.actionTypes.findByName("brightness"); + property var actionState: itemDelegate.device.states.getState(stateType.id) from: 0; to: 100 value: actionState.value onMoved: { @@ -82,9 +83,9 @@ Page { } } Switch { - property var stateType: deviceClass.stateTypes.findByName("power"); - property var actionType: deviceClass.actionTypes.findByName("power"); - property var actionState: device.states.getState(stateType.id) + property var stateType: itemDelegate.deviceClass.stateTypes.findByName("power"); + property var actionType: itemDelegate.deviceClass.actionTypes.findByName("power"); + property var actionState: itemDelegate.device.states.getState(stateType.id) checked: actionState.value === true onClicked: { var params = []; diff --git a/nymea-app/ui/devicepages/DevicePageBase.qml b/nymea-app/ui/devicepages/DevicePageBase.qml index 612c22c5..20dfc825 100644 --- a/nymea-app/ui/devicepages/DevicePageBase.qml +++ b/nymea-app/ui/devicepages/DevicePageBase.qml @@ -16,13 +16,55 @@ Page { onBackPressed: pageStack.pop() HeaderButton { - imageSource: "../images/magic.svg" - onClicked: pageStack.push(Qt.resolvedUrl("../magic/DeviceRulesPage.qml"), {device: root.device}) + imageSource: "../images/navigation-menu.svg" + onClicked: thingMenu.open(); + } + } + + TagsProxyModel { + id: favoritesProxy + filterDeviceId: root.device.id + filterTagId: "favorites" + } + + AutoSizeMenu { + id: thingMenu + x: parent.width - width + + 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 (Engine.jsonRpcClient.ensureServerVersion(1.6)) { + thingMenu.addItem(menuEntryComponent.createObject(thingMenu, + { + text: Qt.binding(function() { return favoritesProxy.count === 0 ? qsTr("Mark as favorite") : qsTr("Remove from favorites")}), + iconSource: Qt.binding(function() { return favoritesProxy.count === 0 ? "../images/starred.svg" : "../images/non-starred.svg"}), + functionName: "toggleFavorite" + })) + } + } + function openDeviceMagicPage() { + pageStack.push(Qt.resolvedUrl("../magic/DeviceRulesPage.qml"), {device: root.device}) + } + function openDeviceInfoPage() { + pageStack.push(Qt.resolvedUrl("GenericDeviceStateDetailsPage.qml"), {device: root.device}) + } + function toggleFavorite() { + if (favoritesProxy.count === 0) { + Engine.tagsManager.tagDevice(root.device.id, "favorites", 100000) + } else { + Engine.tagsManager.untagDevice(root.device.id, "favorites") + } } - HeaderButton { - imageSource: "../images/info.svg" - onClicked: pageStack.push(Qt.resolvedUrl("GenericDeviceStateDetailsPage.qml"), {device: root.device}) + Component { + id: menuEntryComponent + IconMenuItem { + width: parent.width + property string functionName: "" + onTriggered: thingMenu[functionName]() + } } } diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 682222b4..58a4ba27 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -5,22 +5,8 @@ import Nymea 1.0 import "../components" import "../customviews" -Page { +DevicePageBase { id: root - property var device: null - readonly property var deviceClass: Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) - - - header: GuhHeader { - text: device.name - onBackPressed: pageStack.pop() - - HeaderButton { - imageSource: "../images/info.svg" - onClicked: pageStack.push(Qt.resolvedUrl("GenericDeviceStateDetailsPage.qml"), {device: root.device}) - } - } - ColumnLayout { id: contentColumn diff --git a/nymea-app/ui/images/magic.svg.2018_05_29_14_21_27.0.svg b/nymea-app/ui/images/magic.svg.2018_05_29_14_21_27.0.svg deleted file mode 100644 index d8ce6570..00000000 --- a/nymea-app/ui/images/magic.svg.2018_05_29_14_21_27.0.svg +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nymea-app/ui/images/non-starred.svg b/nymea-app/ui/images/non-starred.svg new file mode 100644 index 00000000..6297b86a --- /dev/null +++ b/nymea-app/ui/images/non-starred.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/slideshow.svg b/nymea-app/ui/images/slideshow.svg new file mode 100644 index 00000000..7f423bec --- /dev/null +++ b/nymea-app/ui/images/slideshow.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/starred.svg b/nymea-app/ui/images/starred.svg new file mode 100644 index 00000000..fa4fc238 --- /dev/null +++ b/nymea-app/ui/images/starred.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/magic/EditRulePage.qml b/nymea-app/ui/magic/EditRulePage.qml index 5c5e8981..0033ad9c 100644 --- a/nymea-app/ui/magic/EditRulePage.qml +++ b/nymea-app/ui/magic/EditRulePage.qml @@ -12,10 +12,14 @@ Page { readonly property bool isEventBased: rule.eventDescriptors.count > 0 || rule.timeDescriptor.timeEventItems.count > 0 readonly property bool isStateBased: (rule.stateEvaluator !== null || rule.timeDescriptor.calendarItems.count > 0) && !isEventBased - readonly property bool actionsVisible: !isEmpty + readonly property bool actionsVisible: true readonly property bool exitActionsVisible: actionsVisible && isStateBased + readonly property bool hasActions: rule.actions.count > 0 readonly property bool hasExitActions: rule.exitActions.count > 0 - readonly property bool isEmpty: !isEventBased && !isStateBased + readonly property bool isEmpty: !isEventBased && !isStateBased && !hasActions + + property string ruleIcon: Engine.tagsManager.tags.findRuleTag(rule.id, "icon").value + property string ruleColor: Engine.tagsManager.tags.findRuleTag(rule.id, "color").value signal accept(); signal cancel(); @@ -223,6 +227,7 @@ Page { Layout.leftMargin: app.margins Layout.rightMargin: app.margins Layout.topMargin: app.margins + visible: !root.isEmpty property bool showDetails: false @@ -268,6 +273,101 @@ Page { } } } + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: ruleSettings.showDetails ? implicitHeight : 0 + opacity: ruleSettings.showDetails ? 1 : 0 + Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad} } + Behavior on opacity { NumberAnimation {duration: 200; easing.type: Easing.InOutQuad } } + Label { + Layout.fillWidth: true + text: qsTr("This is a scene" + root.ruleColor, root.ruleIcon) + } + + CheckBox { + checked: root.rule.executable + onClicked: { + root.rule.executable = checked + } + } + } + + GridLayout { + id: colorsGrid + Layout.fillWidth: true + columns: (root.width / 10 < app.iconSize + app.margins) ? 5 : 10 + columnSpacing: app.margins + rowSpacing: app.margins + Layout.preferredHeight: opacity > 0 ? implicitHeight : 0 + opacity: Engine.jsonRpcClient.ensureServerVersion(1.6) && ruleSettings.showDetails && root.rule.executable ? 1 : 0 + Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad} } + Behavior on opacity { NumberAnimation {duration: 200; easing.type: Easing.InOutQuad } } + + Repeater { + model: ["red", "orange", "yellow", "lime", "green", "aqua", "skyblue", "blue", "magenta", "purple"] + + delegate: Item { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + app.margins + Rectangle { + height: parent.height + width: height + color: "transparent" + border.width: 2 + border.color: modelData === root.ruleColor ? app.guhAccent : "transparent" + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + onClicked: { + root.ruleColor = modelData + } + } + + ColorIcon { + height: app.iconSize + width: app.iconSize + color: modelData + name: "../images/" + (root.ruleIcon ? root.ruleIcon : "slideshow") + ".svg" + anchors.centerIn: parent + } + } + } + Repeater { + model: ["torch-on", "torch-off", "alarm-clock", "media-preview-start", "network-secure", "notification", "sensors", "shutter-10", "mail-mark-important", "eye"] + + delegate: Item { + Layout.fillWidth: true + Layout.preferredHeight: app.iconSize + app.margins + Rectangle { + height: parent.height + width: height + color: "transparent" + border.width: 2 + border.color: modelData === root.ruleIcon ? app.guhAccent : "transparent" + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + onClicked: { + root.ruleIcon = modelData + } + } + + ColorIcon { + height: app.iconSize + width: app.iconSize + color: root.ruleColor + name: "../images/" + modelData + ".svg" + anchors.centerIn: parent + } + } + } + + + } } ThinDivider { visible: !root.isStateBased } @@ -277,7 +377,7 @@ Page { Layout.margins: app.margins font.pixelSize: app.mediumFont wrapMode: Text.WordWrap - text: eventsRepeater.count === 0 && timeEventRepeater.count === 0 ? + text: eventsRepeater.count === 0 && timeEventRepeater.count === 0 && actionsRepeater.count === 0 ? qsTr("Execute actions when something happens.") : qsTr("When any of these events happen...") visible: !root.isStateBased @@ -425,9 +525,10 @@ Page { ThinDivider { visible: root.actionsVisible } Label { - text: root.isStateBased ? - (root.rule.stateEvaluator === 0 ? qsTr("...come true, execute those actions:") : qsTr("...comes true, execute those actions:")) : - qsTr("...execute those actions:") + text: root.isEmpty ? qsTr("Create a scene.") : + root.isStateBased ? + (root.rule.stateEvaluator === 0 ? qsTr("...come true, execute those actions:") : qsTr("...comes true, execute those actions:")) : + qsTr("...execute those actions:") font.pixelSize: app.mediumFont Layout.fillWidth: true Layout.margins: app.margins @@ -436,6 +537,17 @@ Page { font.bold: true } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + font.italic: true + text: qsTr("Just pick some actions which will be executed when the scene is activated. Scenes are like any other magic except they can also be activated manually.") + visible: root.isEmpty + } + Repeater { id: actionsRepeater model: root.actionsVisible ? root.rule.actions : null @@ -449,8 +561,12 @@ Page { Button { Layout.fillWidth: true Layout.margins: app.margins - text: actionsRepeater.count == 0 ? qsTr("Add an action...") : qsTr("Add another action...") + text: root.isEmpty ? qsTr("Configure...") : + actionsRepeater.count == 0 ? qsTr("Add an action...") : qsTr("Add another action...") onClicked: { + if (root.isEmpty) { + root.rule.executable = true; + } var page = pageStack.push(ruleActionQuestionPageComponent, {exitAction: false}); } visible: root.actionsVisible diff --git a/nymea-app/ui/magic/SelectActionPage.qml b/nymea-app/ui/magic/SelectActionPage.qml index 6475218d..425090fd 100644 --- a/nymea-app/ui/magic/SelectActionPage.qml +++ b/nymea-app/ui/magic/SelectActionPage.qml @@ -37,7 +37,7 @@ Page { Component.onCompleted: { actualModel.clear() for (var i = 0; i < actionModel.count; i++) { - ifaceFilterModel.filterInterface = actionModel.get(i).interfaceName; + ifaceFilterModel.shownInterfaces = [actionModel.get(i).interfaceName]; if (actionModel.get(i).interfaceName === "" || ifaceFilterModel.count > 0) { actualModel.append(actionModel.get(i)) } @@ -253,7 +253,7 @@ Page { model: DevicesProxy { id: lightsModel devices: Engine.deviceManager.devices - filterInterface: "light" + shownInterfaces: ["light"] } delegate: CheckDelegate { width: parent.width @@ -338,7 +338,7 @@ Page { model: DevicesProxy { id: notificationsModel devices: Engine.deviceManager.devices - filterInterface: "notifications" + shownInterfaces: ["notifications"] } delegate: CheckDelegate { width: parent.width diff --git a/nymea-app/ui/mainviews/DevicesPage.qml b/nymea-app/ui/mainviews/DevicesPage.qml new file mode 100644 index 00000000..fbaa2f5b --- /dev/null +++ b/nymea-app/ui/mainviews/DevicesPage.qml @@ -0,0 +1,33 @@ +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import Nymea 1.0 +import "../components" + +Item { + id: root + property alias count: interfacesGridView.count + property alias model: interfacesGridView.model + + GridView { + id: interfacesGridView + anchors.fill: parent + anchors.margins: app.margins / 2 + + readonly property int minTileWidth: 180 + readonly property int minTileHeight: 240 + readonly property int tilesPerRow: root.width / minTileWidth + + model: InterfacesModel { + id: interfacesModel + devices: Engine.deviceManager.devices + } + cellWidth: width / tilesPerRow + cellHeight: Math.max(cellWidth, minTileHeight) + delegate: DevicesPageDelegate { + width: interfacesGridView.cellWidth + height: interfacesGridView.cellHeight + } + } +} diff --git a/nymea-app/ui/mainviews/DevicesPageDelegate.qml b/nymea-app/ui/mainviews/DevicesPageDelegate.qml new file mode 100644 index 00000000..863e8bee --- /dev/null +++ b/nymea-app/ui/mainviews/DevicesPageDelegate.qml @@ -0,0 +1,264 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.2 +import Nymea 1.0 +import "../components" + +Item { + id: root + Pane { + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 1 + + Column { + width: parent.width + anchors.centerIn: parent + anchors.verticalCenterOffset: -app.iconSize + spacing: app.margins + ColorIcon { + height: app.iconSize * 2 + width: height + color: app.guhAccent + anchors.horizontalCenter: parent.horizontalCenter + name: interfaceToIcon(model.name) + } + + Label { + text: interfaceToString(model.name).toUpperCase() + width: parent.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + var page; + switch (model.name) { + case "light": + page = "LightsDeviceListPage.qml" + break; + default: + page = "GenericDeviceListPage.qml" + } + if (model.name === "uncategorized") { + pageStack.push(Qt.resolvedUrl("../devicelistpages/" + page), {hiddenInterfaces: app.supportedInterfaces}) + } else { + pageStack.push(Qt.resolvedUrl("../devicelistpages/" + page), {shownInterfaces: [model.name]}) + } + } + } + + DevicesProxy { + id: devicesProxy + devices: Engine.deviceManager.devices + shownInterfaces: [model.name] + } + } + + Item { + id: inlineControlPane + anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: app.margins / 2 } + height: app.iconSize + app.margins * 2 + Rectangle { + anchors.fill: parent + // color: app.guhAccent + color: "black" + opacity: .05 + } + + Loader { + id: inlineControlLoader + anchors { + fill: parent + leftMargin: app.margins + rightMargin: app.margins + topMargin: app.margins / 2 + bottomMargin: app.margins / 2 + } + sourceComponent: { + switch (model.name) { + case "sensor": + case "weather": + return labelComponent; + + case "light": + case "media": + case "garagegate": + case "shutter": + case "blind": + return buttonComponent + } + } + } + } + + Component { + id: buttonComponent + MouseArea { + onClicked: { + switch (model.name) { + case "light": + if (devicesProxy.count == 1) { + var device = devicesProxy.get(0); + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var stateType = deviceClass.stateTypes.findByName("power") + var actionType = deviceClass.actionTypes.findByName("power") + var params = []; + var param1 = {}; + param1["paramTypeId"] = actionType.paramTypes.get(0).id; + param1["value"] = !device.states.getState(stateType.id).value; + params.push(param1) + Engine.deviceManager.executeAction(device.id, actionType.id, params) + } else { + for (var i = 0; i < devicesProxy.count; i++) { + var device = devicesProxy.get(i); + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var actionType = deviceClass.actionTypes.findByName("power"); + + var params = []; + var param1 = {}; + param1["paramTypeId"] = actionType.paramTypes.get(0).id; + param1["value"] = false; + params.push(param1) + Engine.deviceManager.executeAction(device.id, actionType.id, params) + } + } + break; + case "media": + var device = devicesProxy.get(0) + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var stateType = deviceClass.stateTypes.findByName("playbackStatus"); + var state = device.states.getState(stateType.id) + + var actionName + switch (state.value) { + case "PLAYING": + actionName = "pause"; + break; + case "PAUSED": + actionName = "play"; + break; + } + var actionTypeId = deviceClass.actionTypes.findByName(actionName).id; + + print("executing", device, device.id, actionTypeId, actionName, deviceClass.actionTypes) + + Engine.deviceManager.executeAction(device.id, actionTypeId) + case "garagegate": + case "shutter": + case "blind": + for (var i = 0; i < devicesProxy.count; i++) { + var device = devicesProxy.get(i); + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var actionType = deviceClass.actionTypes.findByName("close"); + Engine.deviceManager.executeAction(device.id, actionType.id) + } + + } + } + + ColumnLayout { + anchors.fill: parent + + Label { + id: label + Layout.fillWidth: true + text: { + switch (model.name) { + case "media": + return devicesProxy.get(0).name; + case "light": + var count = 0; + for (var i = 0; i < devicesProxy.count; i++) { + var device = devicesProxy.get(i); + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var stateType = deviceClass.stateTypes.findByName("power") + if (device.states.getState(stateType.id).value === true) { + count++; + } + } + return count === 0 ? qsTr("All off") : qsTr("%1 on").arg(count) + case "garagegate": + var count = 0; + for (var i = 0; i < devicesProxy.count; i++) { + var device = devicesProxy.get(i); + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var stateType = deviceClass.stateTypes.findByName("state"); + if (device.states.getState(stateType.id).value !== "closed") { + count++; + } + } + return count === 0 ? qsTr("All closed") : qsTr("%1 open").arg(count) + case "shutter": + return qsTr("%1 installed").arg(devicesProxy.count) + } + console.warn("Unhandled interface", model.name) + } + font.pixelSize: app.smallFont + elide: Text.ElideRight + } + ColorIcon { + id: icon + width: app.largeFont + height: width + color: app.guhAccent + Layout.alignment: Qt.AlignRight + name: { + switch (model.name) { + case "media": + var device = devicesProxy.get(0) + var deviceClass = Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + var stateType = deviceClass.stateTypes.findByName("playbackStatus"); + var state = device.states.getState(stateType.id) + return state.value === "PLAYING" ? "../images/media-playback-pause.svg" : + state.value === "PAUSED" ? "../images/media-playback-start.svg" : + "" + case "light": + return "../images/system-shutdown.svg" + case "garagegate": + case "shutter": + case "blind": + return "../images/down.svg" + } + } + } + } + } + } + + Component { + id: labelComponent + ColumnLayout { + property var device: devicesProxy.get(0) + property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + property var state: deviceClass ? device.states.getState(deviceClass.stateTypes.findByName("temperature").id) : null + + Label { + text: parent.device.name + font.pixelSize: app.smallFont + Layout.fillWidth: true + elide: Text.ElideRight + } + + Label { + font.pixelSize: app.largeFont + color: app.guhAccent + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: { + if (devicesProxy.count > 0) { + var stateName; + // switch (model.name) { + // case "sensor": + // } + return parent.state.value + "°C"; + } + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/FavoritesView.qml b/nymea-app/ui/mainviews/FavoritesView.qml new file mode 100644 index 00000000..747b6550 --- /dev/null +++ b/nymea-app/ui/mainviews/FavoritesView.qml @@ -0,0 +1,146 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.2 +import Nymea 1.0 +import "../components" + +Item { + id: root + + readonly property int count: tagsProxy.count + + TagsProxyModel { + id: tagsProxy + filterTagId: "favorites" + } + + GridView { + id: gridView + anchors.fill: parent + anchors.margins: app.margins / 2 + readonly property int minTileWidth: 180 + readonly property int minTileHeight: 240 + readonly property int tilesPerRow: root.width / minTileWidth + cellWidth: gridView.width / tilesPerRow + cellHeight: cellWidth + + model: tagsProxy + delegate: favoritesDelegateComponent + MouseArea { + id: dndArea + anchors.fill: parent + propagateComposedEvents: true + + property int from: -1 + property int to: -1 + property int index: gridView.indexAt(mouseX, mouseY) // Item underneath cursor + property var dndDelegate: null + + property int dx + property int dy + + onPressAndHold: { + //currentId = icons.get(newIndex = index).gridId + preventStealing = true; + from = index; + print("pressandHold on", index) + var tag = tagsProxy.get(index); + var originalDelegate = gridView.itemAt(mouseX, mouseY) + dndDelegate = favoritesDelegateComponent.createObject(dndArea, {deviceId: tag.deviceId, ruleId: tag.ruleId, x: originalDelegate.x, y: originalDelegate.y}) + dx = mouseX - originalDelegate.x; + dy = mouseY - originalDelegate.y; + } + onReleased: { + preventStealing = false; + from = -1; + dndDelegate.destroy(); + } + + onPositionChanged: { + if (dndDelegate) { + dndDelegate.x = mouseX - dx + dndDelegate.y = mouseY - dy + } + + if (dndArea.from >= 0 && to != index && from != index) { + to = index; + print("should move", from, "to", to) + for (var i = 0; i < tagsProxy.count; i++) { + if (i < Math.min(from, to) || i > Math.max(from, to)) { + // outside the range... don't touch + continue; + } + var newIdx; + if (i == from) { + newIdx = to; + } else { + if (from < to) { + // item is moved down the list + newIdx = i - 1; + } else { + newIdx = i + 1; + } + } + + var tag = tagsProxy.get(i); + Engine.tagsManager.tagDevice(tag.deviceId, tag.tagId, newIdx); + } + from = index; + } + // tagsProxy.move(newIndex, newIndex = index, 1) + } + } + + + } + + Component { + id: favoritesDelegateComponent + Item { + id: delegateRoot + property string deviceId: model.deviceId + property string ruleId: model.ruleId + readonly property var device: Engine.deviceManager.devices.getDevice(deviceId) + readonly property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + + visible: index !== undefined && index !== dndArea.from + width: gridView.cellWidth + height: gridView.cellHeight + + Pane { + id: pane + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 1 + ColumnLayout { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + spacing: app.margins + + ColorIcon { + Layout.preferredWidth: app.iconSize * 2 + Layout.preferredHeight: width + name: app.interfacesToIcon(delegateRoot.deviceClass.interfaces) + color: app.guhAccent + Layout.alignment: Qt.AlignHCenter + } + + Label { + Layout.fillWidth: true + text: delegateRoot.device.name + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + pageStack.push(Qt.resolvedUrl("../devicepages/" + app.interfaceListToDevicePage(delegateRoot.deviceClass.interfaces)), {device: delegateRoot.device}) + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/ScenesView.qml b/nymea-app/ui/mainviews/ScenesView.qml new file mode 100644 index 00000000..44e2691a --- /dev/null +++ b/nymea-app/ui/mainviews/ScenesView.qml @@ -0,0 +1,79 @@ +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: interfacesGridView.count + + GridView { + id: interfacesGridView + 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: RulesFilterModel { + rules: Engine.ruleManager.rules + filterExecutable: true + } + cellWidth: width / tilesPerRow + cellHeight: Math.max(cellWidth, minTileHeight) + delegate: Item { + id: scenesDelegate + width: interfacesGridView.cellWidth + height: interfacesGridView.cellHeight + + property var colorTag: Engine.tagsManager.tags.findRuleTag(model.id, "color") + property var iconTag: Engine.tagsManager.tags.findRuleTag(model.id, "icon") + Connections { + target: Engine.tagsManager.tags + onCountChanged: { + colorTag = Engine.tagsManager.tags.findRuleTag(model.id, "color") + iconTag = Engine.tagsManager.tags.findRuleTag(model.id, "icon") + } + } + + Pane { + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 1 + + MouseArea { + anchors.fill: parent + onClicked: { + Engine.ruleManager.executeActions(model.id) + } + } + + ColumnLayout { + width: parent.width + anchors.centerIn: parent + spacing: app.margins + + ColorIcon { + Layout.preferredHeight: app.iconSize * 2 + Layout.preferredWidth: height + Layout.alignment: Qt.AlignHCenter + name: scenesDelegate.iconTag ? "../images/" + scenesDelegate.iconTag.value + ".svg" : "../images/slideshow.svg"; + color: scenesDelegate.colorTag ? scenesDelegate.colorTag.value : app.guhAccent; + } + + Label { + Layout.fillWidth: true + text: model.name + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + +}