From 652f115b7c25bcbec2094c3c6942da00732a365f Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 28 Feb 2021 22:12:11 +0100 Subject: [PATCH] Add a dashboard view --- libnymea-app/appdata.cpp | 164 ++++++ libnymea-app/appdata.h | 57 +++ libnymea-app/connection/awsclient.cpp | 54 +- libnymea-app/connection/awsclient.h | 9 +- libnymea-app/libnymea-app-core.h | 3 + libnymea-app/libnymea-app.pri | 2 + libnymea-app/models/logsmodelng.cpp | 4 + libnymea-app/rulemanager.cpp | 9 + libnymea-app/rulemanager.h | 4 + libnymea-app/tagsmanager.cpp | 21 +- libnymea-app/tagsmanager.h | 16 +- nymea-app/dashboard/dashboarditem.cpp | 165 ++++++ nymea-app/dashboard/dashboarditem.h | 113 +++++ nymea-app/dashboard/dashboardmodel.cpp | 223 ++++++++ nymea-app/dashboard/dashboardmodel.h | 54 ++ nymea-app/images.qrc | 6 +- nymea-app/main.cpp | 13 + nymea-app/mouseobserver.cpp | 37 ++ nymea-app/mouseobserver.h | 36 ++ nymea-app/nymea-app.pro | 8 + nymea-app/resources.qrc | 11 + nymea-app/ui/MainPage.qml | 135 +++-- nymea-app/ui/StyleBase.qml | 4 +- nymea-app/ui/components/BigTile.qml | 1 + nymea-app/ui/components/MainPageTile.qml | 24 +- nymea-app/ui/components/MainViewBase.qml | 2 + nymea-app/ui/components/MeaDialog.qml | 5 +- nymea-app/ui/components/MediaPlayer.qml | 2 +- nymea-app/ui/components/ProgressButton.qml | 2 + nymea-app/ui/components/SelectionTabs.qml | 53 ++ nymea-app/ui/components/ThingContextMenu.qml | 4 +- nymea-app/ui/customviews/GenericTypeGraph.qml | 43 +- nymea-app/ui/delegates/ThingTile.qml | 22 +- .../ui/devicepages/CleaningRobotThingPage.qml | 2 +- nymea-app/ui/devicepages/ThingPageBase.qml | 2 +- nymea-app/ui/images/chart.svg | 72 +++ nymea-app/ui/images/dashboard.svg | 187 +++++++ .../{folder-symbolic.svg => folder.svg} | 0 .../{view-grid-symbolic.svg => groups.svg} | 0 nymea-app/ui/mainviews/DashboardView.qml | 90 ++++ nymea-app/ui/mainviews/FavoritesView.qml | 165 ++++-- nymea-app/ui/mainviews/GroupsView.qml | 6 +- nymea-app/ui/mainviews/ScenesView.qml | 2 +- .../ui/mainviews/dashboard/Dashboard.qml | 477 ++++++++++++++++++ .../dashboard/DashboardAddWizard.qml | 400 +++++++++++++++ .../dashboard/DashboardDelegateBase.qml | 60 +++ .../dashboard/DashboardFolderDelegate.qml | 117 +++++ .../dashboard/DashboardGraphDelegate.qml | 63 +++ .../ui/mainviews/dashboard/DashboardPage.qml | 60 +++ .../dashboard/DashboardSceneDelegate.qml | 68 +++ .../dashboard/DashboardThingDelegate.qml | 53 ++ .../dashboard/DashboardWebViewDelegate.qml | 176 +++++++ .../thingconfiguration/ConfigureThingPage.qml | 2 +- nymea-app/ui/utils/NymeaUtils.qml | 46 ++ 54 files changed, 3161 insertions(+), 193 deletions(-) create mode 100644 libnymea-app/appdata.cpp create mode 100644 libnymea-app/appdata.h create mode 100644 nymea-app/dashboard/dashboarditem.cpp create mode 100644 nymea-app/dashboard/dashboarditem.h create mode 100644 nymea-app/dashboard/dashboardmodel.cpp create mode 100644 nymea-app/dashboard/dashboardmodel.h create mode 100644 nymea-app/mouseobserver.cpp create mode 100644 nymea-app/mouseobserver.h create mode 100644 nymea-app/ui/components/SelectionTabs.qml create mode 100644 nymea-app/ui/images/chart.svg create mode 100644 nymea-app/ui/images/dashboard.svg rename nymea-app/ui/images/{folder-symbolic.svg => folder.svg} (100%) rename nymea-app/ui/images/{view-grid-symbolic.svg => groups.svg} (100%) create mode 100644 nymea-app/ui/mainviews/DashboardView.qml create mode 100644 nymea-app/ui/mainviews/dashboard/Dashboard.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardDelegateBase.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardGraphDelegate.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardPage.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardSceneDelegate.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardThingDelegate.qml create mode 100644 nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml diff --git a/libnymea-app/appdata.cpp b/libnymea-app/appdata.cpp new file mode 100644 index 00000000..19b57923 --- /dev/null +++ b/libnymea-app/appdata.cpp @@ -0,0 +1,164 @@ +#include "appdata.h" + +#include "engine.h" + +#include + +#include "config.h" +#include "logging.h" +NYMEA_LOGGING_CATEGORY(dcAppData, "AppData") + +AppData::AppData(QObject *parent) : JsonHandler(parent) +{ + m_syncTimer.setSingleShot(true); + connect(&m_syncTimer, &QTimer::timeout, this, &AppData::store); +} + +AppData::~AppData() +{ + if (m_engine && m_syncTimer.isActive()) { + store(); + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } +} + +void AppData::classBegin() +{ + for (int i = 0; i < metaObject()->propertyCount(); i++) { + qCDebug(dcAppData) << "ClassBegin property:" << metaObject()->property(i).name(); + } +} + +void AppData::componentComplete() +{ + // setup change notifications + for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) { + QMetaProperty prop = metaObject()->property(i); + if (prop.hasNotifySignal()) { + static const int propertyChangedIndex = metaObject()->indexOfSlot("onPropertyChanged()"); + QMetaObject::connect(this, prop.notifySignalIndex(), this, propertyChangedIndex); + } + } + + load(); +} + +QString AppData::nameSpace() const +{ + return "AppData"; +} + +Engine *AppData::engine() const +{ + return m_engine; +} + +void AppData::setEngine(Engine *engine) +{ + if (m_engine == engine) { + return; + } + + if (m_engine) { + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } + + m_engine = engine; + + if (m_engine) { + m_engine->jsonRpcClient()->registerNotificationHandler(this, "notificationReceived"); + } + emit engineChanged(); +} + +QString AppData::group() const +{ + return m_group; +} + +void AppData::setGroup(const QString &group) +{ + if (m_group != group) { + if (m_syncTimer.isActive()) { + m_syncTimer.stop(); + store(); + } + m_group = group; + load(); + } +} + +void AppData::load() +{ + if (!m_engine) { + return; + } + + for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) { + QMetaProperty prop = metaObject()->property(i); + qCDebug(dcAppData) << "ComponentComplete property:" << prop.name() << prop.isUser() << prop.type() << prop.isScriptable(this) << prop.isScriptable(); + QVariantMap params; + params.insert("appId", APPLICATION_NAME); + if (!m_group.isEmpty()) { + params.insert("group", m_group); + } + params.insert("key", prop.name()); + int id = m_engine->jsonRpcClient()->sendCommand("AppData.Load", params, this, "appDataReceived"); + m_readRequests.insert(id, prop.name()); + + } +} + +void AppData::store() +{ + if (!m_engine) { + return; + } + + for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) { + QMetaProperty prop = metaObject()->property(i); + QVariantMap params; + params.insert("appId", APPLICATION_NAME); + params.insert("key", prop.name()); + if (!m_group.isEmpty()) { + params.insert("group", m_group); + } + params.insert("value", prop.read(this)); + m_engine->jsonRpcClient()->sendCommand("AppData.Store", params, this, "appDataWritten"); + } + +} + +void AppData::onPropertyChanged() +{ + if (!m_loopLock) { + m_syncTimer.start(500); + } +} + +void AppData::appDataReceived(int commandId, const QVariantMap ¶ms) +{ + if (m_readRequests.contains(commandId)) { + QString propName = m_readRequests.take(commandId); + for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) { + QMetaProperty prop = metaObject()->property(i); + if (prop.name() == propName) { + m_loopLock = true; + prop.write(this, params.value("value").toString()); + m_loopLock = false; + return; + } + } + qCWarning(dcAppData()) << "Retrieved app data property does not exist" << propName; + } +} + +void AppData::appDataWritten(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcAppData()) << "App data written:" << commandId << params; +} + +void AppData::notificationReceived(const QVariantMap ¬ification) +{ + qCDebug(dcAppData()) << "AppData notification" << notification; +} diff --git a/libnymea-app/appdata.h b/libnymea-app/appdata.h new file mode 100644 index 00000000..bd36315e --- /dev/null +++ b/libnymea-app/appdata.h @@ -0,0 +1,57 @@ +#ifndef APPDATA_H +#define APPDATA_H + +#include "jsonrpc/jsonhandler.h" + +#include +#include + +class Engine; + +class AppData : public JsonHandler, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(QString group READ group WRITE setGroup NOTIFY groupChanged) + +public: + explicit AppData(QObject *parent = nullptr); + ~AppData() override; + + void classBegin() override; + void componentComplete() override; + + QString nameSpace() const override; + + Engine *engine() const; + void setEngine(Engine *engine); + + QString group() const; + void setGroup(const QString &group); + +signals: + void engineChanged(); + void groupChanged(); + +private slots: + void load(); + void store(); + + void onPropertyChanged(); + + void appDataReceived(int commandId, const QVariantMap ¶ms); + void appDataWritten(int commandId, const QVariantMap ¶ms); + + void notificationReceived(const QVariantMap ¬ification); +private: + Engine *m_engine = nullptr; + QTimer m_syncTimer; + QString m_group; + + bool m_loopLock = false; + QHash m_readRequests; + +}; + +#endif // APPDATA_H diff --git a/libnymea-app/connection/awsclient.cpp b/libnymea-app/connection/awsclient.cpp index f84a8419..bff4be97 100644 --- a/libnymea-app/connection/awsclient.cpp +++ b/libnymea-app/connection/awsclient.cpp @@ -188,15 +188,15 @@ bool AWSClient::confirmationPending() const return m_confirmationPending; } -void AWSClient::login(const QString &username, const QString &password) +bool AWSClient::login(const QString &username, const QString &password) { if (m_usedConfig.isEmpty()) { qCInfo(dcCloud()) << "AWS config not set. Not logging in."; - return; + return false; } if (m_loginInProgress) { qCDebug(dcCloud()) << "Login already pending..."; - return; + return false; } m_loginInProgress = true; @@ -243,16 +243,19 @@ void AWSClient::login(const QString &username, const QString &password) if (reply->error() == QNetworkReply::HostNotFoundError) { qCWarning(dcCloud()) << "Error logging in to aws due to network connection."; emit loginResult(LoginErrorNetworkError); + cancelCallQueue(); return; } if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { qCWarning(dcCloud()) << "Looks like a wrong password."; m_username.clear(); m_password.clear(); + cancelCallQueue(); emit loginResult(LoginErrorInvalidUserOrPass); return; } qCWarning(dcCloud()) << "Error logging in to aws. Error:" << reply->error() << reply->errorString(); + cancelCallQueue(); emit loginResult(LoginErrorUnknownError); return; } @@ -263,6 +266,7 @@ void AWSClient::login(const QString &username, const QString &password) qCWarning(dcCloud()) << "Failed to parse AWS login response" << error.errorString(); m_username.clear(); m_password.clear(); + cancelCallQueue(); emit loginResult(LoginErrorUnknownError); return; } @@ -277,6 +281,8 @@ void AWSClient::login(const QString &username, const QString &password) QList jwtParts = m_idToken.split('.'); if (jwtParts.count() != 3) { qCWarning(dcCloud()) << "Error: JWT token doesn't have 3 parts. Cannot retrieve AWS Cognito ID."; + cancelCallQueue(); + emit loginResult(LoginErrorUnknownError); return; } // qDebug() << "decoded header:" << QByteArray::fromBase64(jwtParts.at(0)); @@ -287,6 +293,7 @@ void AWSClient::login(const QString &username, const QString &password) // qDebug() << "Getting cognito ID"; getId(); }); + return true; } void AWSClient::logout() @@ -634,6 +641,7 @@ void AWSClient::getId() reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qCWarning(dcCloud()) << "Error calling GetId" << reply->error() << reply->errorString(); + cancelCallQueue(); return; } QByteArray data = reply->readAll(); @@ -641,6 +649,7 @@ void AWSClient::getId() QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qCWarning(dcCloud()) << "Error parsing json reply for GetId" << error.errorString(); + cancelCallQueue(); return; } m_identityId = jsonDoc.toVariant().toMap().value("IdentityId").toByteArray(); @@ -820,6 +829,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qCWarning(dcCloud()) << "Error calling GetCredentialsForIdentity" << reply->errorString(); + cancelCallQueue(); emit loginResult(LoginErrorUnknownError); return; } @@ -828,6 +838,7 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qCWarning(dcCloud()) << "Error parsing JSON reply from GetCredentialsForIdentity" << error.errorString(); + cancelCallQueue(); emit loginResult(LoginErrorUnknownError); return; } @@ -884,12 +895,25 @@ void AWSClient::getCredentialsForIdentity(const QString &identityId) }); } +void AWSClient::cancelCallQueue() +{ + while (!m_callQueue.isEmpty()) { + QueuedCall qc = m_callQueue.takeFirst(); + // Only postToMQTT needs calling a callback with error + if (qc.method == "postToMQTT") { + if (!qc.sender.isNull()) { + qc.callback(false); + } + } + } +} + bool AWSClient::tokensExpired() const { return (m_accessTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()) || (m_sessionTokenExpiry.addSecs(-10) < QDateTime::currentDateTime()); } -bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject* sender, std::function callback) +bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QPointer sender, std::function callback) { if (!isLoggedIn()) { qCWarning(dcCloud()) << "Cannot post to MQTT. Not logged in to AWS"; @@ -899,12 +923,10 @@ bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject* qCDebug(dcCloud()) << "Cannot post to MQTT. Need to refresh the tokens first"; refreshAccessToken(); QueuedCall::enqueue(m_callQueue, QueuedCall("postToMQTT", coreId, nonce, sender, callback)); - return true; // So far it looks we're doing ok... let's return true + return true; // Pretending we're doing fine } QString topic = QString("%1/%2/proxy").arg(coreId).arg(QString(m_identityId)); - QPointer senderWatcher = QPointer(sender); - // This is somehow broken in AWS... // The Signature needs to be created with having the topic percentage-encoded twice // while the actual request needs to go out with it only being encoded once. @@ -938,18 +960,18 @@ bool AWSClient::postToMQTT(const QString &coreId, const QString &nonce, QObject* // } qCDebug(dcCloud) << "Payload:" << payload; QNetworkReply *reply = m_nam->post(request, payload); - QTimer::singleShot(5000, reply, [reply, senderWatcher, callback](){ + QTimer::singleShot(5000, reply, [reply, sender, callback](){ reply->deleteLater(); qCWarning(dcCloud) << "Timeout posting to MQTT"; - if (senderWatcher) { + if (!sender.isNull()) { callback(false); } }); - connect(reply, &QNetworkReply::finished, this, [reply, senderWatcher, callback]() { + connect(reply, &QNetworkReply::finished, this, [reply, sender, callback]() { reply->deleteLater(); QByteArray data = reply->readAll(); - qCDebug(dcCloud()) << "MQTT post reply" << data; - if (senderWatcher.isNull()) { +// qDebug() << "MQTT post reply" << data; + if (sender.isNull()) { qCDebug(dcCloud()) << "Request object disappeared. Discarding MQTT reply..."; return; } @@ -1052,18 +1074,17 @@ void AWSClient::fetchDevices() }); } -void AWSClient::refreshAccessToken() +bool AWSClient::refreshAccessToken() { if (!isLoggedIn()) { qCWarning(dcCloud()) << "Cannot refresh tokens. Not logged in to AWS"; - return; + return false; } // We should use REFRESH_TOKEN_AUTH to refresh our tokens but it's not working // https://forums.aws.amazon.com/thread.jspa?threadID=287978 // Let's re-login instead with user & pass - login(m_username, m_password); - return; + return login(m_username, m_password); // Non-working block... Enable this if Amazon ever fixes their API... @@ -1126,6 +1147,7 @@ void AWSClient::refreshAccessToken() emit isLoggedInChanged(); }); + return true; } diff --git a/libnymea-app/connection/awsclient.h b/libnymea-app/connection/awsclient.h index 9e911210..601d1bef 100644 --- a/libnymea-app/connection/awsclient.h +++ b/libnymea-app/connection/awsclient.h @@ -139,7 +139,7 @@ public: AWSDevices* awsDevices() const; bool confirmationPending() const; - Q_INVOKABLE void login(const QString &username, const QString &password); + Q_INVOKABLE bool login(const QString &username, const QString &password); Q_INVOKABLE void logout(); Q_INVOKABLE void signup(const QString &username, const QString &password); Q_INVOKABLE void confirmRegistration(const QString &code); @@ -151,7 +151,7 @@ public: Q_INVOKABLE void fetchDevices(); - Q_INVOKABLE bool postToMQTT(const QString &coreId, const QString &nonce, QObject* sender, std::function callback); + Q_INVOKABLE bool postToMQTT(const QString &coreId, const QString &nonce, QPointer sender, std::function callback); Q_INVOKABLE void getId(); Q_INVOKABLE void registerPushNotificationEndpoint(const QString ®istrationId, const QString &deviceDisplayName, const QString mobileDeviceId, const QString &mobileDeviceManufacturer, const QString &mobileDeviceModel); @@ -185,7 +185,7 @@ private: explicit AWSClient(QObject *parent = nullptr); static AWSClient* s_instance; - void refreshAccessToken(); + bool refreshAccessToken(); void getCredentialsForIdentity(const QString &identityId); void connectMQTT(); @@ -218,7 +218,7 @@ private: QueuedCall(const QString &method): method(method) { } QueuedCall(const QString &method, const QString &arg1): method(method), arg1(arg1) { } QueuedCall(const QString &method, const QString &arg1, const QString &arg2, const QString &arg3, const QString &arg4, const QString &arg5): method(method), arg1(arg1), arg2(arg2), arg3(arg3), arg4(arg4), arg5(arg5) { } - QueuedCall(const QString &method, const QString &arg1, const QString &arg2, QObject* sender, std::function callback): method(method), arg1(arg1), arg2(arg2), sender(sender), callback(callback) {} + QueuedCall(const QString &method, const QString &arg1, const QString &arg2, QPointer sender, std::function callback): method(method), arg1(arg1), arg2(arg2), sender(sender), callback(callback) {} QString method; QString arg1; QString arg2; @@ -241,6 +241,7 @@ private: queue.append(call); } }; + void cancelCallQueue(); QList m_callQueue; diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index 89a2d782..af5ad46e 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -129,6 +129,7 @@ #include "zigbee/zigbeenetworks.h" #include "applogcontroller.h" #include "tagwatcher.h" +#include "appdata.h" #include @@ -338,6 +339,8 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "IOInputConnectionWatcher"); qmlRegisterType(uri, 1, 0, "IOOutputConnectionWatcher"); + qmlRegisterType(uri, 1, 0, "AppData"); + qmlRegisterType(uri, 1, 0, "SortFilterProxyModel"); } diff --git a/libnymea-app/libnymea-app.pri b/libnymea-app/libnymea-app.pri index ff45df9b..7be3a0f3 100644 --- a/libnymea-app/libnymea-app.pri +++ b/libnymea-app/libnymea-app.pri @@ -20,6 +20,7 @@ INCLUDEPATH += \ $$top_srcdir/QtZeroConf SOURCES += \ + $$PWD/appdata.cpp \ $$PWD/models/scriptsproxymodel.cpp \ $$PWD/tagwatcher.cpp \ $${PWD}/logging.cpp \ @@ -167,6 +168,7 @@ SOURCES += \ HEADERS += \ + $$PWD/appdata.h \ $$PWD/models/scriptsproxymodel.h \ $$PWD/tagwatcher.h \ $${PWD}/logging.h \ diff --git a/libnymea-app/models/logsmodelng.cpp b/libnymea-app/models/logsmodelng.cpp index 47955b29..c62a653b 100644 --- a/libnymea-app/models/logsmodelng.cpp +++ b/libnymea-app/models/logsmodelng.cpp @@ -287,6 +287,10 @@ void LogsModelNg::logsReply(int commandId, const QVariantMap &data) } StateType *entryStateType = thing->thingClass()->stateTypes()->getStateType(entry->typeId()); + if (!entryStateType) { + qWarning() << "StateType" << entry->typeId() << "not found on thing" << thing->name(); + continue; + } if (m_graphSeries) { if (entryStateType->type().toLower() == "bool") { diff --git a/libnymea-app/rulemanager.cpp b/libnymea-app/rulemanager.cpp index 6e019aee..4ed85d36 100644 --- a/libnymea-app/rulemanager.cpp +++ b/libnymea-app/rulemanager.cpp @@ -74,9 +74,16 @@ void RuleManager::clear() void RuleManager::init() { + m_fetchingData = true; + emit fetchingDataChanged(); m_jsonClient->sendCommand("Rules.GetRules", this, "getRulesReply"); } +bool RuleManager::fetchingData() const +{ + return m_fetchingData; +} + Rules *RuleManager::rules() const { return m_rules; @@ -176,6 +183,8 @@ void RuleManager::getRulesReply(int /*commandId*/, const QVariantMap ¶ms) requestParams.insert("ruleId", rule->id()); m_jsonClient->sendCommand("Rules.GetRuleDetails", requestParams, this, "getRuleDetailsReply"); } + m_fetchingData = false; + emit fetchingDataChanged(); } void RuleManager::getRuleDetailsReply(int commandId, const QVariantMap ¶ms) diff --git a/libnymea-app/rulemanager.h b/libnymea-app/rulemanager.h index f76d4f4c..f3309e6f 100644 --- a/libnymea-app/rulemanager.h +++ b/libnymea-app/rulemanager.h @@ -50,6 +50,7 @@ class RuleManager : public JsonHandler { Q_OBJECT Q_PROPERTY(Rules* rules READ rules CONSTANT) + Q_PROPERTY(bool fetchingData READ fetchingData NOTIFY fetchingDataChanged) public: explicit RuleManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); @@ -58,6 +59,7 @@ public: void clear(); void init(); + bool fetchingData() const; Rules* rules() const; @@ -99,10 +101,12 @@ private: signals: void addRuleReply(int commandId, const QString &ruleError, const QString &ruleId); void editRuleReply(int commandId, const QString &ruleError); + void fetchingDataChanged(); private: JsonRpcClient *m_jsonClient; Rules* m_rules; + bool m_fetchingData = false; }; #endif // RULEMANAGER_H diff --git a/libnymea-app/tagsmanager.cpp b/libnymea-app/tagsmanager.cpp index 0620d956..be8f3959 100644 --- a/libnymea-app/tagsmanager.cpp +++ b/libnymea-app/tagsmanager.cpp @@ -33,6 +33,7 @@ #include "engine.h" #include +#include TagsManager::TagsManager(JsonRpcClient *jsonClient, QObject *parent): JsonHandler(parent), @@ -52,7 +53,7 @@ void TagsManager::init() m_busy = true; emit busyChanged(); m_tags->clear(); - m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply"); + m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsResponse"); } void TagsManager::clear() @@ -79,7 +80,7 @@ int TagsManager::tagThing(const QString &thingId, const QString &tagId, const QS tag.insert("tagId", tagId); tag.insert("value", value); params.insert("tag", tag); - return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagReply"); + return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagResponse"); } int TagsManager::untagThing(const QString &thingId, const QString &tagId) @@ -90,7 +91,7 @@ int TagsManager::untagThing(const QString &thingId, const QString &tagId) tag.insert("appId", "nymea:app"); tag.insert("tagId", tagId); params.insert("tag", tag); - return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagReply"); + return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagResponse"); } int TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QString &value) @@ -102,7 +103,7 @@ int TagsManager::tagRule(const QString &ruleId, const QString &tagId, const QStr tag.insert("tagId", tagId); tag.insert("value", value); params.insert("tag", tag); - return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagReply"); + return m_jsonClient->sendCommand("Tags.AddTag", params, this, "addTagResponse"); } int TagsManager::untagRule(const QString &ruleId, const QString &tagId) @@ -113,7 +114,7 @@ int TagsManager::untagRule(const QString &ruleId, const QString &tagId) tag.insert("appId", "nymea:app"); tag.insert("tagId", tagId); params.insert("tag", tag); - return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagReply"); + return m_jsonClient->sendCommand("Tags.RemoveTag", params, this, "removeTagResponse"); } void TagsManager::handleTagsNotification(const QVariantMap ¶ms) @@ -156,7 +157,7 @@ void TagsManager::handleTagsNotification(const QVariantMap ¶ms) } } -void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap ¶ms) +void TagsManager::getTagsResponse(int /*commandId*/, const QVariantMap ¶ms) { QList tags; foreach (const QVariant &tagVariant, params.value("tags").toList()) { @@ -171,14 +172,18 @@ void TagsManager::getTagsReply(int /*commandId*/, const QVariantMap ¶ms) emit busyChanged(); } -void TagsManager::addTagReply(int commandId, const QVariantMap ¶ms) +void TagsManager::addTagResponse(int commandId, const QVariantMap ¶ms) { qCDebug(dcTags()) << "AddTag reply" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit addTagReply(commandId, static_cast(metaEnum.keyToValue(params.value("params").toMap().value("error").toByteArray()))); } -void TagsManager::removeTagReply(int commandId, const QVariantMap ¶ms) +void TagsManager::removeTagResponse(int commandId, const QVariantMap ¶ms) { qCDebug(dcTags()) << "RemoveTag reply" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + emit removeTagReply(commandId, static_cast(metaEnum.keyToValue(params.value("params").toMap().value("error").toByteArray()))); } Tag* TagsManager::unpackTag(const QVariantMap &tagMap) diff --git a/libnymea-app/tagsmanager.h b/libnymea-app/tagsmanager.h index 49e417de..dd3dcae6 100644 --- a/libnymea-app/tagsmanager.h +++ b/libnymea-app/tagsmanager.h @@ -43,6 +43,14 @@ class TagsManager : public JsonHandler Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: + enum TagError { + TagErrorNoError, + TagErrorThingNotFound, + TagErrorRuleNotFound, + TagErrorTagNotFound + }; + Q_ENUM(TagError) + explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr); QString nameSpace() const override; @@ -59,12 +67,14 @@ public: signals: void busyChanged(); + void addTagReply(int commandId, TagError error); + void removeTagReply(int commandId, TagError error); private slots: void handleTagsNotification(const QVariantMap ¶ms); - void getTagsReply(int commandId, const QVariantMap ¶ms); - void addTagReply(int commandId, const QVariantMap ¶ms); - void removeTagReply(int commandId, const QVariantMap ¶ms); + void getTagsResponse(int commandId, const QVariantMap ¶ms); + void addTagResponse(int commandId, const QVariantMap ¶ms); + void removeTagResponse(int commandId, const QVariantMap ¶ms); private: Tag *unpackTag(const QVariantMap &tagMap); diff --git a/nymea-app/dashboard/dashboarditem.cpp b/nymea-app/dashboard/dashboarditem.cpp new file mode 100644 index 00000000..281fc031 --- /dev/null +++ b/nymea-app/dashboard/dashboarditem.cpp @@ -0,0 +1,165 @@ +#include "dashboarditem.h" +#include "dashboardmodel.h" + +#include + +DashboardItem::DashboardItem(const QString &type, QObject *parent): + QObject(parent), + m_type(type) +{ + +} + +QString DashboardItem::type() const +{ + return m_type; +} + +int DashboardItem::columnSpan() const +{ + return m_columnSpan; +} + +void DashboardItem::setColumnSpan(int columnSpan) +{ + if (m_columnSpan != columnSpan) { + m_columnSpan = columnSpan; + emit columnSpanChanged(); + emit changed(); + } +} + +int DashboardItem::rowSpan() const +{ + return m_rowSpan; +} + +void DashboardItem::setRowSpan(int rowSpan) +{ + if (m_rowSpan != rowSpan) { + m_rowSpan = rowSpan; + qCritical() << "emitting changed"; + emit rowSpanChanged(); + emit changed(); + } +} + +DashboardThingItem::DashboardThingItem(const QUuid &thingId, QObject *parent): + DashboardItem("thing", parent), + m_thingId(thingId) +{ + +} + +QUuid DashboardThingItem::thingId() const +{ + return m_thingId; +} + +DashboardFolderItem::DashboardFolderItem(const QString &name, const QString &icon, QObject *parent): + DashboardItem("folder", parent), + m_name(name), + m_icon(icon) +{ + m_model = new DashboardModel(this); + connect(m_model, &DashboardModel::changed, this, &DashboardItem::changed); +} + +QString DashboardFolderItem::name() const +{ + return m_name; +} + +void DashboardFolderItem::setName(const QString &name) +{ + if (m_name != name) { + m_name = name; + emit nameChanged(); + emit changed(); + } +} + +QString DashboardFolderItem::icon() const +{ + return m_icon; +} + +void DashboardFolderItem::setIcon(const QString &icon) +{ + if (m_icon != icon) { + m_icon = icon; + emit iconChanged(); + emit changed(); + } +} + +DashboardModel *DashboardFolderItem::model() const +{ + return m_model; +} + +DashboardGraphItem::DashboardGraphItem(const QUuid &thingId, const QUuid &stateTypeId, QObject *parent): + DashboardItem("graph", parent), + m_thingId(thingId), + m_stateTypeId(stateTypeId) +{ + setColumnSpan(2); +} + +QUuid DashboardGraphItem::thingId() const +{ + return m_thingId; +} + +QUuid DashboardGraphItem::stateTypeId() const +{ + return m_stateTypeId; +} + +DashboardSceneItem::DashboardSceneItem(const QUuid &ruleId, QObject *parent): + DashboardItem("scene", parent), + m_ruleId(ruleId) +{ + +} + +QUuid DashboardSceneItem::ruleId() const +{ + return m_ruleId; +} + +DashboardWebViewItem::DashboardWebViewItem(const QUrl &url, bool interactive, QObject *parent): + DashboardItem("webview", parent), + m_url(url), + m_interactive(interactive) +{ + +} + +QUrl DashboardWebViewItem::url() const +{ + return m_url; +} + +void DashboardWebViewItem::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + emit urlChanged(); + emit changed(); + } +} + +bool DashboardWebViewItem::interactive() const +{ + return m_interactive; +} + +void DashboardWebViewItem::setInteractive(bool interactive) +{ + if (m_interactive != interactive) { + m_interactive = interactive; + emit interactiveChanged(); + emit changed(); + } +} diff --git a/nymea-app/dashboard/dashboarditem.h b/nymea-app/dashboard/dashboarditem.h new file mode 100644 index 00000000..ebbf330d --- /dev/null +++ b/nymea-app/dashboard/dashboarditem.h @@ -0,0 +1,113 @@ +#ifndef DASHBOARDITEM_H +#define DASHBOARDITEM_H + +#include +#include +#include + +class DashboardModel; + +class DashboardItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString type READ type CONSTANT) + Q_PROPERTY(int columnSpan READ columnSpan WRITE setColumnSpan NOTIFY columnSpanChanged) + Q_PROPERTY(int rowSpan READ rowSpan WRITE setRowSpan NOTIFY rowSpanChanged) +public: + explicit DashboardItem(const QString &type, QObject *parent = nullptr); + QString type() const; + int columnSpan() const; + void setColumnSpan(int columnSpan); + int rowSpan() const; + void setRowSpan(int rowSpan); +signals: + // For convenience when *any* change needs to be tracked + void changed(); + + void columnSpanChanged(); + void rowSpanChanged(); +private: + QString m_type; + int m_columnSpan = 1; + int m_rowSpan = 1; +}; + +class DashboardThingItem: public DashboardItem +{ + Q_OBJECT + Q_PROPERTY(QUuid thingId READ thingId CONSTANT) +public: + explicit DashboardThingItem(const QUuid &thingId, QObject *parent = nullptr); + + QUuid thingId() const; +private: + QUuid m_thingId; +}; + +class DashboardFolderItem: public DashboardItem +{ + Q_OBJECT + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QString icon READ icon WRITE setIcon NOTIFY iconChanged) + Q_PROPERTY(DashboardModel* model READ model CONSTANT) +public: + explicit DashboardFolderItem(const QString &name, const QString &icon, QObject *parent = nullptr); + QString name() const; + void setName(const QString &name); + QString icon() const; + void setIcon(const QString &icon); + DashboardModel *model() const; +signals: + void nameChanged(); + void iconChanged(); +private: + QString m_name; + QString m_icon; + DashboardModel *m_model= nullptr; +}; + +class DashboardGraphItem: public DashboardItem +{ + Q_OBJECT + Q_PROPERTY(QUuid thingId READ thingId CONSTANT) + Q_PROPERTY(QUuid stateTypeId READ stateTypeId CONSTANT) +public: + explicit DashboardGraphItem(const QUuid &thingId, const QUuid &stateTypeId, QObject *parent = nullptr); + QUuid thingId() const; + QUuid stateTypeId() const; +private: + QUuid m_thingId; + QUuid m_stateTypeId; +}; + +class DashboardSceneItem: public DashboardItem +{ + Q_OBJECT + Q_PROPERTY(QUuid ruleId READ ruleId CONSTANT) +public: + explicit DashboardSceneItem(const QUuid &ruleId, QObject *parent = nullptr); + QUuid ruleId() const; +private: + QUuid m_ruleId; +}; + +class DashboardWebViewItem: public DashboardItem +{ + Q_OBJECT + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) +public: + explicit DashboardWebViewItem(const QUrl &url, bool interactive = false, QObject *parent = nullptr); + QUrl url() const; + void setUrl(const QUrl &url); + bool interactive() const; + void setInteractive(bool interactive); +signals: + void urlChanged(); + void interactiveChanged(); +private: + QUrl m_url; + bool m_interactive = false; +}; + +#endif // DASHBOARDITEM_H diff --git a/nymea-app/dashboard/dashboardmodel.cpp b/nymea-app/dashboard/dashboardmodel.cpp new file mode 100644 index 00000000..831d8454 --- /dev/null +++ b/nymea-app/dashboard/dashboardmodel.cpp @@ -0,0 +1,223 @@ +#include "dashboardmodel.h" +#include "dashboarditem.h" + +#include +#include + +DashboardModel::DashboardModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +int DashboardModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant DashboardModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleType: + return m_list.at(index.row())->type(); + case RoleColumnSpan: + return m_list.at(index.row())->columnSpan(); + case RoleRowSpan: + return m_list.at(index.row())->rowSpan(); + } + Q_ASSERT_X(false, "DashboardModel", "Unhandled role"); + return QVariant(); +} + +QHash DashboardModel::roleNames() const +{ + QHash roles; + roles.insert(RoleType, "type"); + roles.insert(RoleColumnSpan, "columnSpan"); + roles.insert(RoleRowSpan, "rowSpan"); + return roles; +} + +DashboardItem *DashboardModel::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +void DashboardModel::addThingItem(const QUuid &thingId, int index) +{ + DashboardThingItem *item = new DashboardThingItem(thingId, this); + addItem(item, index); +} + +void DashboardModel::addFolderItem(const QString &name, const QString &icon, int index) +{ + DashboardFolderItem *item = new DashboardFolderItem(name, icon, this); + connect(item->model(), &DashboardModel::save, this, &DashboardModel::save); + addItem(item, index); +} + +void DashboardModel::addGraphItem(const QUuid &thingId, const QUuid &stateTypeId, int index) +{ + DashboardGraphItem *item = new DashboardGraphItem(thingId, stateTypeId, this); + item->setColumnSpan(2); + addItem(item, index); +} + +void DashboardModel::addSceneItem(const QUuid &ruleId, int index) +{ + DashboardSceneItem *item = new DashboardSceneItem(ruleId, this); + addItem(item, index); +} + +void DashboardModel::addWebViewItem(const QUrl &url, int columnSpan, int rowSpan, bool interactive, int index) +{ + QUrl fixedUrl = url; + // Correct url if no scheme is given as it would end up being qrc:// by default which no user will want... + if (fixedUrl.scheme().isEmpty()) { + fixedUrl.setScheme("https"); + } + DashboardWebViewItem *item = new DashboardWebViewItem(fixedUrl, interactive, this); + item->setColumnSpan(columnSpan); + item->setRowSpan(rowSpan); + addItem(item, index); +} + +void DashboardModel::removeItem(int index) +{ + qWarning() << "removing" << index; + beginRemoveRows(QModelIndex(), index, index); + m_list.removeAt(index); + endRemoveRows(); + emit changed(); + emit countChanged(); +} + +void DashboardModel::move(int from, int to) +{ + // QList's and QAbstractItemModel's move implementation differ when moving an item up the list :/ + // While QList needs the index in the resulting list, beginMoveRows expects it to be in the current list + // adjust the model's index by +1 in case we're moving upwards + int newModelIndex = to > from ? to+1 : to; + + beginMoveRows(QModelIndex(), from, from, QModelIndex(), newModelIndex); + m_list.move(from, to); + endMoveRows(); + emit changed(); +} + +void DashboardModel::loadFromJson(const QByteArray &json) +{ + if (toJson() == json) { + return; + } + beginResetModel(); + + qDeleteAll(m_list); + m_list.clear(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(json); + foreach (const QVariant &itemVariant, jsonDoc.toVariant().toList()) { + QVariantMap itemMap = itemVariant.toMap(); + QString type = itemMap.value("type").toString(); + DashboardItem *item; + if (type == "folder") { + DashboardFolderItem *folderItem = new DashboardFolderItem(itemMap.value("name").toString(), itemMap.value("icon", "folder").toString(), this); + folderItem->model()->loadFromJson(QJsonDocument::fromVariant(itemMap.value("model").toList()).toJson(QJsonDocument::Compact)); + connect(folderItem->model(), &DashboardModel::save, this, &DashboardModel::save); + item = folderItem; + } else if (type == "thing") { + item = new DashboardThingItem(itemMap.value("thingId").toUuid(), this); + } else if (type == "graph") { + item = new DashboardGraphItem(itemMap.value("thingId").toUuid(), itemMap.value("stateTypeId").toUuid(), this); + } else if (type == "scene") { + item = new DashboardSceneItem(itemMap.value("ruleId").toUuid(), this); + } else if (type == "webview") { + item = new DashboardWebViewItem(itemMap.value("url").toUrl(), itemMap.value("interactive", false).toBool(), this); + } else { + qWarning() << "Dashboard item type" << type << "is not implemented. Skipping..."; + continue; + } + item->setColumnSpan(itemMap.value("columnSpan", 1).toInt()); + item->setRowSpan(itemMap.value("rowSpan", 1).toInt()); + addItem(item); +// connect(item, &DashboardItem::changed, this, &DashboardModel::changed); +// m_list.append(item); + } + + endResetModel(); + emit countChanged(); +} + +QByteArray DashboardModel::toJson() const +{ + QVariantList list; + foreach (DashboardItem* item, m_list) { + QVariantMap map; + map.insert("type", item->type()); + if (item->type() == "thing") { + DashboardThingItem *thingItem = dynamic_cast(item); + map.insert("thingId", thingItem->thingId()); + } else if (item->type() == "folder") { + DashboardFolderItem *folderItem = dynamic_cast(item); + map.insert("name", folderItem->name()); + map.insert("icon", folderItem->icon()); + QJsonDocument modelDoc = QJsonDocument::fromJson(folderItem->model()->toJson()); + map.insert("model", modelDoc.toVariant()); + } else if (item->type() == "graph") { + DashboardGraphItem *grapItem = dynamic_cast(item); + map.insert("thingId", grapItem->thingId()); + map.insert("stateTypeId", grapItem->stateTypeId()); + } else if (item->type() == "scene") { + DashboardSceneItem *sceneItem = dynamic_cast(item); + map.insert("ruleId", sceneItem->ruleId()); + } else if (item->type() == "webview") { + DashboardWebViewItem *webViewItem = dynamic_cast(item); + map.insert("url", webViewItem->url()); + if (webViewItem->interactive()) { + map.insert("interactive", true); + } + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Type " + item->type().toUtf8() + " not implemented!"); + continue; + } + if (item->columnSpan() != 1) { + map.insert("columnSpan", item->columnSpan()); + } + if (item->rowSpan() != 1) { + map.insert("rowSpan", item->rowSpan()); + } + list.append(map); + } + QJsonDocument jsonDoc = QJsonDocument::fromVariant(list); + return jsonDoc.toJson(QJsonDocument::Compact); +} + +void DashboardModel::addItem(DashboardItem *item, int index) +{ + if (index < 0 || index > m_list.count()) { + index = m_list.count(); + } + connect(item, &DashboardItem::rowSpanChanged, this, [this, item](){ + int idx = m_list.indexOf(item); + if (idx >= 0) { + emit dataChanged(this->index(idx), this->index(idx), {RoleRowSpan}); + } + }); + connect(item, &DashboardItem::columnSpanChanged, this, [this, item](){ + int idx = m_list.indexOf(item); + if (idx >= 0) { + emit dataChanged(this->index(idx), this->index(idx), {RoleColumnSpan}); + } + }); + connect(item, &DashboardItem::changed, this, [this]() { + emit changed(); + }); + beginInsertRows(QModelIndex(), index, index); + m_list.insert(index, item); + endInsertRows(); + emit changed(); + emit countChanged(); +} diff --git a/nymea-app/dashboard/dashboardmodel.h b/nymea-app/dashboard/dashboardmodel.h new file mode 100644 index 00000000..dc738873 --- /dev/null +++ b/nymea-app/dashboard/dashboardmodel.h @@ -0,0 +1,54 @@ +#ifndef DASHBOARDMODEL_H +#define DASHBOARDMODEL_H + +#include + +class DashboardItem; + +class DashboardModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleType, + RoleColumnSpan, + RoleRowSpan, + }; + Q_ENUM(Roles) + + explicit DashboardModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + Q_INVOKABLE DashboardItem* get(int index) const; + + Q_INVOKABLE void addThingItem(const QUuid &thingId, int index = -1); + Q_INVOKABLE void addFolderItem(const QString &name, const QString &icon, int index = -1); + Q_INVOKABLE void addGraphItem(const QUuid &thingId, const QUuid &stateTypeId, int index = -1); + Q_INVOKABLE void addSceneItem(const QUuid &ruleId, int index = -1); + Q_INVOKABLE void addWebViewItem(const QUrl &url, int columnSpan, int rowSpan, bool interactive, int index = -1); + + Q_INVOKABLE void removeItem(int index); + Q_INVOKABLE void move(int from, int to); + + Q_INVOKABLE void loadFromJson(const QByteArray &json); + Q_INVOKABLE QByteArray toJson() const; +signals: + void changed(); + void countChanged(); + + void save(); + +private: + void addItem(DashboardItem *item, int index = -1); + +private: + QList m_list; + +}; + + +#endif // DASHBOARDMODEL_H diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 6c0163cf..d0ca9cc1 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -157,7 +157,7 @@ ui/images/lock-closed.svg ui/images/lock-open.svg ui/images/system-update.svg - ui/images/folder-symbolic.svg + ui/images/folder.svg ui/images/browser/BrowserIconFile.svg ui/images/browser/BrowserIconFolder.svg ui/images/browser/MediaBrowserIconSpotify.svg @@ -189,7 +189,8 @@ ui/images/browser/MediaBrowserIconNapster.svg ui/images/browser/MediaBrowserIconSoundCloud.svg ui/images/browser/MediaBrowserIconDeezer.svg - ui/images/view-grid-symbolic.svg + ui/images/groups.svg + ui/images/dashboard.svg ui/images/script.svg ui/images/save.svg ui/images/edit-clear.svg @@ -257,5 +258,6 @@ ui/images/zigbee/NXP.svg ui/images/nymea-splash.svg ui/images/cleaning-robot.svg + ui/images/chart.svg diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index 862590ba..b375eb50 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -46,6 +46,9 @@ #include "nfchelper.h" #include "nfcthingactionwriter.h" #include "platformhelper.h" +#include "dashboard/dashboardmodel.h" +#include "dashboard/dashboarditem.h" +#include "mouseobserver.h" #include "../config.h" #include "logging.h" @@ -151,6 +154,16 @@ int main(int argc, char *argv[]) qmlRegisterSingletonType("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); + qmlRegisterType("Nymea", 1, 0, "DashboardModel"); + qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardItem", ""); + qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardThingItem", ""); + qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardFolderItem", ""); + qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardGraphItem", ""); + qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardSceneItem", ""); + qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardWebViewItem", ""); + + qmlRegisterType("Nymea", 1, 0, "MouseObserver"); + engine->rootContext()->setContextProperty("appVersion", APP_VERSION); engine->rootContext()->setContextProperty("qtBuildVersion", QT_VERSION_STR); engine->rootContext()->setContextProperty("qtVersion", qVersion()); diff --git a/nymea-app/mouseobserver.cpp b/nymea-app/mouseobserver.cpp new file mode 100644 index 00000000..93f60347 --- /dev/null +++ b/nymea-app/mouseobserver.cpp @@ -0,0 +1,37 @@ +#include "mouseobserver.h" + +#include +#include + +MouseObserver::MouseObserver(QQuickItem *parent) : QQuickItem(parent) +{ + qCritical() << "*************************** creating observer" << window(); + + EventFilter *filter = new EventFilter(this); + connect(filter, &EventFilter::pressed, this, [=](){ + m_timer.start(); + }); + connect(filter, &EventFilter::released, this, [=](){ + m_timer.stop(); + }); + installEventFilter(filter); + setAcceptedMouseButtons(Qt::AllButtons); + + + m_timer.setInterval(200); + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, this, &MouseObserver::longPressed); +} + + + +EventFilter::EventFilter(QObject *parent): QObject(parent) +{ + +} + +bool EventFilter::eventFilter(QObject *watched, QEvent *event) +{ + qWarning() << "************ eventfilter" << event->type(); + return QObject::eventFilter(watched, event); +} diff --git a/nymea-app/mouseobserver.h b/nymea-app/mouseobserver.h new file mode 100644 index 00000000..8d658561 --- /dev/null +++ b/nymea-app/mouseobserver.h @@ -0,0 +1,36 @@ +#ifndef MOUSEOBSERVER_H +#define MOUSEOBSERVER_H + +#include +#include + +class MouseObserver : public QQuickItem +{ + Q_OBJECT +public: + explicit MouseObserver(QQuickItem *parent = nullptr); + +signals: + void longPressed(); + + +private: + QTimer m_timer; +}; + +class EventFilter: public QObject +{ + Q_OBJECT +public: + explicit EventFilter(QObject *parent = nullptr); + + bool eventFilter(QObject *watched, QEvent *event) override; + +signals: + void pressed(); + void released(); + +}; + + +#endif // MOUSEOBSERVER_H diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index 06bbb6d9..73789221 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -19,7 +19,10 @@ PRE_TARGETDEPS += ../libnymea-app linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a HEADERS += \ + dashboard/dashboarditem.h \ + dashboard/dashboardmodel.h \ mainmenumodel.h \ + mouseobserver.h \ nfchelper.h \ nfcthingactionwriter.h \ platformintegration/generic/screenhelper.h \ @@ -31,7 +34,10 @@ HEADERS += \ ruletemplates/messages.h SOURCES += main.cpp \ + dashboard/dashboarditem.cpp \ + dashboard/dashboardmodel.cpp \ mainmenumodel.cpp \ + mouseobserver.cpp \ nfchelper.cpp \ nfcthingactionwriter.cpp \ platformintegration/generic/screenhelper.cpp \ @@ -114,6 +120,8 @@ ios: { OBJECTIVE_SOURCES += $$PWD/../packaging/ios/platformhelperios.mm \ $$PWD/../packaging/ios/pushnotifications.mm \ + OTHER_FILES += $${OBJECTIVE_SOURCES} + # Add Firebase SDK QMAKE_LFLAGS += -ObjC $(inherited) firebase_files.files += $$files(../packaging/ios/GoogleService-Info.plist) diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index a57a99d7..1207ad3d 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -235,5 +235,16 @@ ui/appsettings/LoggingCategories.qml ui/ConfigurationBase.qml ui/devicepages/CleaningRobotThingPage.qml + ui/mainviews/DashboardView.qml + ui/mainviews/dashboard/DashboardThingDelegate.qml + ui/mainviews/dashboard/DashboardFolderDelegate.qml + ui/mainviews/dashboard/Dashboard.qml + ui/mainviews/dashboard/DashboardPage.qml + ui/mainviews/dashboard/DashboardDelegateBase.qml + ui/mainviews/dashboard/DashboardAddWizard.qml + ui/mainviews/dashboard/DashboardGraphDelegate.qml + ui/mainviews/dashboard/DashboardSceneDelegate.qml + ui/mainviews/dashboard/DashboardWebViewDelegate.qml + ui/components/SelectionTabs.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 53997172..02d885fe 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -70,7 +70,18 @@ Page { qsTr("Configure main view") : swipeView.currentItem.item.title.length > 0 ? swipeView.currentItem.item.title : filteredContentModel.modelData(swipeView.currentIndex, "displayName") } + + Repeater { + model: swipeView.currentItem.item.hasOwnProperty("headerButtons") ? swipeView.currentItem.item.headerButtons : 0 + delegate: HeaderButton { + imageSource: swipeView.currentItem.item.headerButtons[index].iconSource + onClicked: swipeView.currentItem.item.headerButtons[index].trigger() + visible: swipeView.currentItem.item.headerButtons[index].visible + color: swipeView.currentItem.item.headerButtons[index].color + } + } } + } Connections { @@ -109,13 +120,14 @@ Page { ListModel { id: mainMenuBaseModel - ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things" } - ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred" } - ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "view-grid-symbolic" } - ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow" } - ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100" } - ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter" } - ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media" } + ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things"; minVersion: "0.0" } + ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred"; minVersion: "2.0" } + ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "groups"; minVersion: "2.0" } + ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow"; minVersion: "2.0" } + ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100"; minVersion: "2.0" } + ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter"; minVersion: "2.0" } + ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media"; minVersion: "2.0" } + ListElement { name: "dashboard"; source: "DashboardView"; displayName: qsTr("Dashboard"); icon: "dashboard"; minVersion: "5.5" } } ListModel { @@ -143,6 +155,11 @@ Page { for (var i = 0; i < mainMenuBaseModel.count; i++) { var item = mainMenuBaseModel.get(i); + if (!engine.jsonRpcClient.ensureServerVersion(item.minVersion)) { + console.log("Skipping main view", item.name, "as the minimum required server version isn't met:", engine.jsonRpcClient.jsonRpcVersion, "<", item.minVersion) + continue; + } + var idx = mainViewSettings.sortOrder.indexOf(item.name); if (idx === -1) { newList[newItems++] = item; @@ -340,14 +357,14 @@ Page { id: configListView model: mainMenuModel width: parent.width - height: parent.height / 2.5 + height: parent.height / 3 anchors.centerIn: parent orientation: ListView.Horizontal moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 200 } } - property int delegateWidth: width / 2.5 + property int delegateWidth: width / 3 property bool dragging: draggingIndex >= 0 property int draggingIndex : -1 @@ -433,50 +450,35 @@ Page { } } - delegate: Item { + delegate: BigTile { id: configDelegate width: configListView.delegateWidth height: configListView.height property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0 visible: configListView.draggingIndex !== index - Pane { - anchors.fill: parent - anchors.margins: app.margins / 2 - Material.elevation: 2 + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 + header: RowLayout { + id: headerRow + Label { + text: model.displayName + } + } - contentItem: ItemDelegate { - anchors.fill: parent + contentItem: Item { + Layout.fillWidth: true + implicitHeight: configListView.height - headerRow.height - Style.margins * 2 - padding: app.margins * 2 - contentItem: GridLayout { - columns: 1 - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - ColorIcon { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) * .8 - height: width - name: Qt.resolvedUrl("images/" + model.icon + ".svg") - color: configDelegate.isEnabled ? Style.accentColor : Style.iconColor - } - } - - - Label { - text: model.displayName - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - font.pixelSize: app.largeFont - } - } + ColorIcon { + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) * .6 + height: width + name: Qt.resolvedUrl("images/" + model.icon + ".svg") + color: configDelegate.isEnabled ? Style.accentColor : Style.iconColor } } } @@ -500,13 +502,14 @@ Page { target: dndItem property: "scale" from: 1 - to: 0.9 + to: 0.95 duration: 200 } - Pane { + BigTile { + id: dndTile anchors.fill: parent - anchors.margins: app.margins / 2 +// anchors.margins: app.margins / 2 Material.elevation: 2 leftPadding: 0 @@ -514,32 +517,22 @@ Page { topPadding: 0 bottomPadding: 0 - contentItem: ItemDelegate { - anchors.fill: parent + header: RowLayout { + Label { + text: dndItem.displayName + } + } - padding: app.margins * 2 - contentItem: GridLayout { - columns: 1 + contentItem: Item { + Layout.fillWidth: true + implicitHeight: configListView.height - header.height - Item { - Layout.fillWidth: true - Layout.fillHeight: true - ColorIcon { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) * .8 - height: width - name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg") - color: dndItem.isEnabled ? Style.accentColor : Style.iconColor - } - } - - - Label { - text: dndItem.displayName - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - font.pixelSize: app.largeFont - } + ColorIcon { + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) * .6 + height: width + name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg") + color: dndItem.isEnabled ? Style.accentColor : Style.iconColor } } } diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index 4a19abae..0a729a84 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -4,7 +4,7 @@ Item { property color backgroundColor: "#fafafa" property color foregroundColor: "#202020" - property color accentColor: "#ff57baae" + property color accentColor: "#57baae" property color iconColor: "#808080" property color headerBackgroundColor: "#ffffff" @@ -91,6 +91,6 @@ Item { "currentPower": "deepskyblue", } - readonly property color red: "#ad4754" + readonly property color red: "#952727" readonly property color white: "white" } diff --git a/nymea-app/ui/components/BigTile.qml b/nymea-app/ui/components/BigTile.qml index b158d4f7..d7494260 100644 --- a/nymea-app/ui/components/BigTile.qml +++ b/nymea-app/ui/components/BigTile.qml @@ -10,6 +10,7 @@ Item { property alias header: headerContainer.children property alias contentItem: content.contentItem + property int contentHeight: root.height - headerContainer.height - content.topPadding - content.bottomPadding property alias showHeader: headerContainer.visible diff --git a/nymea-app/ui/components/MainPageTile.qml b/nymea-app/ui/components/MainPageTile.qml index 354da903..41606ad1 100644 --- a/nymea-app/ui/components/MainPageTile.qml +++ b/nymea-app/ui/components/MainPageTile.qml @@ -51,6 +51,7 @@ Item { property bool updateStatus: false property alias contentItem: innerContent.children + property alias lowerText: lowerTextLabel.text signal clicked(); signal pressAndHold(); @@ -146,7 +147,7 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true - visible: backgroundImg.status !== Image.Ready + visible: backgroundImg.status !== Image.Ready && label.text != "" Label { id: label @@ -155,7 +156,7 @@ Item { text: root.text.toUpperCase() font.pixelSize: app.smallFont font.letterSpacing: 1 - wrapMode: Text.WordWrap + wrapMode: Text.WrapAtWordBoundaryOrAnywhere horizontalAlignment: Text.AlignHCenter maximumLineCount: 2 elide: Text.ElideRight @@ -166,15 +167,26 @@ Item { } } + Label { + id: lowerTextLabel + anchors.fill: innerContent + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + padding: app.margins / 2 + visible: root.contentItem.length === 0 + } + + MouseArea { + anchors.fill: innerContent + } + Item { id: innerContent anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: app.margins / 2 } height: Style.iconSize + app.margins * 2 Material.foreground: Style.tileOverlayForegroundColor - - MouseArea { - anchors.fill: parent - } } RowLayout { diff --git a/nymea-app/ui/components/MainViewBase.qml b/nymea-app/ui/components/MainViewBase.qml index 421655eb..718070e0 100644 --- a/nymea-app/ui/components/MainViewBase.qml +++ b/nymea-app/ui/components/MainViewBase.qml @@ -41,6 +41,8 @@ Item { property string title: "" + property var headerButtons: [] + // Prevent scroll events to swipe left/right in case they fall through the grid MouseArea { anchors.fill: parent diff --git a/nymea-app/ui/components/MeaDialog.qml b/nymea-app/ui/components/MeaDialog.qml index fdc0a611..6f46a258 100644 --- a/nymea-app/ui/components/MeaDialog.qml +++ b/nymea-app/ui/components/MeaDialog.qml @@ -60,7 +60,7 @@ Dialog { } header: Item { - implicitHeight: headerRow.height + app.margins * 2 + implicitHeight: headerRow.height + app.margins implicitWidth: parent.width visible: root.title.length > 0 RowLayout { @@ -86,9 +86,8 @@ Dialog { } } } - ColumnLayout { + contentItem: ColumnLayout { id: content - anchors { left: parent.left; top: parent.top; right: parent.right } Label { id: contentLabel diff --git a/nymea-app/ui/components/MediaPlayer.qml b/nymea-app/ui/components/MediaPlayer.qml index 3b9f23f3..cb50b78b 100644 --- a/nymea-app/ui/components/MediaPlayer.qml +++ b/nymea-app/ui/components/MediaPlayer.qml @@ -226,7 +226,7 @@ Item { ProgressButton { longpressEnabled: false visible: root.thing.thingClass.browsable - imageSource: "../images/folder-symbolic.svg" + imageSource: "../images/folder.svg" onClicked: { if (!d.browser) { d.browser = browserPage.createObject(root, {x: 0, y: root.height}) diff --git a/nymea-app/ui/components/ProgressButton.qml b/nymea-app/ui/components/ProgressButton.qml index 52fffa28..0bff8f1b 100644 --- a/nymea-app/ui/components/ProgressButton.qml +++ b/nymea-app/ui/components/ProgressButton.qml @@ -65,6 +65,7 @@ Item { buttonDelegate.longpressed = false } onReleased: { + print("onReleased!") if (!containsMouse) { print("cancelled") buttonDelegate.longpressed = false; @@ -79,6 +80,7 @@ Item { root.clicked(); } buttonDelegate.longpressed = false + print("released end") } } diff --git a/nymea-app/ui/components/SelectionTabs.qml b/nymea-app/ui/components/SelectionTabs.qml new file mode 100644 index 00000000..559ece71 --- /dev/null +++ b/nymea-app/ui/components/SelectionTabs.qml @@ -0,0 +1,53 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import Nymea 1.0 + +Rectangle { + id: root + color: Style.tileBackgroundColor + radius: Style.smallCornerRadius + implicitHeight: layout.implicitHeight + + property int currentIndex: 0 + property alias model: repeater.model + readonly property var currentValue: model.hasOwnProperty("get") ? model.get(currentIndex) : model[currentIndex] + + + Rectangle { + x: repeater.count > 0 ? repeater.itemAt(root.currentIndex).x + 1 : 0 + anchors.verticalCenter: parent.verticalCenter + Behavior on x { NumberAnimation { duration: 150; easing.type: Easing.InOutQuad } } + height: layout.height - 2 + width: Math.floor(root.width / repeater.count) - 2 + color: Style.tileOverlayColor + radius: Style.smallCornerRadius + } + + RowLayout { + id: layout + anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter } + spacing: 0 + + Repeater { + id: repeater + + delegate: Item { + Layout.fillWidth: true + height: label.implicitHeight + Style.smallMargins + Label { + id: label + anchors.centerIn: parent + text: modelData + } + MouseArea { + anchors.fill: parent + onClicked: { + print("current index:", index) + root.currentIndex = index + } + } + } + } + } +} diff --git a/nymea-app/ui/components/ThingContextMenu.qml b/nymea-app/ui/components/ThingContextMenu.qml index a0604aa7..c4387663 100644 --- a/nymea-app/ui/components/ThingContextMenu.qml +++ b/nymea-app/ui/components/ThingContextMenu.qml @@ -32,7 +32,7 @@ AutoSizeMenu { root.addItem(menuEntryComponent.createObject(root, { text: qsTr("Grouping"), - iconSource: "../images/view-grid-symbolic.svg", + iconSource: "../images/groups.svg", functionName: "addToGroup" })) @@ -99,7 +99,7 @@ AutoSizeMenu { id: addToGroupDialog MeaDialog { title: qsTr("Groups for %1").arg(root.thing.name) - headerIcon: "../images/view-grid-symbolic.svg" + headerIcon: "../images/groups.svg" // NOTE: If CloseOnPressOutside is active (default) it will break the QtVirtualKeyboard // https://bugreports.qt.io/browse/QTBUG-56918 closePolicy: Popup.CloseOnEscape diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml index 2a553690..b4c8d8e0 100644 --- a/nymea-app/ui/customviews/GenericTypeGraph.qml +++ b/nymea-app/ui/customviews/GenericTypeGraph.qml @@ -48,16 +48,16 @@ Item { property string iconSource: "" property alias title: titleLabel.text - readonly property var valueState: thing.states.getState(stateType.id) - readonly property bool hasConnectable: thing.thingClass.interfaces.indexOf("connectable") >= 0 + readonly property State valueState: thing && stateType ? thing.states.getState(stateType.id) : null readonly property StateType connectedStateType: hasConnectable ? thing.thingClass.stateTypes.findByName("connected") : null + readonly property bool hasConnectable: connectedStateType != null LogsModelNg { id: logsModelNg - engine: _engine - thingId: root.thing.id - typeIds: [root.stateType.id] + engine: root.thing ? _engine : null + thingId: root.thing ? root.thing.id : "" + typeIds: root.stateType ? [root.stateType.id] : [] live: true graphSeries: lineSeries1 viewStartTime: xAxis.min @@ -66,7 +66,7 @@ Item { LogsModelNg { id: connectedLogsModel engine: root.hasConnectable ? _engine : null // don't even try to poll if we don't have a connectable interface - thingId: root.thing.id + thingId: root.thing ? root.thing.id : "" typeIds: root.hasConnectable ? [root.connectedStateType.id] : [] live: true graphSeries: connectedLineSeries @@ -78,8 +78,8 @@ Item { anchors.fill: parent margins.top: Style.iconSize + app.margins margins.bottom: app.margins / 2 - margins.left: 0 - margins.right: 0 + margins.left: Style.smallMargins + margins.right: Style.smallMargins backgroundColor: Style.tileBackgroundColor backgroundRoundness: Style.cornerRadius legend.visible: false @@ -104,6 +104,7 @@ Item { Label { id: titleLabel Layout.fillWidth: true + elide: Text.ElideRight text: root.stateType.type.toLowerCase() === "bool" ? root.stateType.displayName : 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit) @@ -118,9 +119,9 @@ Item { } HeaderButton { imageSource: "../images/zoom-in.svg" - enabled: xAxis.timeDiff > (60 * 30) + enabled: xAxis.timeDiff > (60 * 5) onClicked: { - var newTime = new Date(Math.min(xAxis.min.getTime() + (xAxis.timeDiff * 1000 / 4), xAxis.max.getTime() - (1000 * 60 * 30))) + var newTime = new Date(Math.min(xAxis.min.getTime() + (xAxis.timeDiff * 1000 / 4), xAxis.max.getTime() - (1000 * 60 * 5))) xAxis.min = newTime; } } @@ -129,27 +130,25 @@ Item { ValueAxis { id: yAxis max: { - switch (root.stateType.type.toLowerCase()) { - case "bool": + if (root.stateType && root.stateType.type.toLowerCase() == "bool") { return 1; - default: + } else { Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05)) } } min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) // onMinChanged: applyNiceNumbers(); // onMaxChanged: applyNiceNumbers(); - labelsFont.pixelSize: app.smallFont + labelsFont: Style.smallFont labelFormat: { - switch (root.stateType.type.toLowerCase()) { - case "bool": + if (root.stateType && root.stateType.type.toLowerCase() == "bool") { return "x"; - default: + } else { return "%d"; } } labelsColor: Style.foregroundColor - tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 + tickCount: root.stateType && root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) gridLineColor: color } @@ -166,7 +165,7 @@ Item { gridVisible: false color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) tickCount: chartView.width / 70 - labelsFont.pixelSize: app.smallFont + labelsFont: Style.smallFont labelsColor: Style.foregroundColor property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 @@ -265,9 +264,9 @@ Item { id: mainSeries axisX: xAxis axisY: yAxis - name: root.stateType.displayName + name: root.stateType ? root.stateType.displayName : "" borderColor: root.color - borderWidth: 4 + borderWidth: 2 lowerSeries: LineSeries { id: lineSeries0 XYPoint { x: xAxis.max.getTime(); y: 0 } @@ -347,7 +346,7 @@ Item { borderColor: root.color axisX: xAxis axisY: yAxis - pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool" + pointLabelsVisible: root.stateType && root.stateType.type.toLowerCase() !== "bool" pointLabelsColor: Style.foregroundColor pointLabelsFont.pixelSize: app.smallFont pointLabelsFormat: "@yPoint" diff --git a/nymea-app/ui/delegates/ThingTile.qml b/nymea-app/ui/delegates/ThingTile.qml index 3d7d1d5b..975c2fcd 100644 --- a/nymea-app/ui/delegates/ThingTile.qml +++ b/nymea-app/ui/delegates/ThingTile.qml @@ -37,30 +37,34 @@ import "../components" MainPageTile { id: root - text: thing.name.toUpperCase() - iconName: app.interfacesToIcon(thing.thingClass.interfaces) + text: thing ? thing.name.toUpperCase() : "" + iconName: thing ? app.interfacesToIcon(thing.thingClass.interfaces) : "" iconColor: Style.accentColor - isWireless: thing.thingClass.interfaces.indexOf("wirelessconnectable") >= 0 + isWireless: thing && thing.thingClass.interfaces.indexOf("wirelessconnectable") >= 0 batteryCritical: batteryCriticalState && batteryCriticalState.value === true disconnected: connectedState && connectedState.value === false signalStrength: signalStrengthState ? signalStrengthState.value : -1 - setupStatus: thing.setupStatus + setupStatus: thing ? thing.setupStatus : Thing.ThingSetupStatusNone updateStatus: updateStatusState && updateStatusState.value !== "idle" backgroundImage: artworkState && artworkState.value.length > 0 ? artworkState.value : "" property Thing thing: null property alias device: root.thing - readonly property State connectedState: thing.stateByName("connected") - readonly property State signalStrengthState: thing.stateByName("signalStrength") - readonly property State batteryCriticalState: thing.stateByName("batteryCritical") - readonly property State artworkState: thing.stateByName("artwork") - readonly property State updateStatusState: thing.stateByName("updateStatus") + readonly property State connectedState: thing ? thing.stateByName("connected") : null + readonly property State signalStrengthState: thing ? thing.stateByName("signalStrength") : null + readonly property State batteryCriticalState: thing ? thing.stateByName("batteryCritical") : null + readonly property State artworkState: thing ? thing.stateByName("artwork") : null + readonly property State updateStatusState: thing ? thing.stateByName("updateStatus") : null contentItem: Loader { id: loader anchors.fill: parent sourceComponent: { + if (!root.thing) { + return null + } + for (var i = 0; i < root.thing.thingClass.interfaces.length; i++) { switch (root.thing.thingClass.interfaces[i]) { case "closable": diff --git a/nymea-app/ui/devicepages/CleaningRobotThingPage.qml b/nymea-app/ui/devicepages/CleaningRobotThingPage.qml index aa399005..edd22a29 100644 --- a/nymea-app/ui/devicepages/CleaningRobotThingPage.qml +++ b/nymea-app/ui/devicepages/CleaningRobotThingPage.qml @@ -285,7 +285,7 @@ ThingPageBase { } ProgressButton { longpressEnabled: false - imageSource: "../images/view-grid-symbolic.svg" + imageSource: "../images/groups.svg" mode: "normal" size: Style.bigIconSize visible: root.thing.thingClass.browsable diff --git a/nymea-app/ui/devicepages/ThingPageBase.qml b/nymea-app/ui/devicepages/ThingPageBase.qml index 5837ff3d..ed6e22bd 100644 --- a/nymea-app/ui/devicepages/ThingPageBase.qml +++ b/nymea-app/ui/devicepages/ThingPageBase.qml @@ -58,7 +58,7 @@ Page { } HeaderButton { - imageSource: "../images/folder-symbolic.svg" + imageSource: "../images/folder.svg" visible: root.thingClass.browsable && root.showBrowserButton onClicked: { pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {thing: root.thing}) diff --git a/nymea-app/ui/images/chart.svg b/nymea-app/ui/images/chart.svg new file mode 100644 index 00000000..4bd2b189 --- /dev/null +++ b/nymea-app/ui/images/chart.svg @@ -0,0 +1,72 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/nymea-app/ui/images/dashboard.svg b/nymea-app/ui/images/dashboard.svg new file mode 100644 index 00000000..6a49f195 --- /dev/null +++ b/nymea-app/ui/images/dashboard.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/folder-symbolic.svg b/nymea-app/ui/images/folder.svg similarity index 100% rename from nymea-app/ui/images/folder-symbolic.svg rename to nymea-app/ui/images/folder.svg diff --git a/nymea-app/ui/images/view-grid-symbolic.svg b/nymea-app/ui/images/groups.svg similarity index 100% rename from nymea-app/ui/images/view-grid-symbolic.svg rename to nymea-app/ui/images/groups.svg diff --git a/nymea-app/ui/mainviews/DashboardView.qml b/nymea-app/ui/mainviews/DashboardView.qml new file mode 100644 index 00000000..e22f3caf --- /dev/null +++ b/nymea-app/ui/mainviews/DashboardView.qml @@ -0,0 +1,90 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import Qt.labs.settings 1.1 +import "../components" +import "dashboard" + +MainViewBase { + id: root + + headerButtons: [ + { + iconSource: "/ui/images/configure.svg", + color: dashboard.editMode ? Style.accentColor : Style.iconColor, + trigger: function() { + dashboard.editMode = !dashboard.editMode; + }, + visible: true + } + ] + + DashboardModel { + id: dashboardModel + + Component.onCompleted: { + print("loading dashboard:", dashboardSettings.dashboardConfig) + loadFromJson(dashboardSettings.dashboardConfig) + } + + onSave: { + print("saving dashboard"); + dashboardSettings.dashboardConfig = dashboardModel.toJson() + } + } + + AppData { + id: dashboardSettings + engine: _engine + group: "dashboard-1" + property string dashboardConfig: "" + onDashboardConfigChanged: { + print("dashboard changed on server! Reloading...") + dashboardModel.loadFromJson(dashboardConfig) + } + } + + Settings { + category: engine.jsonRpcClient.currentHost.uuid + property string dashboardConfig + } + + Dashboard { + id: dashboard + anchors.fill: parent + model: dashboardModel + } +} diff --git a/nymea-app/ui/mainviews/FavoritesView.qml b/nymea-app/ui/mainviews/FavoritesView.qml index 644cd740..284972a4 100644 --- a/nymea-app/ui/mainviews/FavoritesView.qml +++ b/nymea-app/ui/mainviews/FavoritesView.qml @@ -60,24 +60,85 @@ MainViewBase { moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 150; easing.type: Easing.InOutQuad } } model: tagsProxy - delegate: ThingTile { + delegate: Item { id: delegateRoot width: gridView.cellWidth height: gridView.cellHeight - thing: engine.thingManager.things.getThing(thingId) - visible: thingId !== fakeDragItem.thingId - onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing}) + property Thing thing: engine.thingManager.things.getThing(thingId) - onPressAndHold: root.editMode = true + property alias tile: thingTile - SequentialAnimation { - loops: Animation.Infinite - running: root.editMode - alwaysRunToEnd: true - NumberAnimation { from: 0; to: 3; target: delegateRoot; duration: 75; property: "rotation" } - NumberAnimation { from: 3; to: -3; target: delegateRoot; duration: 150; property: "rotation" } - NumberAnimation { from: -3; to: 0; target: delegateRoot; duration: 75; property: "rotation" } + ThingTile { + id: thingTile + anchors.fill: parent + thing: delegateRoot.thing + enabled: !root.editMode + onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing}) + onPressAndHold: root.editMode = true + opacity: dragArea.fakeDragItem !== null && delegateRoot.thing === dragArea.fakeDragItem.thing ? .3 : 1 + } + + Rectangle { + anchors.fill: parent + anchors.margins: Style.smallMargins + color: "transparent" + border.color: Style.accentColor + border.width: 4 + radius: Style.cornerRadius + visible: dragArea.fakeDragItem !== null && delegateRoot.thing === dragArea.fakeDragItem.thing + } + + Rectangle { + z: 2 + anchors.fill: parent + anchors.margins: Style.smallMargins + visible: opacity > 0 + radius: Style.cornerRadius + color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, .5) + opacity: root.editMode ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 200 } } + + MouseArea { + anchors.fill: parent + onClicked: root.editMode = false + } + + Rectangle { + anchors { + left: parent.left; top: parent.top; + margins: Style.smallMargins + } + height: Style.largeIconSize + width: Style.largeIconSize + color: Style.red + radius: Style.cornerRadius + opacity: dragArea.fakeDragItem == null + Rectangle { + anchors.fill: parent + radius: Style.cornerRadius + color: Style.foregroundColor + opacity: deleteMouseArea.pressed || deleteMouseArea.containsMouse ? .08 : 0 + Behavior on opacity { + NumberAnimation { duration: 200 } + } + } + ColorIcon { + name: "/ui/images/delete.svg" + size: Style.iconSize + anchors.centerIn: parent + color: Style.white + } + MouseArea { + id: deleteMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + print("delete clicked") + engine.tagsManager.untagThing(model.thingId, "favorites") + } + } + } } } @@ -88,41 +149,54 @@ MainViewBase { propagateComposedEvents: true property var dragOffset: ({}) property var draggedItem: null + property var fakeDragItem: null - onPressed: { - var item = gridView.itemAt(mouseX, mouseY) + onPressAndHold: { + var gridViewCoords = mapToItem(gridView.contentItem, mouseX, mouseY) + var item = gridView.itemAt(gridViewCoords.x, gridViewCoords.y); draggedItem = item; dragOffset = mapToItem(item, mouseX, mouseY) - fakeDragItem.x = mouseX - dragOffset.x; - fakeDragItem.y = mouseY - dragOffset.y; - fakeDragItem.text = item.text - fakeDragItem.iconName = item.iconName - fakeDragItem.iconColor = item.iconColor; - fakeDragItem.thingId = item.thing.id - fakeDragItem.batteryCritical = item.batteryCritical - fakeDragItem.disconnected = item.disconnected + + dragArea.fakeDragItem = dragItemComponent.createObject(dragArea, { + x: mouseX - dragOffset.x, + y: mouseY - dragOffset.y, + thing: draggedItem.thing + }) + drag.target = fakeDragItem } onReleased: { - drag.target = null - draggedItem = null - fakeDragItem.thingId = "" + if (drag.target) { + drag.target = null + dragArea.fakeDragItem.destroy() + dragArea.fakeDragItem = null + dragArea.draggedItem = null + } } onClicked: { - root.editMode = false + var gridViewCoords = mapToItem(gridView.contentItem, mouseX, mouseY) + var itemUnderMouse = gridView.itemAt(gridViewCoords.x, gridViewCoords.y); + if (itemUnderMouse !== null) { + mouse.accepted = false + } else { + root.editMode = false + } } } - MainPageTile { - id: fakeDragItem - width: gridView.cellWidth - height: gridView.cellHeight - Drag.active: dragArea.drag.active - visible: thingId !== "" - property var thingId: "" + Component { + id: dragItemComponent + + ThingTile { + id: fakeDragItem + width: gridView.cellWidth + height: gridView.cellHeight + Drag.active: dragArea.drag.active + } } + DropArea { id: dropArea anchors.fill: gridView @@ -130,14 +204,31 @@ MainViewBase { property int from: -1 property int to: -1 + property int pendingCommand: -1 + Connections { + target: engine.tagsManager + onAddTagReply: { + if (commandId == dropArea.pendingCommand) { + dropArea.pendingCommand = -1 + } + } + } + onEntered: { - var index = gridView.indexAt(drag.x + dragArea.dragOffset.x, drag.y + dragArea.dragOffset.y); + var gridViewCoords = mapToItem(gridView.contentItem, drag.x, drag.y) + var index = gridView.indexAt(gridViewCoords.x + dragArea.dragOffset.x, gridViewCoords.y + dragArea.dragOffset.y); from = index; to = index; } onPositionChanged: { - var index = gridView.indexAt(drag.x + dragArea.dragOffset.x, drag.y + dragArea.dragOffset.y); + if (dropArea.pendingCommand != -1) { + // busy + return + } + + var gridViewCoords = mapToItem(gridView.contentItem, drag.x, drag.y) + var index = gridView.indexAt(gridViewCoords.x + dragArea.dragOffset.x, gridViewCoords.y + dragArea.dragOffset.y); if (to !== index && from !== index && index >= 0 && index <= tagsProxy.count) { to = index; print("should move", from, "to", to) @@ -159,7 +250,7 @@ MainViewBase { } var tag = tagsProxy.get(i); - engine.tagsManager.tagThing(tag.thingId, tag.tagId, newIdx); + dropArea.pendingCommand = engine.tagsManager.tagThing(tag.thingId, tag.tagId, newIdx); } from = index; } @@ -168,7 +259,7 @@ MainViewBase { } EmptyViewPlaceholder { - anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors { left: parent.left; right: parent.right; margins: Style.margins } anchors.verticalCenter: parent.verticalCenter visible: gridView.count === 0 && !engine.thingManager.fetchingData title: qsTr("There are no favorite things yet.") diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml index fdb07a49..70aa0270 100644 --- a/nymea-app/ui/mainviews/GroupsView.qml +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -61,9 +61,9 @@ MainViewBase { delegate: MainPageTile { width: groupsGridView.cellWidth height: groupsGridView.cellHeight - iconName: "../images/view-grid-symbolic.svg" + iconName: "../images/groups.svg" iconColor: Style.accentColor - text: model.tagId.substring(6) + lowerText: model.tagId.substring(6) onClicked: { pageStack.push(Qt.resolvedUrl("../grouping/GroupInterfacesPage.qml"), {groupTag: model.tagId}) } @@ -76,7 +76,7 @@ MainViewBase { visible: groupsGridView.count == 0 && !engine.thingManager.fetchingData && !engine.tagsManager.busy title: qsTr("There are no groups set up yet.") text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.") - imageSource: "../images/view-grid-symbolic.svg" + imageSource: "../images/groups.svg" buttonVisible: false } } diff --git a/nymea-app/ui/mainviews/ScenesView.qml b/nymea-app/ui/mainviews/ScenesView.qml index ea9bf399..814106df 100644 --- a/nymea-app/ui/mainviews/ScenesView.qml +++ b/nymea-app/ui/mainviews/ScenesView.qml @@ -59,7 +59,7 @@ MainViewBase { iconName: iconTag ? "../images/" + iconTag.value + ".svg" : "../images/slideshow.svg"; fallbackIconName: "../images/slideshow.svg" iconColor: colorTag && colorTag.value.length > 0 ? colorTag.value : Style.accentColor; - text: model.name.toUpperCase() + lowerText: model.name property var colorTag: engine.tagsManager.tags.findRuleTag(model.id, "color") property var iconTag: engine.tagsManager.tags.findRuleTag(model.id, "icon") diff --git a/nymea-app/ui/mainviews/dashboard/Dashboard.qml b/nymea-app/ui/mainviews/dashboard/Dashboard.qml new file mode 100644 index 00000000..86b9c59a --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/Dashboard.qml @@ -0,0 +1,477 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +Item { + id: root + + property var model: null + property bool editMode: false + + function addItem(index) { + if (index === undefined) { + index = root.model.count + } + var addComponent = Qt.createComponent(Qt.resolvedUrl("DashboardAddWizard.qml")) + var popup = addComponent.createObject(root, {dashboardModel: root.model, index: index}) + popup.open() + } + + readonly property var componentMap: { + "thing": "DashboardThingDelegate.qml", + "folder": "DashboardFolderDelegate.qml", + "graph": "DashboardGraphDelegate.qml", + "scene": "DashboardSceneDelegate.qml", + "webview": "DashboardWebViewDelegate.qml" + } + + onEditModeChanged: { + if (!editMode) { + root.model.save() + } + } + + Flickable { + id: flickable + anchors.fill: parent + anchors.margins: app.margins / 2 + contentHeight: Math.max(layout.implicitHeight, height) + contentWidth: width + + MouseArea { + width: flickable.contentWidth + height: flickable.contentHeight + onPressAndHold: root.editMode = true + } + + GridLayout { + id: layout + width: Math.min(root.model.count * cellWidth, flickable.width) + readonly property int minTileWidth: 172 + readonly property int tilesPerRow: root.width / minTileWidth + columns: tilesPerRow + columnSpacing: 0 + rowSpacing: 0 + + property int cellWidth: flickable.width / tilesPerRow + property int cellHeight: cellWidth + + Repeater { + id: repeater + model: root.model + + Loader { + id: loader + Layout.preferredWidth: layout.cellWidth * columnSpan + Layout.preferredHeight: layout.cellHeight * rowSpan + property int columnSpan: model.columnSpan + property int rowSpan: model.rowSpan + + Layout.columnSpan: columnSpan + Layout.rowSpan: rowSpan + property string type: model.type + property DashboardItem dashboardItem: root.model.get(index) + opacity: dragArea.fromIndex == index ? .3 : 1 + + Component.onCompleted: { + setSource(Qt.resolvedUrl(componentMap[model.type]), {item: loader.dashboardItem}) + } + + Binding { + target: loader.item + property: "enabled" + value: !root.editMode// dragArea.editIndex === index + } + Binding { + target: loader.item + property: "editMode" + value: root.editMode + } + Binding { + target: loader.item + property: "bottomClip" + value: loader.bottomClip + when: ["android", "ios"].indexOf(Qt.platform.os) >= 0 + } + Binding { + target: loader.item + property: "topClip" + value: loader.topClip + when: ["android", "ios"].indexOf(Qt.platform.os) >= 0 + } + Connections { + target: loader.item + onOpenDialog: { + dialogComponent.createObject(root).open() + } + } + + property int topClip: Math.min(height, Math.max(0, -y + (flickable.contentY - flickable.originY) - Style.margins)) + property int bottomClip: Math.max(0, y + height - flickable.height - Style.margins - (flickable.contentY - flickable.originY)) + + Rectangle { + anchors.fill: parent + anchors.margins: Style.smallMargins + color: "transparent" + border.color: Style.accentColor + border.width: 4 + radius: Style.cornerRadius + visible: dragArea.fromIndex == index + } + + + Rectangle { + z: 2 + anchors.fill: parent + anchors.margins: Style.smallMargins + visible: opacity > 0 + radius: Style.cornerRadius + color: Qt.rgba(Style.backgroundColor.r, Style.backgroundColor.g, Style.backgroundColor.b, .5) + opacity: root.editMode ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 200 } } + + MouseArea { + anchors.fill: parent + onClicked: root.editMode = false + } + + Rectangle { + anchors { + left: parent.left; top: parent.top; + margins: Style.smallMargins + } + height: Style.largeIconSize + width: Style.largeIconSize + color: Style.red + radius: Style.cornerRadius + opacity: dragArea.fromIndex == -1 + Rectangle { + anchors.fill: parent + radius: Style.cornerRadius + color: Style.foregroundColor + opacity: deleteMouseArea.pressed || deleteMouseArea.containsMouse ? .08 : 0 + Behavior on opacity { + NumberAnimation { duration: 200 } + } + } + ColorIcon { + name: "/ui/images/delete.svg" + size: Style.iconSize + anchors.centerIn: parent + color: Style.white + } + MouseArea { + id: deleteMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + print("delete clicked") + root.model.removeItem(index) + } + } + } + Rectangle { + anchors { + right: parent.right + top: parent.top + margins: Style.smallMargins + } + height: Style.largeIconSize + width: Style.largeIconSize + color: Style.tileOverlayColor + radius: Style.cornerRadius + visible: opacity > 0 + opacity: dragArea.fromIndex == -1 && loader.item.configurable + Behavior on opacity { NumberAnimation { duration: 200 } } + + Rectangle { + anchors.fill: parent + radius: Style.cornerRadius + color: Style.foregroundColor + opacity: configureMouseArea.pressed || configureMouseArea.containsMouse ? .08 : 0 + Behavior on opacity { + NumberAnimation { duration: 200 } + } + + } + ColorIcon { + name: "/ui/images/configure.svg" + size: Style.iconSize + anchors.centerIn: parent +// color: Style.white + } + MouseArea { + id: configureMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + loader.item.configure() + } + } + } + } + } + } + + MouseArea { + id: addTile + Layout.preferredWidth: layout.cellWidth + Layout.preferredHeight: layout.cellHeight + hoverEnabled: true + opacity: root.editMode ? 1 : 0 + visible: opacity > 0 + Behavior on opacity { NumberAnimation { duration: 200 } } + + onClicked: { + print("add clicked") + root.addItem() + } + + Rectangle { + anchors.fill: parent + anchors.margins: Style.smallMargins + border.width: 4 + border.color: Style.tileOverlayColor + color: Qt.rgba( Style.tileBackgroundColor.r, Style.tileBackgroundColor.g, Style.tileBackgroundColor.b, addTile.containsMouse ? 1 : 0) + Behavior on color { ColorAnimation { duration: 200 } } + radius: Style.cornerRadius + + ColorIcon { + name: "/ui/images/add.svg" + size: Style.bigIconSize + anchors.centerIn: parent + color: Style.tileOverlayColor + } + } + } + + } + + MouseArea { + id: dragArea + anchors.fill: parent + enabled: root.editMode + propagateComposedEvents: true + preventStealing: fakeDragItem != null + property var fakeDragItem: null + property int fromIndex: -1 + property int editIndex: -1 + property int fakeDragOffsetX: 0 + property int fakeDragOffsetY: 0 + + Timer { + id: scrollTimer + interval: 10 + repeat: true + running: dragArea.fakeDragItem !== null + + property int scrollOffset: 0 + onTriggered: { + var mappedPos = dragArea.mapToItem(flickable, dragArea.mouseX, dragArea.mouseY) + var scrollPixels = 0 + if (mappedPos.y + scrollOffset < 60) { + scrollPixels = Math.max(-2, -flickable.contentY) + } else if (mappedPos.y + scrollOffset > flickable.height - 60) { + scrollPixels = Math.min(2, flickable.contentHeight - flickable.height - flickable.contentY) + } + flickable.contentY += scrollPixels + scrollOffset += scrollPixels + dragArea.fakeDragItem.y += scrollPixels + } + } + + onClicked: { + var ret = itemUnderMouse() + if (ret.item) { + dragArea.editIndex = ret.index + // Let the click pass through + mouse.accepted = false + } else if (itemContainsMouse(addTile)) { + mouse.accepted = false + } else { + root.editMode = false + } + } + + onPressAndHold: { + print("position", mouseX, mouseY) + + + var draggedItem = null + for (var i = 0; i < repeater.count; i++) { + var item = repeater.itemAt(i); + print("item coords:", item.x, item.y, item.width, item.height) + if (itemContainsMouse(item)) { + print("Yes!, Item at:", i) + fromIndex = i; + draggedItem = item; + break; + } + } + if (!draggedItem) { + return + } + + var mappedCursor = dragArea.mapToItem(item, mouseX, mouseY) + fakeDragOffsetX = mappedCursor.x + fakeDragOffsetY = mappedCursor.y + + dragArea.fakeDragItem = dragItemComponent.createObject(dragArea, + { + x: mouseX - fakeDragOffsetX, + y: mouseY - fakeDragOffsetY, + draggedItem: draggedItem + }) + } + + + function itemUnderMouse() { + var ret = {} + for (var i = 0; i < repeater.count; i++) { + var item = repeater.itemAt(i); + if (itemContainsMouse(item)) { + ret.item = item; + ret.index = i + break; + } + } + return ret + } + + function itemContainsMouse(item) { + var mapped = dragArea.mapToItem(item, mouseX, mouseY) + return mapped.x > 0 && mapped.x < item.width && mapped.y > 0 && mapped.y < item.height + } + + onPositionChanged: { + if (!fakeDragItem) { + return + } + scrollTimer.scrollOffset = 0; + + fakeDragItem.x = mouseX - fakeDragOffsetX + fakeDragItem.y = mouseY - fakeDragOffsetY + var itemUnderCursor = null + var itemIndex = -1 + for (var i = 0; i < repeater.count; i++) { + var item = repeater.itemAt(i); + if (itemContainsMouse(item)) { + print("Yes!, Item at:", i) + itemUnderCursor = item; + itemIndex = i + break; + } + } + if (!itemUnderCursor) { + return + } + if (fromIndex === itemIndex) { + return + } + + print("over item:", itemIndex) + + root.model.move(fromIndex, itemIndex) + fromIndex = itemIndex + + } + onReleased: { + if (dragArea.fakeDragItem) { + dragArea.fakeDragItem.destroy(); + dragArea.fakeDragItem = null + dragArea.fromIndex = -1 + } + } + } + } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: Style.margins } + anchors.verticalCenter: parent.verticalCenter + visible: root.model.count === 0 && !root.editMode + title: qsTr("Dashboard is empty") + text: qsTr("Start with adding a new item to this dashboard.") + buttonText: qsTr("Add item") + imageSource: "/ui/images/dashboard.svg" + onButtonClicked: { + root.addItem() + } + } + + Component { + id: dragItemComponent + Item { + property Item draggedItem: null + + layer.enabled: true + layer.effect: ShaderEffectSource { + sourceItem: draggedItem + live: true + } + + height: draggedItem.height + width: draggedItem.width + } + } + + Component { + id: editDialogComponent + + MeaDialog { + id: editDialog + standardButtons: Dialog.NoButton + + property DashboardItem dashboardItem: null + property int index: -1 + + + ColumnLayout { + Button { + text: qsTr("Remove") + Layout.fillWidth: true + onClicked: { + root.model.removeItem(editDialog.index) + editDialog.close() + } + } + } + } + } +} + + diff --git a/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml b/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml new file mode 100644 index 00000000..91328e1f --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml @@ -0,0 +1,400 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +MeaDialog { + id: root + + title: qsTr("Add item") + standardButtons: Dialog.NoButton + + property DashboardModel dashboardModel: null + property int index: 0 + + padding: Style.margins + + contentItem: StackView { + id: internalPageStack + implicitHeight: currentItem.implicitHeight + clip: true + + initialItem: ColumnLayout { + id: contentColumn + implicitHeight: childrenRect.height + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Thing") + iconName: "things" + onClicked: { + internalPageStack.push(addThingSelectionComponent) + } + } + NymeaItemDelegate { + Layout.fillWidth: true + iconName: "folder" + text: qsTr("Folder") + onClicked: { + internalPageStack.push(addFolderComponent) + } + } + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Chart") + iconName: "chart" + onClicked: { + internalPageStack.push(addGraphSelectThingComponent) + } + } + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Scene") + iconName: "slideshow" + onClicked: { + internalPageStack.push(addSceneComponent) + } + } + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Web view") + iconName: "stock_website" + visible: Qt.platform.os != "android" + onClicked: { + internalPageStack.push(addWebViewComponent) + } + } + } + + + Component { + id: addThingSelectionComponent + ColumnLayout { + RowLayout { + ColorIcon { + name: "/ui/images/find.svg" + } + TextField { + id: filterTextField + Layout.fillWidth: true + + } + } + + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: Style.delegateHeight * 6 + clip: true + model: ThingsProxy { + id: thingsProxy + engine: _engine + nameFilter: filterTextField.displayText + } + + ScrollBar.vertical: ScrollBar {} + + delegate: NymeaItemDelegate { + width: parent.width + text: model.name + iconName: app.interfacesToIcon(thingsProxy.get(index).thingClass.interfaces) + progressive: false + onClicked: { + root.dashboardModel.addThingItem(model.id, root.index) + root.close(); + } + } + } + } + } + + Component { + id: addFolderComponent + ColumnLayout { + property bool needsOkButton: true + + TextField { + id: folderNameTextField + Layout.fillWidth: true + placeholderText: qsTr("Name") + } + + GridView { + id: iconsGrid + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: Style.bigIconSize * 6 + model: Object.entries(NymeaUtils.namedIcons) + property int columns: width / Style.bigIconSize - 1 + cellWidth: width / columns + cellHeight: cellWidth + + property string currentIcon: "dashboard" + + clip: true + delegate: MouseArea { + width: iconsGrid.cellWidth + height: iconsGrid.cellHeight + onClicked: { + print("clicked", modelData[0]) + iconsGrid.currentIcon = modelData[0] + } + + ColorIcon { + anchors.centerIn: parent + name: modelData[1] + color: modelData[0] == iconsGrid.currentIcon ? Style.accentColor : Style.iconColor + size: Style.bigIconSize + } + } + } + + Connections { + target: okButton + onClicked: { + root.dashboardModel.addFolderItem(folderNameTextField.text, iconsGrid.currentIcon, root.index) + root.close(); + } + } + } + } + + Component { + id: addGraphSelectThingComponent + ColumnLayout { + RowLayout { + ColorIcon { + name: "/ui/images/find.svg" + } + TextField { + id: filterTextField + Layout.fillWidth: true + } + } + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: Style.delegateHeight * 6 + clip: true + + ScrollBar.vertical: ScrollBar {} + + model: ThingsProxy { + id: thingsProxy + engine: _engine + nameFilter: filterTextField.displayText + } + delegate: NymeaItemDelegate { + text: model.name + width: parent ? parent.width : 0 // silence warning on delegate descruction + iconName: app.interfacesToIcon(thingsProxy.get(index).thingClass.interfaces) + onClicked: { + internalPageStack.push(addGraphSelectStateComponent, {thing: thingsProxy.get(index)}) + } + } + } + } + + } + Component { + id: addGraphSelectStateComponent + ListView { + implicitHeight: Style.delegateHeight * 6 + clip: true + + ScrollBar.vertical: ScrollBar {} + + property Thing thing: null + model: thing.thingClass.stateTypes + width: parent.width + delegate: NymeaItemDelegate { + width: parent.width + text: model.displayName + onClicked: { + root.dashboardModel.addGraphItem(thing.id, model.id, root.index) + root.close() + } + } + } + } + + Component { + id: addSceneComponent + ListView { + width: parent.width + implicitHeight: Style.delegateHeight * 6 + + ScrollBar.vertical: ScrollBar {} + + model: RulesFilterModel { + rules: engine.ruleManager.rules + filterExecutable: true + } + delegate: NymeaItemDelegate { + width: parent.width + text: model.name + iconName: iconTag.tag.value + iconColor: colorTag.tag.value + + TagWatcher { + id: iconTag + tags: engine.tagsManager.tags + ruleId: model.id + tagId: "icon" + } + TagWatcher { + id: colorTag + tags: engine.tagsManager.tags + ruleId: model.id + tagId: "color" + } + + onClicked: { + root.dashboardModel.addSceneItem(model.id, root.index) + root.close() + } + } + } + } + + Component { + id: addWebViewComponent + ColumnLayout { + property bool needsOkButton: true + property bool okButtonEnabled: urlTextField.displayText.length > 0 + + Connections { + target: okButton + onClicked: { + root.dashboardModel.addWebViewItem(urlTextField.text, columnsTabs.currentValue, rowsTabs.currentValue, interactiveSwitch.checked, root.index) + root.close(); + } + } + + SettingsPageSectionHeader { + Layout.fillWidth: true + text: qsTr("Location") + } + + TextField { + id: urlTextField + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + placeholderText: qsTr("Enter a URL") + text: "https://" + } + + SettingsPageSectionHeader { + Layout.fillWidth: true + text: qsTr("Size") + } + + GridLayout { + columns: width > 300 ? 2 : 1 + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + columnSpacing: Style.smallMargins + rowSpacing: Style.smallMargins + Label { + text: qsTr("Columns") + } + SelectionTabs { + id: columnsTabs + Layout.fillWidth: true + model: [1, 2, 3, 4, 5, 6] + currentIndex: root.item.columnSpan - 1 + } + Label { + text: qsTr("Rows") + } + SelectionTabs { + id: rowsTabs + Layout.fillWidth: true + model: [1, 2, 3, 4, 5, 6] + currentIndex: root.item.rowSpan - 1 + } + } + + SettingsPageSectionHeader { + Layout.fillWidth: true + text: qsTr("Behavior") + visible: ["android", "ios"].indexOf(Qt.platform.os) < 0 + } + + SwitchDelegate { + id: interactiveSwitch + Layout.fillWidth: true + checked: root.item.interactive + text: qsTr("Interactive") + visible: ["android", "ios"].indexOf(Qt.platform.os) < 0 + } + } + } + } + + footer: Item { + implicitHeight: buttonRow.implicitHeight + Style.margins + + RowLayout { + id: buttonRow + anchors { left: parent.left; right: parent.right; bottom: parent.bottom; margins: Style.margins} + spacing: Style.smallMargins + + Button { + text: qsTr("Cancel") + onClicked: root.close() + } + Button { + text: qsTr("Back") + visible: internalPageStack.depth > 1 + onClicked: internalPageStack.pop() + } + + Item { + Layout.fillWidth: true + } + Button { + id: okButton + text: qsTr("OK") + visible: internalPageStack.currentItem.hasOwnProperty("needsOkButton") && internalPageStack.currentItem.needsOkButton === true + enabled: !internalPageStack.currentItem.hasOwnProperty("okButtonEnabled") || internalPageStack.currentItem.okButtonEnabled + } + } + } +} diff --git a/nymea-app/ui/mainviews/dashboard/DashboardDelegateBase.qml b/nymea-app/ui/mainviews/dashboard/DashboardDelegateBase.qml new file mode 100644 index 00000000..27850f0e --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardDelegateBase.qml @@ -0,0 +1,60 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +Item { + id: root + + property alias contentItem: contentContainer.children + + property bool configurable: false + function configure() { + console.warn("Dashboard item claims to be configurable but doesn't implement configure() function") + } + signal openDialog(var dialogComponent); + + property bool editMode: false + + property int topClip: 0 + property int bottomClip: 0 + + Item { + id: contentContainer + anchors.fill: parent + } +} diff --git a/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml new file mode 100644 index 00000000..c766b3c8 --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml @@ -0,0 +1,117 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +DashboardDelegateBase { + id: root + property DashboardFolderItem item: null + + configurable: true + + function configure() { + print("configure called") + root.openDialog(configDialogComponent) + } + + contentItem: MainPageTile { + id: delegateRoot + height: root.height + width: root.width +// text: root.item.name + iconName: NymeaUtils.namedIcon(root.item.icon) + iconColor: Style.accentColor + onClicked: pageStack.push(Qt.resolvedUrl("DashboardPage.qml"), {item: root.item}) + onPressAndHold: root.longPressed(); + contentItem: Label { + text: root.item.name + width: parent.width + height: parent.height + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + padding: app.margins / 2 + } + } + + Component { + id: configDialogComponent + MeaDialog { + id: configDialog + + onAccepted: { + root.item.name = nameTextField.text + } + + TextField { + id: nameTextField + text: root.item.name + Layout.fillWidth: true + placeholderText: qsTr("Name") + } + + GridView { + id: iconsGrid + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: Style.bigIconSize * 6 + model: Object.entries(NymeaUtils.namedIcons) + property int columns: width / Style.bigIconSize - 1 + cellWidth: width / columns + cellHeight: cellWidth + clip: true + delegate: MouseArea { + width: iconsGrid.cellWidth + height: iconsGrid.cellHeight + onClicked: { + print("clicked", modelData[0]) + root.item.icon = modelData[0] + } + + ColorIcon { + anchors.centerIn: parent + name: modelData[1] + color: modelData[0] == root.item.icon ? Style.accentColor : Style.iconColor + size: Style.bigIconSize + } + } + } + } + } +} + diff --git a/nymea-app/ui/mainviews/dashboard/DashboardGraphDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardGraphDelegate.qml new file mode 100644 index 00000000..23ff1e83 --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardGraphDelegate.qml @@ -0,0 +1,63 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../customviews" + +DashboardDelegateBase { + id: root + property DashboardGraphItem item: null + + readonly property Thing thing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(item.thingId) + readonly property StateType stateType: thing ? thing.thingClass.stateTypes.getStateType(item.stateTypeId) : null + readonly property State state: thing ? thing.states.getState(item.stateTypeId) : null + + contentItem: GenericTypeGraph { + id: graph + width: root.width + height: root.height + title: root.state && root.stateType ? root.thing.name + " " + Types.toUiValue(root.state.value, root.stateType.unit) + Types.toUiUnit(root.stateType.unit) : "" + + thing: root.thing + color: "blue"//app.interfaceToColor(interfaceName) + iconSource: ""// app.interfaceToIcon(interfaceName) + implicitHeight: width * .6 +// property string interfaceName: parent.interfaceName + stateType: root.stateType + property State state: root.state + } +} + diff --git a/nymea-app/ui/mainviews/dashboard/DashboardPage.qml b/nymea-app/ui/mainviews/dashboard/DashboardPage.qml new file mode 100644 index 00000000..ff461fc9 --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardPage.qml @@ -0,0 +1,60 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +Page { + id: root + property DashboardFolderItem item: null + + header: NymeaHeader { + text: root.item.name + onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: "configure" + onClicked: dashboard.editMode = !dashboard.editMode + color: dashboard.editMode ? Style.accentColor : Style.iconColor + } + } + + Dashboard { + id: dashboard + anchors.fill: parent + model: root.item.model + } +} diff --git a/nymea-app/ui/mainviews/dashboard/DashboardSceneDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardSceneDelegate.qml new file mode 100644 index 00000000..8b8f4e4b --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardSceneDelegate.qml @@ -0,0 +1,68 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +DashboardDelegateBase { + id: root + property DashboardSceneItem item: null + + readonly property Rule rule: item && !engine.ruleManager.fetchingData ? engine.ruleManager.rules.getRule(item.ruleId) : null + + property var colorTag: engine.tagsManager.tags.findRuleTag(root.item.ruleId, "color") + property var iconTag: engine.tagsManager.tags.findRuleTag(root.item.ruleId, "icon") + + contentItem: MainPageTile { + width: root.width + height: root.height + iconName: iconTag ? "/ui/images/" + iconTag.value + ".svg" : "/ui/images/slideshow.svg"; + fallbackIconName: "/ui/images/slideshow.svg" + iconColor: colorTag && colorTag.value.length > 0 ? colorTag.value : Style.accentColor; + lowerText: root.rule ? root.rule.name : "" + + onClicked: engine.ruleManager.executeActions(root.item.ruleId) + onPressAndHold: root.longPressed() + + Connections { + target: engine.tagsManager.tags + onCountChanged: { + colorTag = engine.tagsManager.tags.findRuleTag(root.item.ruleId, "color") + iconTag = engine.tagsManager.tags.findRuleTag(root.item.ruleId, "icon") + } + } + } +} diff --git a/nymea-app/ui/mainviews/dashboard/DashboardThingDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardThingDelegate.qml new file mode 100644 index 00000000..c7446479 --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardThingDelegate.qml @@ -0,0 +1,53 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" + +DashboardDelegateBase { + id: root + property DashboardThingItem item: null + + contentItem: ThingTile { + id: delegateRoot + width: root.width + height: root.height + thing: engine.thingManager.fetchingData ? null : engine.thingManager.things.getThing(root.item.thingId) + + onClicked: pageStack.push(Qt.resolvedUrl("../../devicepages/" + NymeaUtils.interfaceListToDevicePage(thing.thingClass.interfaces)), {thing: thing}) + onPressAndHold: root.longPressed() + } +} diff --git a/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml new file mode 100644 index 00000000..924b1ae5 --- /dev/null +++ b/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml @@ -0,0 +1,176 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../../components" +import "../../delegates" +//import QtWebView 1.1 +import QtGraphicalEffects 1.1 + +DashboardDelegateBase { + id: root + property DashboardWebViewItem item: null + configurable: true + + function configure() { + root.openDialog(configDialogComponent) + } + + contentItem: MouseArea { + id: delegateRoot + width: root.width + height: root.height + + Component.onCompleted: { + // This might fail if qml-module-qtwebview isn't around + var webView = Qt.createQmlObject(webViewString, webViewContainer); + print("created webView", webView) + } + property string webViewString: + ' + import QtQuick 2.8; + import QtWebView 1.1; + import Nymea 1.0; + + WebView { + id: webView + anchors.fill: parent +anchors.bottomMargin: root.bottomClip + Style.smallMargins +anchors.topMargin: root.topClip + Style.smallMargins + anchors.margins: Style.smallMargins + url: root.item.url + enabled: root.item.interactive + visible: !app.mainMenu.visible && !root.editMode && root.topClip < root.height && root.bottomClip < height + } + ' + + Item { + id: webViewContainer + anchors.fill: parent + } + + Rectangle { + id: mask + anchors.fill: parent + anchors.margins: Style.smallMargins + radius: Style.cornerRadius + } + + OpacityMask { + anchors.fill: parent + anchors.margins: Style.smallMargins + source: ShaderEffectSource { + sourceItem: webViewContainer + recursive: true + hideSource: true + } + maskSource: mask + } + } + + Component { + id: configDialogComponent + MeaDialog { + id: configDialog + + onAccepted: { + root.item.url = urlTextField.text + root.item.columnSpan = columnsTabs.currentValue + root.item.rowSpan = rowsTabs.currentValue + root.item.interactive = interactiveSwitch.checked + } + + SettingsPageSectionHeader { + Layout.fillWidth: true + text: qsTr("Location") + } + + TextField { + id: urlTextField + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + placeholderText: qsTr("Enter a URL") + text: root.item.url + } + + SettingsPageSectionHeader { + Layout.fillWidth: true + text: qsTr("Size") + } + + GridLayout { + columns: width > 300 ? 2 : 1 + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + columnSpacing: Style.smallMargins + rowSpacing: Style.smallMargins + Label { + text: qsTr("Columns") + } + SelectionTabs { + id: columnsTabs + Layout.fillWidth: true + model: [1, 2, 3, 4, 5, 6] + currentIndex: root.item.columnSpan - 1 + } + Label { + text: qsTr("Rows") + } + SelectionTabs { + id: rowsTabs + Layout.fillWidth: true + model: [1, 2, 3, 4, 5, 6] + currentIndex: root.item.rowSpan - 1 + } + } + + SettingsPageSectionHeader { + Layout.fillWidth: true + text: qsTr("Behavior") + visible: ["android", "ios"].indexOf(Qt.platform.os) < 0 + } + + SwitchDelegate { + id: interactiveSwitch + Layout.fillWidth: true + checked: root.item.interactive + text: qsTr("Interactive") + visible: ["android", "ios"].indexOf(Qt.platform.os) < 0 + } + } + } +} diff --git a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml index 749b67ff..9eb5255f 100644 --- a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml +++ b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml @@ -51,7 +51,7 @@ SettingsPageBase { ThingInfoPane { id: infoPane - anchors { left: parent.left; top: parent.top; right: parent.right } + Layout.fillWidth: true thing: root.thing } diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index 567fd84b..05870cad 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -84,4 +84,50 @@ Item { return ((r * 299 + g * 587 + b * 114) / 1000) < 128 } + + property var namedIcons: { + "dashboard": "/ui/images/dashboard.svg", + "group": "/ui/images/groups.svg", + "folder": "/ui/images/folder.svg", + "star": "/ui/images/starred.svg", + "heart": "/ui/images/like.svg", + "wrench": "/ui/images/configure.svg", + "light": "/ui/images/light-on.svg", + "sensor": "/ui/images/sensors.svg", + "media": "/ui/images/media.svg", + "powersocket": "/ui/images/powersocket.svg", + "power": "/ui/images/system-shutdown.svg", + "weather": "/ui/images/weather-app-symbolic.svg", + "attention": "/ui/images/attention.svg", + "shutter": "/ui/images/shutter/shutter-040.svg", + "garage": "/ui/images/garage/garage-100.svg", + "awning": "/ui/images/awning/awning-100.svg", + "uncategorized": "/ui/images/select-none.svg", + "closable": "/ui/images/closable-move.svg", + "smartmeter": "/ui/images/smartmeter.svg", + "heating": "/ui/images/thermostat/heating.svg", + "cooling": "/ui/images/thermostat/cooling.svg", + "meter": "/ui/images/dial.svg", + "ev-charger": "/ui/images/ev-charger.svg", + "battery": "/ui/images/battery/battery-100.svg", + "message": "/ui/images/notification.svg", + "irrigation": "/ui/images/irrigation.svg", + "ventilation": "/ui/images/ventilation.svg", + "lock": "/ui/images/smartlock.svg", + "qrcode": "/ui/images/qrcode.svg", + "cleaningrobot": "/ui/images/cleaning-robot.svg", + "plant": "/ui/images/sensors/conductivity.svg", + "water": "/ui/images/sensors/water.svg", + "wind": "/ui/images/sensors/windspeed.svg", + "cloud": "/ui/images/weathericons/weather-clouds.svg", + "send": "/ui/images/send.svg" + } + function namedIcon(name) { + if (!namedIcons.hasOwnProperty(name)) { + console.error("No such named icon:", name) + return + } + return namedIcons[name] + } + }