Rework main view

This commit is contained in:
Michael Zanetti 2020-08-21 01:40:41 +02:00
parent 7d7225e9db
commit 57d2986948
28 changed files with 1209 additions and 298 deletions

View File

@ -76,7 +76,7 @@ void DeviceManager::init()
qWarning() << "received an event from a device we don't know..." << deviceId << event;
return;
}
qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
// qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap());
emit eventTriggered(deviceId.toString(), eventTypeId.toString(), event.value("params").toMap());
});
@ -231,7 +231,7 @@ void DeviceManager::notificationReceived(const QVariantMap &data)
qWarning() << "received an event from a device we don't know..." << deviceId << qUtf8Printable(QJsonDocument::fromVariant(data).toJson());
return;
}
qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
// qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson());
dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap());
} else if (notification == "Integrations.IOConnectionAdded") {
QVariantMap connectionMap = data.value("params").toMap().value("ioConnection").toMap();
@ -750,7 +750,7 @@ void DeviceManager::executeBrowserItemActionResponse(const QVariantMap &params)
void DeviceManager::getIOConnectionsResponse(const QVariantMap &params)
{
qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
// qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
foreach (const QVariant &connectionVariant, params.value("params").toMap().value("ioConnections").toList()) {
QVariantMap connectionMap = connectionVariant.toMap();

View File

@ -92,6 +92,7 @@
#include "ruletemplates/ruleactionparamtemplate.h"
#include "connection/awsclient.h"
#include "models/devicemodel.h"
#include "models/sortfilterproxymodel.h"
#include "system/systemcontroller.h"
#include "types/package.h"
#include "types/packages.h"
@ -177,8 +178,11 @@ void registerQmlTypes() {
qmlRegisterType<VendorsProxy>(uri, 1, 0, "VendorsProxy");
qmlRegisterUncreatableType<Device>(uri, 1, 0, "Device", "Can't create this in QML. Get it from the Devices.");
qmlRegisterUncreatableType<Device>(uri, 1, 0, "Thing", "Can't create this in QML. Get it from the Things.");
qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Devices", "Can't create this in QML. Get it from the DeviceManager.");
qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Things", "Can't create this in QML. Get it from the ThingManager.");
qmlRegisterType<DevicesProxy>(uri, 1, 0, "DevicesProxy");
qmlRegisterType<DevicesProxy>(uri, 1, 0, "ThingsProxy");
qmlRegisterType<InterfacesModel>(uri, 1, 0, "InterfacesModel");
qmlRegisterType<InterfacesSortModel>(uri, 1, 0, "InterfacesSortModel");
@ -311,6 +315,8 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<IOConnection>(uri, 1, 0, "IOConnection", "Get it from IOConnections");
qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher");
qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher");
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
}
#endif // LIBNYMEAAPPCORE_H

View File

@ -27,6 +27,7 @@ INCLUDEPATH += $$top_srcdir/QtZeroConf
SOURCES += \
configuration/networkmanager.cpp \
engine.cpp \
models/sortfilterproxymodel.cpp \
ruletemplates/calendaritemtemplate.cpp \
ruletemplates/timedescriptortemplate.cpp \
ruletemplates/timeeventitemtemplate.cpp \
@ -162,6 +163,7 @@ SOURCES += \
HEADERS += \
configuration/networkmanager.h \
engine.h \
models/sortfilterproxymodel.h \
ruletemplates/calendaritemtemplate.h \
ruletemplates/timedescriptortemplate.h \
ruletemplates/timeeventitemtemplate.h \

View File

@ -0,0 +1,62 @@
#include "sortfilterproxymodel.h"
#include <QtDebug>
SortFilterProxyModel::SortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
{
connect(this, &QSortFilterProxyModel::sourceModelChanged, this, [=](){
connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &SortFilterProxyModel::countChanged);
connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SortFilterProxyModel::countChanged);
connect(sourceModel(), &QAbstractItemModel::modelReset, this, &SortFilterProxyModel::countChanged);
emit countChanged();
});
}
QString SortFilterProxyModel::filterRoleName() const
{
return m_filterRoleName;
}
void SortFilterProxyModel::setFilterRoleName(const QString &filterRoleName)
{
if (m_filterRoleName != filterRoleName) {
m_filterRoleName = filterRoleName;
emit filterRoleNameChanged();
invalidateFilter();
emit countChanged();
}
}
QStringList SortFilterProxyModel::filterList() const
{
return m_filterList;
}
void SortFilterProxyModel::setFilterList(const QStringList &filterList)
{
if (m_filterList != filterList) {
m_filterList = filterList;
emit filterListChanged();
invalidateFilter();
emit countChanged();
}
}
QVariant SortFilterProxyModel::data(int row, const QString &role) const
{
int roleId = roleNames().key(role.toUtf8());
return QSortFilterProxyModel::data(index(row, 0), roleId);
}
bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
if (!m_filterList.isEmpty() && !m_filterRoleName.isEmpty()) {
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
int filterRole = sourceModel()->roleNames().key(m_filterRoleName.toUtf8());
QVariant data = sourceModel()->data(idx, filterRole);
if (!m_filterList.contains(data.toString())) {
return false;
}
}
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}

View File

@ -0,0 +1,37 @@
#ifndef SORTFILTERPROXYMODEL_H
#define SORTFILTERPROXYMODEL_H
#include <QSortFilterProxyModel>
class SortFilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(QString filterRoleName READ filterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged)
Q_PROPERTY(QStringList filterList READ filterList WRITE setFilterList NOTIFY filterListChanged)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
public:
explicit SortFilterProxyModel(QObject *parent = nullptr);
QString filterRoleName() const;
void setFilterRoleName(const QString &filterRoleName);
QStringList filterList() const;
void setFilterList(const QStringList &filterList);
Q_INVOKABLE QVariant data(int row, const QString &role) const;
signals:
void filterRoleNameChanged();
void filterListChanged();
void countChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
QString m_filterRoleName;
QStringList m_filterList;
};
#endif // SORTFILTERPROXYMODEL_H

