initial stab on grouping things

This commit is contained in:
Michael Zanetti 2019-11-29 17:44:05 +01:00
parent 1ab384428b
commit 37397e2c65
25 changed files with 971 additions and 38 deletions

View File

@ -23,6 +23,7 @@
#include "devicesproxy.h"
#include "engine.h"
#include "tagsmanager.h"
#include "types/tag.h"
DevicesProxy::DevicesProxy(QObject *parent) :
QSortFilterProxyModel(parent)
@ -93,7 +94,7 @@ QString DevicesProxy::filterTagId() const
void DevicesProxy::setFilterTagId(const QString &filterTag)
{
if (m_filterTagId != filterTagId()) {
if (m_filterTagId != filterTag) {
m_filterTagId = filterTag;
emit filterTagIdChanged();
invalidateFilter();
@ -101,6 +102,21 @@ void DevicesProxy::setFilterTagId(const QString &filterTag)
}
}
QString DevicesProxy::filterTagValue() const
{
return m_filterTagValue;
}
void DevicesProxy::setFilterTagValue(const QString &tagValue)
{
if (m_filterTagValue != tagValue) {
m_filterTagValue = tagValue;
emit filterTagValueChanged();
invalidateFilter();
emit countChanged();
}
}
QString DevicesProxy::filterDeviceClassId() const
{
return m_filterDeviceClassId;
@ -112,6 +128,22 @@ void DevicesProxy::setFilterDeviceClassId(const QString &filterDeviceClassId)
m_filterDeviceClassId = filterDeviceClassId;
emit filterDeviceClassIdChanged();
invalidateFilter();
emit countChanged();
}
}
QString DevicesProxy::filterDeviceId() const
{
return m_filterDeviceId;
}
void DevicesProxy::setFilterDeviceId(const QString &filterDeviceId)
{
if (m_filterDeviceId != filterDeviceId) {
m_filterDeviceId = filterDeviceId;
emit filterDeviceIdChanged();
invalidateFilter();
emit countChanged();
}
}
@ -156,7 +188,7 @@ void DevicesProxy::setNameFilter(const QString &nameFilter)
m_nameFilter = nameFilter;
emit nameFilterChanged();
invalidateFilter();
countChanged();
emit countChanged();
}
}
@ -201,6 +233,7 @@ void DevicesProxy::setGroupByInterface(bool groupByInterface)
m_groupByInterface = groupByInterface;
emit groupByInterfaceChanged();
invalidate();
emit countChanged();
}
}
@ -254,12 +287,21 @@ bool DevicesProxy::filterAcceptsRow(int source_row, const QModelIndex &source_pa
{
Device *device = getInternal(source_row);
if (!m_filterTagId.isEmpty()) {
if (!m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId)) {
Tag *tag = m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId);
if (!tag) {
return false;
}
if (!m_filterTagValue.isEmpty() && tag->value() != m_filterTagValue) {
return false;
}
}
if (!m_filterDeviceClassId.isEmpty()) {
if (device->deviceClassId() != m_filterDeviceClassId) {
if (device->deviceClassId() != QUuid(m_filterDeviceClassId)) {
return false;
}
}
if (!m_filterDeviceId.isEmpty()) {
if (device->id() != QUuid(m_filterDeviceId)) {
return false;
}
}

View File

@ -38,7 +38,9 @@ class DevicesProxy : public QSortFilterProxyModel
Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged)
Q_PROPERTY(DevicesProxy *parentProxy READ parentProxy WRITE setParentProxy NOTIFY parentProxyChanged)
Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged)
Q_PROPERTY(QString filterTagValue READ filterTagValue WRITE setFilterTagValue NOTIFY filterTagValueChanged)
Q_PROPERTY(QString filterDeviceClassId READ filterDeviceClassId WRITE setFilterDeviceClassId NOTIFY filterDeviceClassIdChanged)
Q_PROPERTY(QString filterDeviceId READ filterDeviceId WRITE setFilterDeviceId NOTIFY filterDeviceIdChanged)
Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged)
Q_PROPERTY(QStringList hiddenInterfaces READ hiddenInterfaces WRITE setHiddenInterfaces NOTIFY hiddenInterfacesChanged)
Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged)
@ -63,9 +65,15 @@ public:
QString filterTagId() const;
void setFilterTagId(const QString &filterTag);
QString filterTagValue() const;
void setFilterTagValue(const QString &tagValue);
QString filterDeviceClassId() const;
void setFilterDeviceClassId(const QString &filterDeviceClassId);
QString filterDeviceId() const;
void setFilterDeviceId(const QString &filterDeviceId);
QStringList shownInterfaces() const;
void setShownInterfaces(const QStringList &shownInterfaces);
@ -91,7 +99,9 @@ signals:
void engineChanged();
void parentProxyChanged();
void filterTagIdChanged();
void filterTagValueChanged();
void filterDeviceClassIdChanged();
void filterDeviceIdChanged();
void shownInterfacesChanged();
void hiddenInterfacesChanged();
void nameFilterChanged();
@ -106,7 +116,9 @@ private:
Engine *m_engine = nullptr;
DevicesProxy *m_parentProxy = nullptr;
QString m_filterTagId;
QString m_filterTagValue;
QString m_filterDeviceClassId;
QString m_filterDeviceId;
QStringList m_shownInterfaces;
QStringList m_hiddenInterfaces;
QString m_nameFilter;