View File

@ -129,7 +129,7 @@
<file>ui/images/send.svg</file>
<file>ui/images/sensors.svg</file>
<file>ui/images/settings.svg</file>
<file>ui/images/share.svg</file>
<file>ui/images/things.svg</file>
<file>ui/images/slideshow.svg</file>
<file>ui/images/closable-move.svg</file>
<file>ui/images/starred.svg</file>
@ -234,5 +234,6 @@
<file>ui/images/garage/garage-100.svg</file>
<file>ui/images/navigationpad.svg</file>
<file>ui/images/qrcode.svg</file>
<file>ui/images/energy.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1,12 @@
#include "mainmenumodel.h"
MainMenuModel::MainMenuModel(QObject *parent) : QAbstractListModel(parent)
{
}
int MainMenuModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_list.count();
}

24
nymea-app/mainmenumodel.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef MAINMENUMODEL_H
#define MAINMENUMODEL_H
#include <QAbstractListModel>
class MainMenuItem: public QObject
{
Q_OBJECT
};
class MainMenuModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit MainMenuModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
private:
QList<MainMenuItem*> m_list;
};
#endif // MAINMENUMODEL_H

View File

@ -13,6 +13,7 @@ linux:!android:!nozeroconf:LIBS += -lavahi-client -lavahi-common
PRE_TARGETDEPS += ../libnymea-app
HEADERS += \
mainmenumodel.h \
platformintegration/generic/raspberrypihelper.h \
stylecontroller.h \
pushnotifications.h \
@ -22,6 +23,7 @@ HEADERS += \
ruletemplates/messages.h
SOURCES += main.cpp \
mainmenumodel.cpp \
platformintegration/generic/raspberrypihelper.cpp \
stylecontroller.cpp \
pushnotifications.cpp \

View File

@ -10,8 +10,7 @@
<file>ui/RootItem.qml</file>
<file>ui/mainviews/ScenesView.qml</file>
<file>ui/mainviews/FavoritesView.qml</file>
<file>ui/mainviews/DevicesPageDelegate.qml</file>
<file>ui/mainviews/DevicesPage.qml</file>
<file>ui/mainviews/ThingsView.qml</file>
<file>ui/connection/ConnectPage.qml</file>
<file>ui/connection/ManualConnectPage.qml</file>
<file>ui/connection/ConnectingPage.qml</file>
@ -108,6 +107,7 @@
<file>ui/delegates/ParamDelegate.qml</file>
<file>ui/delegates/ActionDelegate.qml</file>
<file>ui/delegates/ThingDelegate.qml</file>
<file>ui/delegates/InterfaceTile.qml</file>
<file>ui/system/LogViewerPage.qml</file>
<file>ui/system/PluginsPage.qml</file>
<file>ui/system/PluginParamsPage.qml</file>
@ -218,5 +218,9 @@
<file>ui/thingconfiguration/ThingClassDetailsPage.qml</file>
<file>ui/components/ClosablesControlLarge.qml</file>
<file>ui/devicepages/BarcodeScannerThingPage.qml</file>
<file>ui/mainviews/GaragesView.qml</file>
<file>ui/mainviews/EnergyView.qml</file>
<file>ui/components/MainViewBase.qml</file>
<file>ui/components/SmartMeterChart.qml</file>
</qresource>
</RCC>

View File

@ -33,6 +33,8 @@ import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import QtQuick.Window 2.3
import Qt.labs.settings 1.0
import Qt.labs.folderlistmodel 2.2
import Nymea 1.0
import "components"
import "delegates"
@ -42,7 +44,8 @@ Page {
id: root
header: FancyHeader {
title: swipeView.currentItem.title
id: mainHeader
title: filteredContentModel.data(swipeView.currentIndex, "displayName")
leftButtonVisible: true
leftButtonImageSource: {
switch (engine.jsonRpcClient.currentConnection.bearerType) {
@ -65,10 +68,14 @@ Page {
var dialog = connectionDialogComponent.createObject(root)
dialog.open();
}
onMenuOpenChanged: {
if (menuOpen && d.configOverlay) {
d.configOverlay.destroy()
}
}
model: ListModel {
ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "thingconfiguration/EditThingsPage.qml" }
ListElement { iconSource: "../images/things.svg"; text: qsTr("Configure things"); page: "thingconfiguration/EditThingsPage.qml" }
ListElement { iconSource: "../images/magic.svg"; text: qsTr("Magic"); page: "MagicPage.qml" }
ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" }
ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" }
@ -79,84 +86,6 @@ Page {
}
}
property int currentViewIndex: 0
property bool swipeViewReady: false
property bool tabsReady: false
// FIXME: All this can go away when we require Controls 2.3 (Qt 5.10) or greater as TabBar got a major rework there.
// Ideally we'd just list the 3 items and set visible to false if the server version isn't good enough but TabBar
// has troubles dealing with that. For now, let's manually fill it and use a timer to initialize the currentIndex.
Component.onCompleted: {
// Fill SwipeView (The 2 static views things and scenes will already be there).
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
swipeView.insertItem(0, favoritesViewComponent.createObject(swipeView))
}
var experienceView = null;
if (styleController.currentExperience != "Default") {
experienceView = experienceViewComponent.createObject(swipeView, {source: "experiences/" + styleController.currentExperience + "/Main.qml" });
swipeView.insertItem(0, experienceView)
}
root.swipeViewReady = true;
var pi = 0;
if (experienceView) {
tabEntryComponent.createObject(tabBar, {text: experienceView.title, iconSource: experienceView.icon, pageIndex: pi++})
}
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
tabEntryComponent.createObject(tabBar, {text: qsTr("Favorites"), iconSource: "../images/starred.svg", pageIndex: pi++})
}
tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++})
tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++})
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
tabEntryComponent.createObject(tabBar, {text: qsTr("Groups"), iconSource: "../images/view-grid-symbolic.svg", pageIndex: pi++})
}
root.tabsReady = true
}
readonly property bool viewReady: swipeViewReady && tabsReady
onViewReadyChanged: {
if (tabSettings.currentMainViewIndex > swipeView.count) {
tabSettings.currentMainViewIndex = swipeView.count - 1;
}
// Load current index from settings
currentViewIndex = tabSettings.currentMainViewIndex;
// If setting is not initialized yet, init to "Things" page (might be 0 or 1, depending whether we have tags support)
if (currentViewIndex === -1) {
currentViewIndex = engine.jsonRpcClient.ensureServerVersion(1.6) ? 1 : 0
}
// and set up a binding to sync changes back to the settings
tabSettings.currentMainViewIndex = Qt.binding(function() { return root.currentViewIndex; });
// Tabbar gets a little confused if it's bound to it before the init happened, do it now
tabBar.currentIndex = Qt.binding(function() { return root.currentViewIndex; });
}
// FIXME: Currently we don't have any feedback for executeAction
// we don't want all the results, e.g. on looped calls like "all off"
// Connections {
// target: engine.deviceManager
// onExecuteActionReply: {
// var text = params["deviceError"]
// switch(text) {
// case "DeviceErrorNoError":
// return;
// case "DeviceErrorHardwareNotAvailable":
// text = qsTr("Could not execute action. The thing is not available");
// break;
// }
// var errorDialog = Qt.createComponent(Qt.resolvedUrl("components/ErrorDialog.qml"))
// var popup = errorDialog.createObject(root, {text: text})
// popup.open()
// }
// }
Connections {
target: engine.ruleManager
onAddRuleReply: {
@ -170,6 +99,69 @@ Page {
QtObject {
id: d
property var editRulePage: null
property var configOverlay: null
}
Settings {
id: mainViewSettings
category: engine.jsonRpcClient.currentHost.uuid
property string mainMenuContent: ""
property var sortOrder: []
property var filterList: ["things"]
property int currentIndex: 0
}
ListModel {
id: mainMenuBaseModel
// TODO: Should read this from disk somehow maybe?
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" }
}
ListModel {
id: mainMenuModel
ListElement { name: "dummy"; source: "Dummy"; displayName: ""; icon: "" }
Component.onCompleted: {
var configList = {}
var newList = {}
var newItems = 0
for (var i = 0; i < mainMenuBaseModel.count; i++) {
var item = mainMenuBaseModel.get(i);
var idx = mainViewSettings.sortOrder.indexOf(item.name);
if (idx === -1) {
newList[newItems++] = item;
} else {
configList[idx] = item;
}
}
clear();
for (idx in configList) {
item = configList[idx];
mainMenuModel.append(item)
}
for (idx in newList) {
item = newList[idx];
mainMenuModel.append(item)
}
tabBar.currentIndex = Qt.binding(function() { return mainViewSettings.currentIndex; })
swipeView.currentIndex = Qt.binding(function() { return tabBar.currentIndex; })
mainViewSettings.currentIndex = Qt.binding(function() { return swipeView.currentIndex; })
}
}
SortFilterProxyModel {
id: filteredContentModel
sourceModel: mainMenuModel
filterList: mainViewSettings.filterList
filterRoleName: "name"
}
ColumnLayout {
@ -226,8 +218,8 @@ Page {
}
}
Item {
id: contentContainer
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
@ -235,131 +227,17 @@ Page {
SwipeView {
id: swipeView
anchors.fill: parent
currentIndex: root.currentViewIndex
opacity: d.configOverlay === null ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
onCurrentIndexChanged: {
root.currentViewIndex = currentIndex
}
Repeater {
model: filteredContentModel
Component {
id: experienceViewComponent
Loader {
delegate: Loader {
width: swipeView.width
height: swipeView.height
clip: true
readonly property string title: item ? item.title : ""
readonly property string icon: item ? item.icon : ""
}
}
Component {
id: favoritesViewComponent
FavoritesView {
id: favoritesView
objectName: "favorites"
width: swipeView.width
height: swipeView.height
property string title: qsTr("My favorites")
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: favoritesView.count === 0 && !engine.deviceManager.fetchingData
title: qsTr("There are no favorite things yet.")
text: engine.deviceManager.devices.count === 0 ?
qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") :
qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.")
imageSource: "images/starred.svg"
buttonVisible: engine.deviceManager.devices.count === 0
buttonText: qsTr("Add a thing")
onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
}
}
}
DevicesPage {
property string title: qsTr("My things")
width: swipeView.width
height: swipeView.height
model: InterfacesSortModel {
interfacesModel: InterfacesModel {
engine: _engine
devices: DevicesProxy {
engine: _engine
}
shownInterfaces: app.supportedInterfaces
showUncategorized: true
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: engine.deviceManager.devices.count === 0 && !engine.deviceManager.fetchingData
title: qsTr("Welcome to %1!").arg(app.systemName)
// Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice
text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 system to be useful, go ahead and add some things.").arg(app.systemName)
imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
buttonText: qsTr("Add a thing")
onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
}
}
ScenesView {
id: scenesView
property string title: qsTr("My scenes");
width: swipeView.width
height: swipeView.height
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: scenesView.count === 0 && !engine.deviceManager.fetchingData
title: qsTr("There are no scenes set up yet.")
text: engine.deviceManager.devices.count === 0 ?
qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") :
qsTr("Scenes provide a useful way to control your things with just one click.")
imageSource: "images/slideshow.svg"
buttonText: engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene")
onButtonClicked: {
if (engine.deviceManager.devices.count === 0) {
pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
} else {
var newRule = engine.ruleManager.createNewRule();
d.editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: newRule });
d.editRulePage.startAddAction();
d.editRulePage.StackView.onRemoved.connect(function() {
newRule.destroy();
})
d.editRulePage.onAccept.connect(function() {
d.editRulePage.busy = true;
engine.ruleManager.addRule(d.editRulePage.rule);
})
d.editRulePage.onCancel.connect(function() {
pageStack.pop();
})
}
}
}
}
GroupsView {
id: groupsView
property string title: qsTr("My groups");
width: swipeView.width
height: swipeView.height
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: groupsView.count == 0 && !engine.deviceManager.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"
buttonVisible: false
// buttonText: qsTr("Create a group")
// onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml"))
source: "mainviews/" + model.source + ".qml"
}
}
}
@ -383,19 +261,300 @@ Page {
}
}
footer: TabBar {
id: tabBar
Material.elevation: 3
position: TabBar.Footer
implicitHeight: 70 + (app.landscape ? -20 : 0)
footer: Item {
readonly property bool shown: tabsRepeater.count > 1 || mainHeader.menuOpen || d.configOverlay
implicitHeight: shown ? 70 + (app.landscape ? -20 : 0) : 0
Behavior on implicitHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }}
clip: true
Component {
id: tabEntryComponent
MainPageTabButton {
property int pageIndex: 0
// height: tabBar.height
onClicked: root.currentViewIndex = pageIndex
alignment: app.landscape ? Qt.Horizontal : Qt.Vertical
TabBar {
id: tabBar
anchors.fill: parent
Material.elevation: 3
position: TabBar.Footer
visible: !mainHeader.menuOpen && !d.configOverlay
opacity: d.configOverlay === null ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
Repeater {
id: tabsRepeater
model: filteredContentModel
delegate: MainPageTabButton {
alignment: app.landscape ? Qt.Horizontal : Qt.Vertical
height: tabBar.height
anchors.verticalCenter: parent.verticalCenter
text: model.displayName
iconSource: "../images/" + model.icon + ".svg"
onPressAndHold: {
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
d.configOverlay = configComponent.createObject(contentContainer)
mainHeader.menuOpen = false;
}
}
}
}
MainPageTabButton {
anchors.fill: parent
alignment: app.landscape ? Qt.Horizontal : Qt.Vertical
text: d.configOverlay ? qsTr("Done") : qsTr("Configure")
iconSource: "../images/configure.svg"
opacity: visible ? 1 : 0
visible: mainHeader.menuOpen || d.configOverlay
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
checked: false
checkable: false
onClicked: {
if (d.configOverlay) {
d.configOverlay.destroy()
} else {
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
d.configOverlay = configComponent.createObject(contentContainer)
mainHeader.menuOpen = false;
}
}
}
}
Component {
id: configComponent
Item {
id: configOverlay
width: contentContainer.width
height: contentContainer.height
NumberAnimation {
target: configOverlay
property: "scale"
duration: 200
easing.type: Easing.InOutQuad
from: 2
to: 1
running: true
}
NumberAnimation {
target: configOverlay
property: "opacity"
duration: 200
easing.type: Easing.InOutQuad
from: 0
to: 1
running: true
}
ListView {
id: configListView
model: mainMenuModel
width: parent.width
height: parent.height / 2.5
anchors.centerIn: parent
orientation: ListView.Horizontal
moveDisplaced: Transition {
NumberAnimation { properties: "x,y"; duration: 200 }
}
property int delegateWidth: width / 2.5
property bool dragging: draggingIndex >= 0
property int draggingIndex : -1
MouseArea {
id: dndArea
anchors.fill: parent
preventStealing: configListView.dragging
property int dragOffset: 0
onPressAndHold: {
mouse.accepted = true
var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x;
configListView.draggingIndex = configListView.indexAt(mouseXInListView, mouseY)
var item = mainMenuModel.get(configListView.draggingIndex)
dndItem.displayName = item.displayName
dndItem.icon = item.icon
var visualItem = configListView.itemAt(mouseXInListView, mouseY)
dndItem.isEnabled = visualItem.isEnabled
dndArea.dragOffset = configListView.mapToItem(visualItem, mouseX, mouseY).x
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact)
}
onMouseYChanged: {
if (configListView.dragging) {
var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x;
var indexUnderMouse = configListView.indexAt(mouseXInListView - dndArea.dragOffset / 2, mouseY)
indexUnderMouse = Math.min(Math.max(0, indexUnderMouse), configListView.count - 1)
if (configListView.draggingIndex !== indexUnderMouse) {
PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection)
mainMenuModel.move(configListView.draggingIndex, indexUnderMouse, 1)
configListView.draggingIndex = indexUnderMouse;
}
}
}
onReleased: {
print("released!")
var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x;
var clickedIndex = configListView.indexAt(mouseXInListView, mouseY)
var item = mainMenuModel.get(clickedIndex)
var isEnabled = mainViewSettings.filterList.indexOf(item.name) >= 0;
if (!configListView.dragging) {
var newList = []
for (var i = 0; i < mainMenuModel.count; i++) {
var entry = mainMenuModel.get(i).name;
if (entry === item.name) {
if (!isEnabled) {
newList.push(item.name)
}
} else {
if (mainViewSettings.filterList.indexOf(entry) >= 0) {
newList.push(entry)
}
}
}
if (newList.length === 0) {
newList.push("things")
}
mainViewSettings.filterList = newList
}
configListView.draggingIndex = -1;
var newSortOrder = []
for (var i = 0; i < mainMenuModel.count; i++) {
newSortOrder.push(mainMenuModel.get(i).name)
}
mainViewSettings.sortOrder = newSortOrder;
}
Timer {
id: scroller
interval: 2
repeat: true
running: direction != 0
property int direction: {
if (!configListView.dragging) {
return 0;
}
return dndArea.mouseX < 50 ? -1 : dndArea.mouseX > dndArea.width - 50 ? 1 : 0
}
onTriggered: {
configListView.contentX = Math.min(Math.max(0, configListView.contentX + direction), configListView.contentWidth - configListView.width)
}
}
}
delegate: Item {
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
contentItem: ItemDelegate {
anchors.fill: parent
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 ? app.accentColor : keyColor
}
}
Label {
text: model.displayName
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: app.largeFont
}
}
}
}
}
Item {
id: dndItem
width: configListView.delegateWidth
height: configListView.height
property bool isEnabled: false
property string displayName: ""
property string icon: "things"
visible: configListView.dragging
x: dndArea.mouseX - dndArea.dragOffset
onVisibleChanged: {
if (visible) {
dragStartAnimation.start();
}
}
NumberAnimation {
id: dragStartAnimation
target: dndItem
property: "scale"
from: 1
to: 0.9
duration: 200
}
Pane {
anchors.fill: parent
anchors.margins: app.margins / 2
Material.elevation: 2
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: ItemDelegate {
anchors.fill: parent
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/" + dndItem.icon + ".svg")
color: dndItem.isEnabled ? app.accentColor : keyColor
}
}
Label {
text: dndItem.displayName
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
font.pixelSize: app.largeFont
}
}
}
}
}
}
}
}
@ -453,7 +612,7 @@ Page {
font.pixelSize: app.smallFont
elide: Text.ElideRight
color: Material.color(Material.Grey)
// horizontalAlignment: Text.AlignHCenter
// horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
@ -461,7 +620,7 @@ Page {
font.pixelSize: app.smallFont
elide: Text.ElideRight
color: Material.color(Material.Grey)
// horizontalAlignment: Text.AlignHCenter
// horizontalAlignment: Text.AlignHCenter
}
}
ColorIcon {

View File

@ -33,6 +33,7 @@ import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import Qt.labs.settings 1.0
import Qt.labs.folderlistmodel 2.2
import QtQuick.Window 2.3
import Nymea 1.0
@ -533,6 +534,11 @@ ApplicationWindow {
onStateChanged: closeTimer.stop()
}
FolderListModel {
id: availableMainViews
folder: "mainviews"
showFiles: false
}
// NOTE: If using a Dialog, make sure closePolicy does not contain Dialog.CloseOnPressOutside
// or the virtual keyboard will close when pressing it...

View File

@ -48,7 +48,10 @@ Item {
id: image
anchors.fill: parent
anchors.margins: parent ? parent.margins : 0
source: width > 0 && height > 0 && icon.name ? icon.name : ""
source: width > 0 && height > 0 && icon.name ?
icon.name.endsWith(".svg") ? icon.name
: "qrc:/ui/images/" + icon.name + ".svg"
: ""
sourceSize {
width: width
height: height

View File

@ -35,7 +35,7 @@ import QtQuick.Controls.Material 2.1
ToolBar {
id: root
height: 50 + (d.menuOpen ? app.iconSize * 3 + app.margins / 2 : 0)
height: 50 + (menuOpen ? app.iconSize * 3 + app.margins / 2 : 0)
Behavior on height { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
property string title
@ -47,16 +47,13 @@ ToolBar {
signal clicked(int index);
signal leftButtonClicked();
QtObject {
id: d
property bool menuOpen: false
}
property bool menuOpen: false
RowLayout {
id: mainRow
height: 50
width: parent.width
opacity: d.menuOpen ? 0 : 1
opacity: menuOpen ? 0 : 1
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
HeaderButton {
@ -81,7 +78,7 @@ ToolBar {
HeaderButton {
id: menuButton
imageSource: "../images/navigation-menu.svg"
onClicked: d.menuOpen = true
onClicked: menuOpen = true
}
}
@ -89,7 +86,7 @@ ToolBar {
height: 50
anchors.bottom: menuPanel.top
width: parent.width
opacity: d.menuOpen ? 1 : 0
opacity: menuOpen ? 1 : 0
visible: opacity > 0
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
@ -106,7 +103,7 @@ ToolBar {
HeaderButton {
imageSource:"../images/close.svg"
onClicked: d.menuOpen = false
onClicked: menuOpen = false
}
}
@ -118,7 +115,7 @@ ToolBar {
width: Math.min(menuRow.childrenRect.width, parent.width)
height: app.iconSize * 3
contentWidth: menuRow.childrenRect.width
opacity: d.menuOpen ? 1 : 0
opacity: menuOpen ? 1 : 0
visible: opacity > 0
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
@ -132,7 +129,7 @@ ToolBar {
width: app.iconSize * 3
onClicked: {
d.menuOpen = false
menuOpen = false
root.clicked(index)
}

View File

@ -44,22 +44,27 @@ TabButton {
opacity: 0.05
}
contentItem: GridLayout {
columns: root.alignment === Qt.Vertical ? 1 : 2
rowSpacing: 4
ColorIcon {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: app.iconSize
Layout.alignment: Qt.AlignHCenter
name: root.iconSource
color: root.checked ? app.accentColor : keyColor
}
Label {
Layout.fillWidth: root.alignment === Qt.Vertical
text: root.text
horizontalAlignment: Text.AlignHCenter
font.pixelSize: app.smallFont
color: root.checked ? app.accentColor : Material.foreground
contentItem: Item {
height: root.height
Grid {
anchors.centerIn: parent
columns: root.alignment == Qt.Vertical ? 1 : 2
spacing: root.alignment == Qt.Horizontal ? app.margins : app.margins / 2
horizontalItemAlignment: Grid.AlignHCenter
verticalItemAlignment: Grid.AlignVCenter
ColorIcon {
width: app.iconSize
height: app.iconSize
name: root.iconSource
color: root.checked ? app.accentColor : keyColor
}
Label {
id: textLabel
text: root.text
font.pixelSize: app.smallFont
color: root.checked ? app.accentColor : Material.foreground
}
}
}
}

View File

@ -34,30 +34,13 @@ import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import Nymea 1.0
import "../components"
import "../delegates"
MouseArea {
id: root
property alias count: interfacesGridView.count
property alias model: interfacesGridView.model
// Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true
onWheel: wheel.accepted = true
GridView {
id: interfacesGridView
anchors.fill: parent
anchors.margins: app.margins / 2
readonly property int minTileWidth: 172
readonly property int tilesPerRow: root.width / minTileWidth
cellWidth: width / tilesPerRow
cellHeight: cellWidth
delegate: DevicesPageDelegate {
width: interfacesGridView.cellWidth
height: interfacesGridView.cellHeight
iface: Interfaces.findByName(model.name)
}
}
}

View File

@ -0,0 +1,127 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1
import QtCharts 2.2
import Nymea 1.0
ChartView {
id: chart
backgroundColor: app.backgroundColor
theme: ChartView.ChartThemeLight
legend.labelColor: app.foregroundColor
legend.font.pixelSize: app.smallFont
legend.alignment: Qt.AlignRight
titleColor: app.foregroundColor
property ThingsProxy meters: null
property int multiplier: 1
Connections {
target: meters
onCountChanged: chart.refresh()
}
Component.onCompleted: {
chart.refresh()
}
QtObject {
id: d
property var sliceMap: {}
}
function refresh() {
pieSeries.clear();
d.sliceMap = {}
print("calculating", chart.multiplier)
for (var i = 0; i < meters.count; i++) {
var thing = meters.get(i);
print("thing:", thing.name)
var value = 0;
var totalConsumedStateType = thing.thingClass.stateTypes.findByName("totalEnergyConsumed")
if (totalConsumedStateType) {
var totalConsumedState = thing.states.getState(totalConsumedStateType.id)
value = value + (totalConsumedState.value * chart.multiplier)
print("Adding", totalConsumedState.value * chart.multiplier, value)
}
var totalProducedStateType = thing.thingClass.stateTypes.findByName("totalEnergyProduced")
if (totalProducedStateType) {
var totalProducedState = thing.states.getState(totalProducedStateType.id)
value = value - (totalProducedState.value * chart.multiplier)
print("removing", totalProducedState.value * chart.multiplier, value)
}
print("consumed", totalConsumedState.value, "produced", totalProducedState.value)
print("value", value)
var slice = pieSeries.append(thing.name, Math.max(0, value))
var color = app.accentColor
for (var j = 0; j < i; j+=2) {
if (i % 2 == 0) {
color = Qt.lighter(color, 1.2);
} else {
color = Qt.darker(color, 1.2)
}
}
slice.color = color
d.sliceMap[slice] = i
}
}
PieSeries {
id: pieSeries
holeSize: 0.6
size: 0.8
onClicked: {
print("clicked slice", slice, d.sliceMap[slice], meters.get(d.sliceMap[slice]))
pageStack.push("../devicepages/SmartMeterDevicePage.qml", {device: meters.get(d.sliceMap[slice])})
}
}
ColumnLayout {
x: chart.plotArea.x + (chart.plotArea.width * 0.5) - (width / 2)
y: chart.plotArea.y + (chart.plotArea.height * 0.5) - (height / 2)
Label {
font.pixelSize: app.largeFont
Layout.alignment: Qt.AlignHCenter
text: Math.round(pieSeries.sum * 1000) / 1000
}
Label {
text: "KWh"
Layout.alignment: Qt.AlignHCenter
}
}
}

View File

@ -161,7 +161,7 @@ MainPageTile {
case "media":
return mediaControlComponent
default:
console.warn("DevicesPageDelegate, inlineControl: Unhandled interface", iface.name)
console.warn("InterfaceTile, inlineControl: Unhandled interface", iface.name)
}
}
@ -267,7 +267,7 @@ MainPageTile {
if (thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("garagegate") >= 0) {
statefulCount++;
var stateType = thing.thingClass.stateTypes.findByName("state");
if (stateType && device.states.getState(stateType.id).value !== "closed") {
if (stateType && thing.states.getState(stateType.id).value !== "closed") {
count++;
}
}
@ -285,7 +285,7 @@ MainPageTile {
return ""
// return qsTr("%1 installed").arg(devicesProxy.count)
}
console.warn("DevicesPageDelegate, inlineButtonControl: Unhandled interface", model.name)
console.warn("InterfaceTile, inlineButtonControl: Unhandled interface", model.name)
}
font.pixelSize: app.smallFont
elide: Text.ElideRight
@ -329,7 +329,7 @@ MainPageTile {
case "extendedshutter":
return "../images/up.svg"
default:
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name)
console.warn("InterfaceTile", "inlineButtonControl image: Unhandled interface", iface.name)
}
return ""
}
@ -370,7 +370,7 @@ MainPageTile {
}
break;
default:
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name)
console.warn("InterfaceTile:", "inlineButtonControl clicked: Unhandled interface", iface.name)
}
}
}
@ -409,7 +409,7 @@ MainPageTile {
case "extendedshutter":
return "../images/media-playback-stop.svg"
default:
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name)
console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name)
}
return "";
}
@ -450,7 +450,7 @@ MainPageTile {
}
break;
default:
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name)
console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name)
}
}
}
@ -500,7 +500,7 @@ MainPageTile {
case "extendedshutter":
return "../images/down.svg"
default:
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name)
console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name)
}
}
}
@ -585,7 +585,7 @@ MainPageTile {
}
default:
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name)
console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name)
}
}
}