View File

@ -48,6 +48,7 @@
#include "models/wirelessaccesspointsproxy.h"
#include "tagsmanager.h"
#include "models/tagsproxymodel.h"
#include "models/taglistmodel.h"
#include "types/tag.h"
#include "ruletemplates/ruletemplates.h"
#include "ruletemplates/ruletemplate.h"
@ -189,6 +190,7 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<Tags>(uri, 1, 0, "Tags", "Get it from TagsManager");
qmlRegisterUncreatableType<Tag>(uri, 1, 0, "Tag", "Get it from Tags");
qmlRegisterType<TagsProxyModel>(uri, 1, 0, "TagsProxyModel");
qmlRegisterType<TagListModel>(uri, 1, 0, "TagListModel");
qmlRegisterType<NetworkManagerController>(uri, 1, 0, "NetworkManagerController");
qmlRegisterType<BluetoothDiscovery>(uri, 1, 0, "BluetoothDiscovery");

View File

@ -47,6 +47,7 @@ SOURCES += \
deviceclassesproxy.cpp \
devicediscovery.cpp \
models/packagesfiltermodel.cpp \
models/taglistmodel.cpp \
vendorsproxy.cpp \
pluginsproxy.cpp \
interfacesmodel.cpp \
@ -109,6 +110,7 @@ HEADERS += \
deviceclassesproxy.h \
devicediscovery.h \
models/packagesfiltermodel.h \
models/taglistmodel.h \
vendorsproxy.h \
pluginsproxy.h \
interfacesmodel.h \

View File

@ -5,6 +5,7 @@
#include "types/device.h"
#include "devices.h"
#include "devicesproxy.h"
InterfacesProxy::InterfacesProxy(QObject *parent): QSortFilterProxyModel(parent)
{
@ -23,6 +24,7 @@ void InterfacesProxy::setShowEvents(bool showEvents)
m_showEvents = showEvents;
emit showEventsChanged();
invalidateFilter();
void countChanged();
}
}
@ -37,6 +39,7 @@ void InterfacesProxy::setShowActions(bool showActions)
m_showActions = showActions;
emit showActionsChanged();
invalidateFilter();
void countChanged();
}
}
@ -51,6 +54,7 @@ void InterfacesProxy::setShowStates(bool showStates)
m_showStates = showStates;
emit showStatesChanged();
invalidateFilter();
void countChanged();
}
}
@ -58,7 +62,6 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source
{
Q_UNUSED(source_parent)
QString interfaceName = m_interfaces->get(source_row)->name();
qDebug() << "filterAcceptsRow" << interfaceName << m_shownInterfaces;
if (!m_shownInterfaces.isEmpty()) {
if (!m_shownInterfaces.contains(interfaceName)) {
return false;
@ -83,6 +86,24 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source
return false;
}
}
if (m_devicesProxyFilter != nullptr) {
// TODO: This could be improved *a lot* by caching interfaces in the devices model...
bool found = false;
for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) {
Device *d = m_devicesProxyFilter->get(i);
if (!d->deviceClass()) {
qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name();
return false;
}
if (d->deviceClass()->interfaces().contains(interfaceName)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
Interface* iface = m_interfaces->get(source_row);
if (m_showEvents) {

View File

@ -4,15 +4,18 @@
#include <QSortFilterProxyModel>
class Devices;
class DevicesProxy;
class Interface;
class Interfaces;
class InterfacesProxy: public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged)
Q_PROPERTY(Devices* devicesFilter READ devicesFilter WRITE setDevicesFilter NOTIFY devicesFilterChanged)
Q_PROPERTY(DevicesProxy* devicesProxyFilter READ devicesProxyFilter WRITE setDevicesProxyFilter NOTIFY devicesProxyFilterChanged)
Q_PROPERTY(bool showEvents READ showEvents WRITE setShowEvents NOTIFY showEventsChanged)
Q_PROPERTY(bool showActions READ showActions WRITE setShowActions NOTIFY showActionsChanged)
Q_PROPERTY(bool showStates READ showStates WRITE setShowStates NOTIFY showStatesChanged)
@ -26,6 +29,9 @@ public:
Devices* devicesFilter() const { return m_devicesFilter; }
void setDevicesFilter(Devices *devices) { m_devicesFilter = devices; emit devicesFilterChanged(); invalidateFilter(); }
DevicesProxy* devicesProxyFilter() const { return m_devicesProxyFilter; }
void setDevicesProxyFilter(DevicesProxy *devicesProxy) { m_devicesProxyFilter = devicesProxy; emit devicesProxyFilterChanged(); invalidateFilter(); }
bool showEvents() const;
void setShowEvents(bool showEvents);
@ -42,14 +48,18 @@ public:
signals:
void shownInterfacesChanged();
void devicesFilterChanged();
void devicesProxyFilterChanged();
void showEventsChanged();
void showActionsChanged();
void showStatesChanged();
void countChanged();
private:
Interfaces *m_interfaces = nullptr;
QStringList m_shownInterfaces;
Devices* m_devicesFilter = nullptr;
DevicesProxy* m_devicesProxyFilter = nullptr;
bool m_showEvents = false;
bool m_showActions = false;
bool m_showStates = false;

View File

@ -0,0 +1,100 @@
#include "taglistmodel.h"
#include "tagsproxymodel.h"
#include "types/tag.h"
#include <QDebug>
TagListModel::TagListModel(QObject *parent) : QAbstractListModel(parent)
{
}
TagsProxyModel *TagListModel::tagsProxy() const
{
return m_tagsProxy;
}
void TagListModel::setTagsProxy(TagsProxyModel *tagsProxy)
{
if (m_tagsProxy != tagsProxy) {
m_tagsProxy = tagsProxy;
emit tagsProxyChanged();
connect(tagsProxy, &TagsProxyModel::countChanged, this, &TagListModel::update);
update();
}
}
int TagListModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_list.count();
}
QVariant TagListModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case RoleTagId:
return m_list.at(index.row())->tagId();
case RoleValue:
return m_list.at(index.row())->value();
}
return QVariant();
}
QHash<int, QByteArray> TagListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(RoleTagId, "tagId");
roles.insert(RoleValue, "value");
return roles;
}
bool TagListModel::containsId(const QString &tagId)
{
foreach (Tag* t, m_list) {
if (t->tagId() == tagId) {
return true;
}
}
return false;
}
bool TagListModel::containsValue(const QString &tagValue)
{
foreach (Tag* t, m_list) {
if (t->value() == tagValue) {
return true;
}
}
return false;
}
void TagListModel::update()
{
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
for (int i = 0; i < m_tagsProxy->rowCount(); i++) {
Tag *tag = m_tagsProxy->get(i);
bool found = false;
foreach (Tag* existingTag, m_list) {
if (tag->tagId() == existingTag->tagId() && tag->value() == existingTag->value()) {
found = true;
break;
}
}
if (!found) {
Tag *t = new Tag(tag->tagId(), tag->value(), this);
m_list.append(t);
}
}
qDebug() << "Model populated" << m_list.count() << this;
endResetModel();
emit countChanged();
}

View File

@ -0,0 +1,44 @@
#ifndef TAGLISTMODEL_H
#define TAGLISTMODEL_H
#include <QAbstractListModel>
class TagsProxyModel;
class Tag;
class TagListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(TagsProxyModel* tagsProxy READ tagsProxy WRITE setTagsProxy NOTIFY tagsProxyChanged)
public:
enum Roles {
RoleTagId,
RoleValue
};
explicit TagListModel(QObject *parent = nullptr);
TagsProxyModel* tagsProxy() const;
void setTagsProxy(TagsProxyModel* tagsProxy);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE bool containsId(const QString &tagId);
Q_INVOKABLE bool containsValue(const QString &tagValue);
signals:
void countChanged();
void tagsProxyChanged();
private slots:
void update();
private:
TagsProxyModel *m_tagsProxy = nullptr;
QList<Tag*> m_list;
};
#endif // TAGLISTMODEL_H