View File

@ -71,12 +71,12 @@ Page {
cellWidth: width / tilesPerRow
cellHeight: cellWidth
// delegate: DevicesPageDelegate {
// delegate: InterfaceTile {
// width: interfacesGridView.cellWidth
// height: interfacesGridView.cellHeight
// }
delegate: DevicesPageDelegate {
delegate: InterfaceTile {
width: interfacesGridView.cellWidth
height: interfacesGridView.cellHeight
iface: Interfaces.findByName(model.name)

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg id="svg4874" width="96" height="96" version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata id="metadata4879">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1" transform="translate(67.857 -78.505)">
<g id="g4845" transform="matrix(0 -1 -1 0 373.51 516.51)">
<g id="g4778" transform="matrix(-.9996 0 0 1 575.94 -611)">
<g id="g4780" transform="matrix(-1 0 0 1 576 611)">
<rect id="rect4782" transform="scale(-1,1)" x="-438" y="345.36" width="96.038" height="96" style="color:#000000;fill:none"/>
<path id="path4212" d="m341.96 389 56.75 24v-15.273h39.288l-56.75-24v15.273z" style="fill:#808080"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,91 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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"
MainViewBase {
id: root
ThingsProxy {
id: consumers
engine: _engine
shownInterfaces: ["smartmeterconsumer"]
}
ThingsProxy {
id: producers
engine: _engine
shownInterfaces: ["smartmeterproducer"]
}
EmptyViewPlaceholder {
anchors.centerIn: parent
width: parent.width - app.margins * 2
visible: !engine.thingManager.fetchingData && consumers.count == 0
title: qsTr("There are no energy meters installed.")
text: qsTr("To get an overview of your current energy usage, install some energy meters.")
imageSource: "../images/smartmeter.svg"
buttonText: qsTr("Add things")
}
Flickable {
anchors.fill: parent
contentHeight: energyGrid.childrenRect.height
GridLayout {
id: energyGrid
width: parent.width
visible: consumers.count > 0
columns: Math.floor(root.width / 300)
SmartMeterChart {
Layout.fillWidth: true
Layout.preferredHeight: width * .7
meters: consumers
title: qsTr("Total consumed energy")
visible: consumers.count > 0
}
SmartMeterChart {
Layout.fillWidth: true
Layout.preferredHeight: width * .7
meters: producers
title: qsTr("Total produced energy")
visible: producers.count > 0
multiplier: -1
}
}
}
}

View File

@ -36,15 +36,10 @@ import Nymea 1.0
import "../components"
import "../delegates"
MouseArea {
MainViewBase {
id: root
property bool editMode: false
readonly property int count: tagsProxy.count
// Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true
onWheel: wheel.accepted = true
TagsProxyModel {
id: tagsProxy
@ -167,4 +162,19 @@ MouseArea {
}
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: gridView.count === 0 && !engine.deviceManager.fetchingData
title: qsTr("There are no favorite things yet.")
text: engine.deviceManager.devices.count === 0 ?
qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") :
qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.")
imageSource: "../images/starred.svg"
buttonVisible: engine.deviceManager.devices.count === 0
buttonText: qsTr("Add a thing")
onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
}
}

View File

@ -0,0 +1,240 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.3
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.2
import "../components"
import Nymea 1.0
MainViewBase {
id: root
readonly property bool landscape: width > height
DevicesProxy {
id: garagesFilterModel
engine: _engine
shownInterfaces: ["garagedoor", "garagegate"]
}
EmptyViewPlaceholder {
anchors.centerIn: parent
width: parent.width - app.margins * 2
text: qsTr("There are no garage doors set up yet.")
imageSource: "qrc:/ui/images/garage/garage-100.svg"
buttonText: qsTr("Set up now")
visible: garagesFilterModel.count === 0 && !engine.thingManager.fetchingData
onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
}
SwipeView {
id: swipeView
anchors.fill: parent
Repeater {
model: garagesFilterModel
Item {
id: garageGateView
width: swipeView.width
height: swipeView.height
readonly property Device thing: garagesFilterModel.get(index)
readonly property bool isImpulseBased: thing.thingClass.interfaces.indexOf("impulsegaragedoor") >= 0
readonly property bool isStateful: thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0
readonly property bool isExtended: thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0
// Stateful garagedoor
readonly property StateType stateStateType: thing.thingClass.stateTypes.findByName("state")
readonly property State stateState: stateStateType ? thing.states.getState(stateStateType.id) : null
// Extended stateful garagedoor
readonly property StateType percentageStateType: thing.thingClass.stateTypes.findByName("percentage")
readonly property State percentageState: percentageStateType ? thing.states.getState(percentageStateType.id) : null
// Backward compatiblity with old garagegate interface
readonly property StateType intermediatePositionStateType: thing.thingClass.stateTypes.findByName("intermediatePosition")
readonly property var intermediatePositionState: intermediatePositionStateType ? device.states.getState(intermediatePositionStateType.id) : null
// Some garages may also implement the light interface
readonly property var lightStateType: thing.thingClass.stateTypes.findByName("power")
readonly property var lightState: lightStateType ? thing.states.getState(lightStateType.id) : null
ColumnLayout {
anchors.fill: parent
anchors.topMargin: app.margins
anchors.bottomMargin: app.margins
Label {
Layout.fillWidth: true
font.pixelSize: app.largeFont
text: garageGateView.thing.name
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
GridLayout {
columns: root.landscape ? 2 : 1
ColorIcon {
id: shutterImage
Layout.preferredWidth: root.landscape ?
Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) - app.margins
: Math.min(Math.min(parent.width, 500), parent.height - shutterControlsContainer.minimumHeight)
Layout.preferredHeight: width
Layout.alignment: Qt.AlignHCenter
property string currentImage: {
if (garageGateView.isExtended) {
return app.pad(Math.round(garageGateView.percentageState.value / 10), 2) + "0"
}
if (garageGateView.intermediatePositionStateType) {
return garageGateView.stateState.value === "closed" ? "100"
: garageGateView.intermediatePositionState.value === false ? "000" : "050"
}
return "100"
}
name: "../images/garage/garage-" + currentImage + ".svg"
Item {
id: arrows
anchors.centerIn: parent
width: app.iconSize * 2
height: parent.height * .6
clip: true
visible: garageGateView.stateStateType && (garageGateView.stateState.value === "opening" || garageGateView.stateState.value === "closing")
property bool up: garageGateView.stateState && garageGateView.stateState.value === "opening"
// NumberAnimation doesn't reload to/from while it's running. If we switch from closing to opening or vice versa
// we need to somehow stop and start the animation
property bool animationHack: true
onAnimationHackChanged: {
if (!animationHack) hackTimer.start();
}
Timer { id: hackTimer; interval: 1; onTriggered: arrows.animationHack = true }
Connections { target: garageGateView.stateState; onValueChanged: arrows.animationHack = false }
NumberAnimation {
target: arrowColumn
property: "y"
duration: 500
easing.type: Easing.Linear
from: arrows.up ? app.iconSize : -app.iconSize
to: arrows.up ? -app.iconSize : app.iconSize
loops: Animation.Infinite
running: arrows.animationHack && garageGateView.stateState && (garageGateView.stateState.value === "opening" || garageGateView.stateState.value === "closing")
}
Column {
id: arrowColumn
width: parent.width
Repeater {
model: arrows.height / app.iconSize + 1
ColorIcon {
name: arrows.up ? "../images/up.svg" : "../images/down.svg"
width: parent.width
height: width
color: app.accentColor
}
}
}
}
}
Item {
id: shutterControlsContainer
Layout.fillWidth: true
Layout.margins: app.margins * 2
Layout.fillHeight: true
property int minimumWidth: app.iconSize * 2.5 * (garageGateView.lightState ? 4 : 3)
property int minimumHeight: app.iconSize * 2.5
ItemDelegate {
height: app.iconSize * 2
width: height
anchors.centerIn: parent
visible: garageGateView.isImpulseBased
ColorIcon {
anchors.fill: parent
name: "../images/closable-move.svg"
anchors.margins: app.margins
}
onClicked: {
var actionTypeId = garageGateView.thing.thingClass.actionTypes.findByName("triggerImpulse").id
print("Triggering impulse", actionTypeId)
engine.thingManager.executeAction(garageGateView.thing.id, actionTypeId)
}
}
ShutterControls {
id: shutterControls
device: garageGateView.thing
anchors.centerIn: parent
spacing: (parent.width - app.iconSize*2*children.length) / (children.length - 1)
visible: !garageGateView.isImpulseBased
ItemDelegate {
width: app.iconSize * 2
height: width
visible: garageGateView.lightStateType !== null
ColorIcon {
anchors.fill: parent
anchors.margins: app.margins
name: "../images/light-" + (garageGateView.lightState && garageGateView.lightState.value === true ? "on" : "off") + ".svg"
color: garageGateView.lightState && garageGateView.lightState.value === true ? Material.accent : keyColor
}
onClicked: {
var params = [];
var param = {};
param["paramTypeId"] = garageGateView.lightStateType.id;
param["value"] = !garageGateView.lightState.value;
params.push(param)
engine.deviceManager.executeAction(garageGateView.device.id, garageGateView.lightStateType.id, params)
}
}
}
}
}
}
}
}
}
PageIndicator {
anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter }
count: garagesFilterModel.count
currentIndex: swipeView.currentIndex
}
}

View File

@ -35,12 +35,8 @@ import Nymea 1.0
import QtQuick.Controls.Material 2.2
import "../components"
MouseArea {
MainViewBase {
id: root
preventStealing: true
onWheel: wheel.accepted = true
readonly property int count: groupsGridView.count
GridView {
id: groupsGridView
@ -491,4 +487,15 @@ MouseArea {
device: mediaControllers.count > 0 ? mediaControllers.get(0) : null
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: groupsGridView.count == 0 && !engine.deviceManager.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"
buttonVisible: false
}
}

View File

@ -35,15 +35,9 @@ import Nymea 1.0
import QtQuick.Controls.Material 2.2
import "../components"
MouseArea {
MainViewBase {
id: root
readonly property int count: interfacesGridView.count
// Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true
onWheel: wheel.accepted = true
GridView {
id: interfacesGridView
anchors.fill: parent
@ -81,4 +75,37 @@ MouseArea {
}
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: interfacesGridView.count === 0 && !engine.deviceManager.fetchingData
title: qsTr("There are no scenes set up yet.")
text: engine.deviceManager.devices.count === 0 ?
qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") :
qsTr("Scenes provide a useful way to control your things with just one click.")
imageSource: "../images/slideshow.svg"
buttonText: engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene")
onButtonClicked: {
if (engine.deviceManager.devices.count === 0) {
pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
} else {
var newRule = engine.ruleManager.createNewRule();
d.editRulePage = pageStack.push(Qt.resolvedUrl("../magic/EditRulePage.qml"), {rule: newRule });
d.editRulePage.startAddAction();
d.editRulePage.StackView.onRemoved.connect(function() {
newRule.destroy();
})
d.editRulePage.onAccept.connect(function() {
d.editRulePage.busy = true;
engine.ruleManager.addRule(d.editRulePage.rule);
})
d.editRulePage.onCancel.connect(function() {
pageStack.pop();
})
}
}
}
}

View File

@ -0,0 +1,83 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2
import Nymea 1.0
import "../components"
import "../delegates"
MainViewBase {
id: root
InterfacesSortModel {
id: mainModel
interfacesModel: InterfacesModel {
engine: _engine
devices: DevicesProxy {
engine: _engine
}
shownInterfaces: app.supportedInterfaces
showUncategorized: true
}
}
GridView {
id: interfacesGridView
anchors.fill: parent
anchors.margins: app.margins / 2
model: mainModel
readonly property int minTileWidth: 172
readonly property int tilesPerRow: root.width / minTileWidth
cellWidth: width / tilesPerRow
cellHeight: cellWidth
delegate: InterfaceTile {
width: interfacesGridView.cellWidth
height: interfacesGridView.cellHeight
iface: Interfaces.findByName(model.name)
}
}
EmptyViewPlaceholder {
anchors { left: parent.left; right: parent.right; margins: app.margins }
anchors.verticalCenter: parent.verticalCenter
visible: engine.deviceManager.devices.count === 0 && !engine.deviceManager.fetchingData
title: qsTr("Welcome to %1!").arg(app.systemName)
// Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice
text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 system to be useful, go ahead and add some things.").arg(app.systemName)
imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
buttonText: qsTr("Add a thing")
onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml"))
}
}

View File

@ -309,7 +309,7 @@ Page {
}
BusyIndicator {
running: visible
anchors.horizontalCenter: parent.horizontalCenter
Layout.alignment: Qt.AlignHCenter
}
}