View File

@ -94,17 +94,18 @@ bool TagsProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_
Q_UNUSED(source_parent)
Tag *tag = m_tags->get(source_row);
if (!m_filterTagId.isEmpty()) {
if (tag->tagId() != m_filterTagId) {
QRegExp exp(m_filterTagId);
if (!exp.exactMatch(tag->tagId())) {
return false;
}
}
if (!m_filterDeviceId.isEmpty()) {
if (tag->deviceId() != m_filterDeviceId) {
if (QUuid(tag->deviceId()) != QUuid(m_filterDeviceId)) {
return false;
}
}
if (!m_filterRuleId.isEmpty()) {
if (tag->ruleId() != m_filterRuleId) {
if (QUuid(tag->ruleId()) != QUuid(m_filterRuleId)) {
return false;
}
}
@ -115,7 +116,7 @@ bool TagsProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex
{
QString leftValue = m_tags->get(source_left.row())->value();
QString rightValue = m_tags->get(source_right.row())->value();
bool okLeft, okRight;;
bool okLeft, okRight;
qlonglong leftAsNumber = leftValue.toLongLong(&okLeft);
qlonglong rightAsNumber = rightValue.toLongLong(&okRight);
if (okLeft && okRight) {

View File

@ -42,6 +42,7 @@ signals:
void filterTagIdChanged();
void filterDeviceIdChanged();
void filterRuleIdChanged();
void groupSameTagsChanged();
void countChanged();
private:

View File

@ -17,10 +17,17 @@ QString TagsManager::nameSpace() const
void TagsManager::init()
{
m_busy = true;
emit busyChanged();
m_tags->clear();
m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply");
}
bool TagsManager::busy() const
{
return m_busy;
}
Tags *TagsManager::tags() const
{
return m_tags;
@ -83,13 +90,16 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
QString notification = params.value("notification").toString();
if (notification == "Tags.TagAdded") {
addTagInternal(tagMap);
Tag *tag = unpackTag(tagMap);
if (tag) {
m_tags->addTag(tag);
}
} else if (notification == "Tags.TagRemoved") {
for (int i = 0; i < m_tags->rowCount(); i++) {
Tag* tag = m_tags->get(i);
if (tagMap.value("deviceId").toString() == tag->deviceId() &&
tagMap.value("ruleId").toString() == tag->ruleId() &&
if (tagMap.value("deviceId").toUuid() == tag->deviceId() &&
tagMap.value("ruleId").toUuid() == tag->ruleId() &&
tagMap.value("tagId").toString() == tag->tagId()) {
m_tags->removeTag(tag);
return;
@ -99,8 +109,8 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
qDebug() << "tag value changed";
for (int i = 0; i < m_tags->rowCount(); i++) {
Tag* tag = m_tags->get(i);
if (tagMap.value("deviceId").toString() == tag->deviceId() &&
tagMap.value("ruleId").toString() == tag->ruleId() &&
if (tagMap.value("deviceId").toUuid() == tag->deviceId() &&
tagMap.value("ruleId").toUuid() == tag->ruleId() &&
tagMap.value("tagId").toString() == tag->tagId()) {
qDebug() << "Found tag";
tag->setValue(tagMap.value("value").toString());
@ -111,10 +121,18 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
void TagsManager::getTagsReply(const QVariantMap &params)
{
QList<Tag*> tags;
foreach (const QVariant &tagVariant, params.value("params").toMap().value("tags").toList()) {
addTagInternal(tagVariant.toMap());
qDebug() << "aDDING TAG";
Tag *tag = unpackTag(tagVariant.toMap());
if (tag) {
tags.append(tag);
}
}
emit tagsChanged();
m_tags->addTags(tags);
m_busy = false;
emit busyChanged();
}
void TagsManager::addTagReply(const QVariantMap &params)
@ -127,7 +145,7 @@ void TagsManager::removeTagReply(const QVariantMap &params)
qDebug() << "RemoveTag reply" << params;
}
void TagsManager::addTagInternal(const QVariantMap &tagMap)
Tag* TagsManager::unpackTag(const QVariantMap &tagMap)
{
QString deviceId = tagMap.value("deviceId").toString();
QString ruleId = tagMap.value("ruleId").toString();
@ -143,8 +161,8 @@ void TagsManager::addTagInternal(const QVariantMap &tagMap)
} else {
qWarning() << "Invalid tag. Neither deviceId nor ruleId are set. Skipping...";
tag->deleteLater();
return;
return nullptr;
}
// qDebug() << "adding tag" << tag->tagId() << tag->value();
m_tags->addTag(tag);
return tag;
}

View File

@ -9,13 +9,15 @@
class TagsManager : public JsonHandler
{
Q_OBJECT
Q_PROPERTY(Tags* tags READ tags NOTIFY tagsChanged)
Q_PROPERTY(Tags* tags READ tags CONSTANT)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr);
QString nameSpace() const override;
void init();
bool busy() const;
Tags* tags() const;
@ -25,7 +27,7 @@ public:
Q_INVOKABLE void untagRule(const QString &ruleId, const QString &tagId);
signals:
void tagsChanged();
void busyChanged();
private slots:
void handleTagsNotification(const QVariantMap &params);
@ -34,11 +36,12 @@ private slots:
void removeTagReply(const QVariantMap &params);
private:
void addTagInternal(const QVariantMap &tagMap);
Tag *unpackTag(const QVariantMap &tagMap);
JsonRpcClient *m_jsonClient = nullptr;
Tags *m_tags = nullptr;
bool m_busy = false;
};
#endif // TAGSMANAGER_H

View File

@ -89,6 +89,11 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent)
tr("Daylight"),
tr("Daylight changed"));
addInterface("lightsensor", tr("Light intensity sensors"));
addStateType("lightsensor", "lightIntensity", QVariant::Bool, false,
tr("Light intensity"),
tr("Light intensity changed"));
addInterface("evcharger", tr("EV charger"));
addStateType("evcharger", "power", QVariant::Bool, true,
tr("Charging"),

View File

@ -10,22 +10,22 @@ Tag::Tag(const QString &tagId, const QString &value, QObject *parent):
}
QString Tag::deviceId() const
QUuid Tag::deviceId() const
{
return m_deviceId;
}
void Tag::setDeviceId(const QString &deviceId)
void Tag::setDeviceId(const QUuid &deviceId)
{
m_deviceId = deviceId;
}
QString Tag::ruleId() const
QUuid Tag::ruleId() const
{
return m_ruleId;
}
void Tag::setRuleId(const QString &ruleId)
void Tag::setRuleId(const QUuid &ruleId)
{
m_ruleId = ruleId;
}

View File

@ -2,23 +2,24 @@
#define TAG_H
#include <QObject>
#include <QUuid>
class Tag : public QObject
{
Q_OBJECT
Q_PROPERTY(QString deviceId READ deviceId CONSTANT)
Q_PROPERTY(QString ruleId READ ruleId CONSTANT)
Q_PROPERTY(QUuid deviceId READ deviceId CONSTANT)
Q_PROPERTY(QUuid ruleId READ ruleId CONSTANT)
Q_PROPERTY(QString tagId READ tagId CONSTANT)
Q_PROPERTY(QString value READ value NOTIFY valueChanged)
public:
explicit Tag(const QString &tagId, const QString &value, QObject *parent = nullptr);
QString deviceId() const;
void setDeviceId(const QString &deviceId);
QUuid deviceId() const;
void setDeviceId(const QUuid &deviceId);
QString ruleId() const;
void setRuleId(const QString &ruleId);
QUuid ruleId() const;
void setRuleId(const QUuid &ruleId);
QString tagId() const;
@ -29,8 +30,8 @@ signals:
void valueChanged();
private:
QString m_deviceId;
QString m_ruleId;
QUuid m_deviceId;
QUuid m_ruleId;
QString m_tagId;
QString m_value;
};

View File

@ -46,6 +46,19 @@ void Tags::addTag(Tag *tag)
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
m_list.append(tag);
endInsertRows();
qDebug() << "tags count changed";
emit countChanged();
}
void Tags::addTags(QList<Tag *> tags)
{
beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + tags.count() - 1);
foreach (Tag *tag, tags) {
tag->setParent(this);
connect(tag, &Tag::valueChanged, this, &Tags::tagValueChanged);
}
m_list.append(tags);
endInsertRows();
emit countChanged();
}
@ -68,7 +81,7 @@ Tag *Tags::get(int index) const
return m_list.at(index);
}
Tag *Tags::findDeviceTag(const QString &deviceId, const QString &tagId) const
Tag *Tags::findDeviceTag(const QUuid &deviceId, const QString &tagId) const
{
foreach (Tag *tag, m_list) {
if (tag->deviceId() == deviceId && tag->tagId() == tagId) {

View File

@ -25,11 +25,12 @@ public:
QHash<int, QByteArray> roleNames() const override;
void addTag(Tag *tag);
void addTags(QList<Tag*> tags);
void removeTag(Tag *tag);
Tag* get(int index) const;
Q_INVOKABLE Tag* findDeviceTag(const QString &deviceId, const QString &tagId) const;
Q_INVOKABLE Tag* findDeviceTag(const QUuid &deviceId, const QString &tagId) const;
Q_INVOKABLE Tag* findRuleTag(const QString &ruleId, const QString &tagId) const;
void clear();

View File

@ -212,5 +212,6 @@
<file>ui/images/browser/MediaBrowserIconNapster.svg</file>
<file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file>
<file>ui/images/browser/MediaBrowserIconDeezer.svg</file>
<file>ui/images/view-grid-symbolic.svg</file>
</qresource>
</RCC>

View File

@ -197,5 +197,7 @@
<file>ui/magic/NewMagicPage.qml</file>
<file>ui/components/WebViewWrapper.qml</file>
<file>ui/magic/NewScenePage.qml</file>
<file>ui/mainviews/GroupsView.qml</file>
<file>ui/grouping/GroupPage.qml</file>
</qresource>
</RCC>

View File

@ -79,6 +79,10 @@ Page {
}
tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++})
tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++})
if (engine.jsonRpcClient.ensureServerVersion(1.6)) {
tabEntryComponent.createObject(tabBar, {text: qsTr("Groups"), iconSource: "../images/view-grid-symbolic.svg", pageIndex: pi++})
}
root.tabsReady = true
}
@ -292,6 +296,13 @@ Page {
}
}
}
GroupsView {
id: groupsView
property string title: qsTr("My groups" + count);
width: swipeView.width
height: swipeView.height
}
}
ColumnLayout {

View File

@ -86,7 +86,7 @@ Item {
Component.onCompleted: ready = true
anchors.fill: parent
anchors.margins: parent.margins
anchors.margins: parent ? parent.margins : 0
source: ready && width > 0 && height > 0 && icon.name ? icon.name : ""
sourceSize {
width: width
@ -100,7 +100,7 @@ Item {
id: colorizedImage
anchors.fill: parent
anchors.margins: parent.margins
anchors.margins: parent ? parent.margins : 0
visible: active && image.status == Image.Ready
// Whether or not a color has been set.

View File

@ -70,6 +70,13 @@ Page {
iconSource: Qt.binding(function() { return favoritesProxy.count === 0 ? "../images/starred.svg" : "../images/non-starred.svg"}),
functionName: "toggleFavorite"
}))
thingMenu.addItem(menuEntryComponent.createObject(thingMenu,
{
text: qsTr("Grouping"),
iconSource: "../images/view-grid-symbolic.svg",
functionName: "addToGroup"
}))
}
}
function openDeviceMagicPage() {
@ -85,6 +92,12 @@ Page {
engine.tagsManager.untagDevice(root.device.id, "favorites")
}
}
function addToGroup() {
// engine.tagsManager.tagDevice(root.device.id, "group", "My group 1")
var dialog = addToGroupDialog.createObject(root)
dialog.open();
}
function openThingSettingsPage() {
pageStack.push(Qt.resolvedUrl("../thingconfiguration/ConfigureThingPage.qml"), {device: root.device})
}
@ -101,6 +114,65 @@ Page {
onTriggered: thingMenu[functionName]()
}
}
Component {
id: addToGroupDialog
MeaDialog {
title: qsTr("Configure groups")
headerIcon: "../images/view-grid-symbolic.svg"
RowLayout {
TextField {
id: newGroupdTextField
Layout.fillWidth: true
placeholderText: qsTr("New group")
}
Button {
text: qsTr("OK")
enabled: newGroupdTextField.displayText.length > 0 && !groupTags.containsId("group-" + newGroupdTextField.displayText)
onClicked: {
engine.tagsManager.tagDevice(root.device.id, "group-" + newGroupdTextField.text, 1000)
newGroupdTextField.text = ""
}
}
}
ListView {
Layout.fillWidth: true
height: 200
model: TagListModel {
id: groupTags
tagsProxy: TagsProxyModel {
tags: engine.tagsManager.tags
filterTagId: "group-.*"
}
}
delegate: CheckDelegate {
width: parent.width
text: model.tagId.substring(6)
checked: innerProxy.count > 0
onClicked: {
if (innerProxy.count == 0) {
engine.tagsManager.tagDevice(root.device.id, model.tagId, 1000)
} else {
engine.tagsManager.untagDevice(root.device.id, model.tagId, model.value)
}
}
DevicesProxy {
id: innerProxy
engine: _engine
filterTagId: model.tagId
filterDeviceId: root.device.id
}
}
}
}
}
}
Rectangle {

View File

@ -0,0 +1,38 @@
import QtQuick 2.5
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1
import Nymea 1.0
import "../components"
Page {
id: root
header: NymeaHeader {
text: root.groupTag.substring(6)
onBackPressed: pageStack.pop()
}
property string groupTag
DevicesProxy {
id: devicesInGroup
engine: _engine
filterTagId: root.groupTag
}
InterfacesProxy {
id: interfacesInGroup
devicesProxyFilter: devicesInGroup
showStates: true
}
ListView {
anchors.fill: parent
model: devicesInGroup
delegate: Label {
text: model.name
}
}
}

View File

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="view-grid-symbolic.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6199994"
inkscape:cx="-33.300718"
inkscape:cy="62.909239"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
</sodipodi:namedview>
<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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 429.99805,433.36328 -2,0 -34.01367,0 0,-36.00195 36.01367,0 0,36.00195 z m -4,-4.00195 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4201"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 429.99805,389.36133 -2,0 -34.01367,0 0,-36 36.01367,0 0,36 z m -4,-4 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4203"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 385.98242,433.36328 -2,0 -34.01562,0 0,-36.00195 36.01562,0 0,36.00195 z m -4.00195,-4.00195 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4205"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 385.98242,389.36133 -2,0 -34.01562,0 0,-36 36.01562,0 0,36 z m -4.00195,-4 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4207"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,354 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Nymea 1.0
import QtQuick.Controls.Material 2.2
import "../components"
Item {
id: root
readonly property int count: groupsGridView.count
GridView {
id: groupsGridView
anchors.fill: parent
anchors.margins: app.margins / 2
readonly property int minTileWidth: 180
readonly property int minTileHeight: 180
readonly property int tilesPerRow: root.width / minTileWidth
model: TagListModel {
tagsProxy: TagsProxyModel {
tags: engine.tagsManager.tags
filterTagId: "group-.*"
}
}
cellWidth: width / tilesPerRow
cellHeight: cellWidth
delegate: Item {
id: groupDelegate
width: groupsGridView.cellWidth
height: groupsGridView.cellHeight
Pane {
anchors.fill: parent
anchors.margins: app.margins / 2
Material.elevation: 2
padding: 0
DevicesProxy {
id: devicesInGroup
engine: _engine
filterTagId: model.tagId
filterTagValue: model.value
}
InterfacesProxy {
id: controlsInGroup
shownInterfaces: ["light", "simpleclosable"]
devicesProxyFilter: devicesInGroup
showStates: true
showActions: true
}
InterfacesProxy {
id: sensorsInGroup
shownInterfaces: ["temperaturesensor", "lightsensor", "presencesensor"]
devicesProxyFilter: devicesInGroup
showStates: true
}
contentItem: ItemDelegate {
padding: 0
onClicked: {
pageStack.push(Qt.resolvedUrl("../grouping/GroupPage.qml"), {groupTag: model.tagId})
}
contentItem: ColumnLayout {
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 30
color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .05)
Label {
anchors.fill: parent
anchors { leftMargin: app.margins; rightMargin: app.margins }
text: model.tagId.substring(6)
elide: Text.ElideRight
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
Repeater {
model: controlsInGroup
delegate: Loader {
id: controlLoader
Layout.fillWidth: true
Layout.leftMargin: app.margins / 2
Layout.rightMargin: app.margins / 2
sourceComponent: {
switch (model.name) {
case "simpleclosable":
return closableDelegate
case "light":
return lightDelegate
}
}
Binding {
target: controlLoader.item
property: "devices"
value: devicesInGroup
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize * 1.2
Layout.alignment: Qt.AlignRight
color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, 0.05)
RowLayout {
anchors.fill: parent
Repeater {
model: sensorsInGroup
delegate: Row {
ColorIcon {
height: app.iconSize * .8
width: height
name: app.interfaceToIcon(model.name)
color: app.interfaceToColor(model.name)
}
DevicesProxy {
id: innerProxy
engine: _engine
parentProxy: devicesInGroup
shownInterfaces: [model.name]
}
Led {
visible: ["presencesensor"].indexOf(model.name) >= 0
state: {
var stateName = null
switch (model.name) {
case "presencesensor":
stateName = "isPresent"
break;
}
if (!stateName) {
return "off";
}
var ret = false;
for (var i = 0; i < innerProxy.count; i++) {
ret |= innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value
}
return ret ? "on" : "off";
}
}
Label {
text: {
var stateName = null;
switch (model.name) {
case "temperaturesensor":
stateName = "temperature";
break;
case "lightsensor":
stateName = "lightIntensity"
break;
}
if (!stateName) {
return "";
}
var ret = 0
for (var i = 0; i < innerProxy.count; i++) {
ret += innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value
}
return (ret / innerProxy.count).toFixed(1)
}
}
}
}
}
}
}
}
}
}
}
}
}
Component {
id: lightDelegate
RowLayout {
property var devices
DevicesProxy {
id: lights
engine: _engine
parentProxy: devices
shownInterfaces: ["light"]
}
DevicesProxy {
id: dimmableLights
engine: _engine
parentProxy: devices
shownInterfaces: ["dimmablelight"]
}
Label {
text: qsTr("Lighting")
Layout.fillWidth: true
Layout.preferredHeight: slider.height
verticalAlignment: Text.AlignVCenter
visible: dimmableLights.count == 0
}
Slider {
id: slider
from: 0
to: 100
visible: dimmableLights.count > 0
value: {
var median = 0
var count = 0;
for (var i = 0; i < dimmableLights.count; i++) {
var device = dimmableLights.get(i);
var brightnessId = device.deviceClass.stateTypes.findByName("brightness").id
median += device.states.getState(brightnessId).value
count++
}
return median / count;
}
Layout.fillWidth: true
onPressedChanged: {
for (var i = 0; i < dimmableLights.count; i++) {
var device = dimmableLights.get(i);
var brightnessId = device.deviceClass.actionTypes.findByName("brightness").id
engine.deviceManager.executeAction(device.id, brightnessId, [{paramTypeId: brightnessId, value: value}]);
}
}
}
ColorIcon {
Layout.preferredHeight: app.iconSize
Layout.preferredWidth: app.iconSize
name: isOn ? "../images/light-on.svg" : "../images/light-off.svg"
color: isOn ? app.accentColor : keyColor
property bool isOn: {
for (var i = 0; i < lights.count; i++) {
var device = lights.get(i)
var powerId = device.deviceClass.stateTypes.findByName("power").id
if (device.states.getState(powerId).value === true) {
return true
}
}
return false;
}
MouseArea {
anchors.fill: parent
onClicked: {
for (var i = 0; i < lights.count; i++) {
var device = lights.get(i)
var powerId = device.deviceClass.stateTypes.findByName("power").id
engine.deviceManager.executeAction(device.id, powerId, [{paramTypeId: powerId, value: !parent.isOn}])
}
}
}
}
}
}
Component {
id: closableDelegate
RowLayout {
property var devices: null
DevicesProxy {
id: simpleClosables
engine: _engine
parentProxy: devices
shownInterfaces: ["simpleclosable"]
}
DevicesProxy {
id: closables
engine: _engine
parentProxy: devices
shownInterfaces: ["closable"]
}
ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize
ColorIcon {
height: parent.height
width: height
anchors.centerIn: parent
name: Qt.resolvedUrl("../images/up.svg")
}
onClicked: {
for (var i = 0; i < simpleClosables.count; i++) {
var device = simpleClosables.get(i)
var openId = device.deviceClass.actionTypes.findByName("open").id
engine.deviceManager.executeAction(device.id, openId)
}
}
}
ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize
visible: closables.count > 0
ColorIcon {
height: parent.height
width: height
anchors.centerIn: parent
name: Qt.resolvedUrl("../images/media-playback-stop.svg")
}
onClicked: {
for (var i = 0; i < closables.count; i++) {
var device = closables.get(i)
var openId = device.deviceClass.actionTypes.findByName("stop").id
engine.deviceManager.executeAction(device.id, openId)
}
}
}
ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize
ColorIcon {
height: parent.height
width: height
anchors.centerIn: parent
name: Qt.resolvedUrl("../images/down.svg")
}
onClicked: {
for (var i = 0; i < simpleClosables.count; i++) {
var device = simpleClosables.get(i)
var closeId = device.deviceClass.actionTypes.findByName("close").id
engine.deviceManager.executeAction(device.id, closeId)
}
}
}
}
}
}