diff --git a/libmea-core/jsonrpc/jsontypes.cpp b/libmea-core/jsonrpc/jsontypes.cpp index f68dabee..e83f446e 100644 --- a/libmea-core/jsonrpc/jsontypes.cpp +++ b/libmea-core/jsonrpc/jsontypes.cpp @@ -34,6 +34,12 @@ #include "types/stateevaluator.h" #include "types/stateevaluators.h" #include "types/statedescriptor.h" +#include "types/timeeventitem.h" +#include "types/timeeventitems.h" +#include "types/timedescriptor.h" +#include "types/repeatingoption.h" +#include "types/calendaritems.h" +#include "types/calendaritem.h" #include @@ -245,6 +251,10 @@ QVariantMap JsonTypes::packRule(Rule *rule) ret.insert("eventDescriptors", packEventDescriptors(rule->eventDescriptors())); } + if (rule->timeDescriptor()->timeEventItems()->rowCount() > 0 || rule->timeDescriptor()->calendarItems()->rowCount() > 0) { + ret.insert("timeDescriptor", packTimeDescriptor(rule->timeDescriptor())); + } + if (rule->stateEvaluator()) { ret.insert("stateEvaluator", packStateEvaluator(rule->stateEvaluator())); } @@ -338,6 +348,67 @@ QVariantMap JsonTypes::packStateEvaluator(StateEvaluator *stateEvaluator) return ret; } +QVariantMap JsonTypes::packTimeDescriptor(TimeDescriptor *timeDescriptor) +{ + QVariantMap ret; + QVariantList timeEventItems; + for (int i = 0; i < timeDescriptor->timeEventItems()->rowCount(); i++) { + timeEventItems.append(packTimeEventItem(timeDescriptor->timeEventItems()->get(i))); + } + if (!timeEventItems.isEmpty()) { + ret.insert("timeEventItems", timeEventItems); + } + QVariantList calendarItems; + for (int i = 0; i < timeDescriptor->calendarItems()->rowCount(); i++) { + calendarItems.append(packCalendarItem(timeDescriptor->calendarItems()->get(i))); + } + if (!calendarItems.isEmpty()) { + ret.insert("calendarItems", calendarItems); + } + return ret; +} + +QVariantMap JsonTypes::packTimeEventItem(TimeEventItem *timeEventItem) +{ + QVariantMap ret; + if (!timeEventItem->time().isNull()) { + ret.insert("time", timeEventItem->time().toString("hh:mm")); + } + if (!timeEventItem->dateTime().isNull()) { + ret.insert("dateTime", timeEventItem->dateTime().toSecsSinceEpoch()); + } + ret.insert("repeating", packRepeatingOption(timeEventItem->repeatingOption())); + return ret; +} + +QVariantMap JsonTypes::packCalendarItem(CalendarItem *calendarItem) +{ + QVariantMap ret; + ret.insert("duration", calendarItem->duration()); + if (!calendarItem->dateTime().isNull()) { + ret.insert("datetime", calendarItem->dateTime().toSecsSinceEpoch()); + } + if (!calendarItem->startTime().isNull()) { + ret.insert("startTime", calendarItem->startTime().toString("hh:mm")); + } + ret.insert("repeating", packRepeatingOption(calendarItem->repeatingOption())); + return ret; +} + +QVariantMap JsonTypes::packRepeatingOption(RepeatingOption *repeatingOption) +{ + QVariantMap ret; + QMetaEnum repeatingModeEnum = QMetaEnum::fromType(); + ret.insert("mode", repeatingModeEnum.valueToKey(repeatingOption->repeatingMode())); + if (!repeatingOption->weekDays().isEmpty()) { + ret.insert("weekDays", repeatingOption->weekDays()); + } + if (!repeatingOption->monthDays().isEmpty()) { + ret.insert("monthDays", repeatingOption->monthDays()); + } + return ret; +} + DeviceClass::SetupMethod JsonTypes::stringToSetupMethod(const QString &setupMethodString) { if (setupMethodString == "SetupMethodJustAdd") { diff --git a/libmea-core/jsonrpc/jsontypes.h b/libmea-core/jsonrpc/jsontypes.h index 90bbba75..3f94daf2 100644 --- a/libmea-core/jsonrpc/jsontypes.h +++ b/libmea-core/jsonrpc/jsontypes.h @@ -43,6 +43,10 @@ class Rule; class StateEvaluator; class RuleActions; class EventDescriptors; +class TimeDescriptor; +class TimeEventItem; +class CalendarItem; +class RepeatingOption; class JsonTypes : public QObject { @@ -65,6 +69,11 @@ public: static QVariantList packEventDescriptors(EventDescriptors* eventDescriptors); static QVariantMap packParam(Param *param); static QVariantMap packStateEvaluator(StateEvaluator* stateEvaluator); + static QVariantMap packTimeDescriptor(TimeDescriptor* timeDescriptor); + static QVariantMap packTimeEventItem(TimeEventItem* timeEventItem); + static QVariantMap packCalendarItem(CalendarItem* calendarItem); + static QVariantMap packRepeatingOption(RepeatingOption* repeatingOption); + private: static DeviceClass::SetupMethod stringToSetupMethod(const QString &setupMethodString); static QList stringListToBasicTags(const QStringList &basicTagsStringList); diff --git a/libmea-core/libmea-core.h b/libmea-core/libmea-core.h index 96efb4fe..cf834c60 100644 --- a/libmea-core/libmea-core.h +++ b/libmea-core/libmea-core.h @@ -18,6 +18,12 @@ #include "types/ruleactionparam.h" #include "types/eventdescriptors.h" #include "types/eventdescriptor.h" +#include "types/timedescriptor.h" +#include "types/timeeventitems.h" +#include "types/timeeventitem.h" +#include "types/repeatingoption.h" +#include "types/calendaritems.h" +#include "types/calendaritem.h" #include "types/rule.h" #include "types/interfaces.h" #include "types/interface.h" @@ -104,6 +110,12 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "StateDescriptor", "Uncreatable"); qmlRegisterUncreatableType(uri, 1, 0, "StateEvaluator", "Uncreatable"); qmlRegisterUncreatableType(uri, 1, 0, "StateEvaluators", "Uncreatable"); + qmlRegisterUncreatableType(uri, 1, 0, "TimeDescriptor", "Uncreatable"); + qmlRegisterUncreatableType(uri, 1, 0, "TimeEventItems", "Uncreatable"); + qmlRegisterUncreatableType(uri, 1, 0, "TimeEventItem", "Uncreatable"); + qmlRegisterUncreatableType(uri, 1, 0, "RepeatingOption", "Uncreatable"); + qmlRegisterUncreatableType(uri, 1, 0, "CalendarItems", "Uncreatable"); + qmlRegisterUncreatableType(uri, 1, 0, "CalendarItem", "Uncreatable"); qmlRegisterUncreatableType(uri, 1, 0, "Interface", "Uncreatable"); qmlRegisterSingletonType(uri, 1, 0, "Interfaces", interfacesModel_provider); diff --git a/libmea-core/rulemanager.cpp b/libmea-core/rulemanager.cpp index a87add94..78f23c96 100644 --- a/libmea-core/rulemanager.cpp +++ b/libmea-core/rulemanager.cpp @@ -12,7 +12,12 @@ #include "types/stateevaluator.h" #include "types/stateevaluators.h" #include "types/statedescriptor.h" +#include "types/timedescriptor.h" +#include "types/timeeventitems.h" #include "types/timeeventitem.h" +#include "types/repeatingoption.h" +#include "types/calendaritems.h" +#include "types/calendaritem.h" #include @@ -179,6 +184,7 @@ Rule *RuleManager::parseRule(const QVariantMap &ruleMap) parseEventDescriptors(ruleMap.value("eventDescriptors").toList(), rule); parseRuleActions(ruleMap.value("actions").toList(), rule); parseRuleExitActions(ruleMap.value("exitActions").toList(), rule); + parseTimeDescriptor(ruleMap.value("timeDescriptor").toMap(), rule); rule->setStateEvaluator(parseStateEvaluator(ruleMap.value("stateEvaluator").toMap())); return rule; } @@ -259,12 +265,36 @@ void RuleManager::parseRuleExitActions(const QVariantList &ruleActions, Rule *ru void RuleManager::parseTimeDescriptor(const QVariantMap &timeDescriptor, Rule *rule) { - Q_UNUSED(rule) - Q_UNUSED(timeDescriptor) -// foreach (const QVariant &timeEventItemVariant, timeDescriptor.value("timeEventItems").toList()) { -// TimeEventItem *timeEventItem = new TimeEventItem(); -// timeEventItem->setDateTime(QDateTime::fromSecsSinceEpoch(timeEventItemVariant.toMap().value("datetime").toULongLong())); -// timeEventItem->setTime(QTime::fromString(timeEventItemVariant.toMap().value("time").toString())); -// timeEventItem->setRepeatingOption(); -// } + foreach (const QVariant &timeEventItemVariant, timeDescriptor.value("timeEventItems").toList()) { + TimeEventItem *timeEventItem = new TimeEventItem(); + if (timeEventItemVariant.toMap().contains("datetime")) { + timeEventItem->setDateTime(QDateTime::fromSecsSinceEpoch(timeEventItemVariant.toMap().value("datetime").toULongLong())); + } + if (timeEventItemVariant.toMap().contains("time")){ + timeEventItem->setTime(QTime::fromString(timeEventItemVariant.toMap().value("time").toString())); + } + QVariantMap repeatingOptionMap = timeEventItemVariant.toMap().value("repeating").toMap(); + QMetaEnum modeEnum = QMetaEnum::fromType(); + timeEventItem->repeatingOption()->setRepeatingMode((RepeatingOption::RepeatingMode)modeEnum.keyToValue(repeatingOptionMap.value("mode").toByteArray())); + timeEventItem->repeatingOption()->setWeekDays(repeatingOptionMap.value("weekDays").toList()); + timeEventItem->repeatingOption()->setMonthDays(repeatingOptionMap.value("monthDays").toList()); + rule->timeDescriptor()->timeEventItems()->addTimeEventItem(timeEventItem); + } + foreach (const QVariant &calendarItemVariant, timeDescriptor.value("calendarItems").toList()) { + CalendarItem *calendarItem = new CalendarItem(); + if (calendarItemVariant.toMap().contains("datetime")) { + calendarItem->setDateTime(QDateTime::fromSecsSinceEpoch(calendarItemVariant.toMap().value("datetime").toULongLong())); + } + if (calendarItemVariant.toMap().contains("startTime")) { + calendarItem->setStartTime(QTime::fromString(calendarItemVariant.toMap().value("startTime").toString())); + } + calendarItem->setDuration(calendarItemVariant.toMap().value("duration").toInt()); + QVariantMap repeatingOptionMap = calendarItemVariant.toMap().value("repeating").toMap(); + QMetaEnum modeEnum = QMetaEnum::fromType(); + calendarItem->repeatingOption()->setRepeatingMode((RepeatingOption::RepeatingMode)modeEnum.keyToValue(repeatingOptionMap.value("mode").toByteArray())); + calendarItem->repeatingOption()->setWeekDays(repeatingOptionMap.value("weekDays").toList()); + calendarItem->repeatingOption()->setMonthDays(repeatingOptionMap.value("monthDays").toList()); + rule->timeDescriptor()->calendarItems()->addCalendarItem(calendarItem); + } +// rule->timeDescriptor() } diff --git a/libnymea-common/libnymea-common.pro b/libnymea-common/libnymea-common.pro index a42c1163..3adc7fff 100644 --- a/libnymea-common/libnymea-common.pro +++ b/libnymea-common/libnymea-common.pro @@ -51,7 +51,8 @@ HEADERS += \ types/timeeventitem.h \ types/calendaritem.h \ types/timeeventitems.h \ - types/calendaritems.h + types/calendaritems.h \ + types/repeatingoption.h SOURCES += \ types/vendor.cpp \ @@ -93,7 +94,8 @@ SOURCES += \ types/timeeventitem.cpp \ types/calendaritem.cpp \ types/timeeventitems.cpp \ - types/calendaritems.cpp + types/calendaritems.cpp \ + types/repeatingoption.cpp # install header file with relative subdirectory for(header, HEADERS) { diff --git a/libnymea-common/types/calendaritem.cpp b/libnymea-common/types/calendaritem.cpp index 18e4d9d9..e2c87956 100644 --- a/libnymea-common/types/calendaritem.cpp +++ b/libnymea-common/types/calendaritem.cpp @@ -1,6 +1,62 @@ #include "calendaritem.h" +#include "repeatingoption.h" + CalendarItem::CalendarItem(QObject *parent) : QObject(parent) { - + m_repeatingOption = new RepeatingOption(this); +} + +int CalendarItem::duration() const +{ + return m_duration; +} + +void CalendarItem::setDuration(int duration) +{ + if (m_duration != duration) { + m_duration = duration; + emit durationChanged(); + } +} + +QDateTime CalendarItem::dateTime() const +{ + return m_dateTime; +} + +void CalendarItem::setDateTime(const QDateTime &dateTime) +{ + if (m_dateTime != dateTime) { + m_dateTime = dateTime; + emit dateTimeChanged(); + } +} + +QTime CalendarItem::startTime() const +{ + return m_startTime; +} + +void CalendarItem::setStartTime(const QTime &startTime) +{ + if (m_startTime != startTime) { + m_startTime = startTime; + emit startTimeChanged(); + } +} + +RepeatingOption *CalendarItem::repeatingOption() const +{ + return m_repeatingOption; +} + +CalendarItem *CalendarItem::clone() const +{ + CalendarItem* ret = new CalendarItem(); + ret->m_dateTime = this->m_dateTime; + ret->m_duration = this->m_duration; + ret->m_repeatingOption = this->m_repeatingOption; + ret->m_startTime = this->m_startTime; + return ret; } diff --git a/libnymea-common/types/calendaritem.h b/libnymea-common/types/calendaritem.h index 850679ce..46f8d8e9 100644 --- a/libnymea-common/types/calendaritem.h +++ b/libnymea-common/types/calendaritem.h @@ -2,16 +2,44 @@ #define CALENDARITEM_H #include +#include + +class RepeatingOption; class CalendarItem : public QObject { Q_OBJECT + Q_PROPERTY(int duration READ duration WRITE setDuration NOTIFY durationChanged) + Q_PROPERTY(QDateTime dateTime READ dateTime WRITE setDateTime NOTIFY dateTimeChanged) + Q_PROPERTY(QTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged) + Q_PROPERTY(RepeatingOption* repeatingOption READ repeatingOption CONSTANT) + public: explicit CalendarItem(QObject *parent = nullptr); -signals: + int duration() const; + void setDuration(int duration); -public slots: + QDateTime dateTime() const; + void setDateTime(const QDateTime &dateTime); + + QTime startTime() const; + void setStartTime(const QTime &startTime); + + RepeatingOption* repeatingOption() const; + + CalendarItem* clone() const; + +signals: + void durationChanged(); + void dateTimeChanged(); + void startTimeChanged(); + +private: + int m_duration = 0; + QDateTime m_dateTime; + QTime m_startTime; + RepeatingOption* m_repeatingOption = nullptr; }; -#endif // CALENDARITEM_H \ No newline at end of file +#endif // CALENDARITEM_H diff --git a/libnymea-common/types/calendaritems.cpp b/libnymea-common/types/calendaritems.cpp index edd79427..cd5bafaa 100644 --- a/libnymea-common/types/calendaritems.cpp +++ b/libnymea-common/types/calendaritems.cpp @@ -1,6 +1,50 @@ #include "calendaritems.h" +#include "calendaritem.h" -CalendarItems::CalendarItems(QObject *parent) : QObject(parent) +CalendarItems::CalendarItems(QObject *parent) : QAbstractListModel(parent) { } + +int CalendarItems::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant CalendarItems::data(const QModelIndex &index, int role) const +{ + return QVariant(); +} + +void CalendarItems::addCalendarItem(CalendarItem *calendarItem) +{ + calendarItem->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(calendarItem); + endInsertRows(); + emit countChanged(); +} + +void CalendarItems::removeCalendarItem(int index) +{ + if (index < 0 || index > m_list.count()) { + return; + } + beginRemoveRows(QModelIndex(), index, index); + m_list.takeAt(index)->deleteLater(); + endRemoveRows(); +} + +CalendarItem *CalendarItems::createNewCalendarItem() const +{ + return new CalendarItem(); +} + +CalendarItem *CalendarItems::get(int index) const +{ + if (index < 0 || index > m_list.count()) { + return nullptr; + } + return m_list.at(index); +} diff --git a/libnymea-common/types/calendaritems.h b/libnymea-common/types/calendaritems.h index 9b441f83..9b4e279e 100644 --- a/libnymea-common/types/calendaritems.h +++ b/libnymea-common/types/calendaritems.h @@ -1,17 +1,31 @@ #ifndef CALENDARITEMS_H #define CALENDARITEMS_H -#include +#include -class CalendarItems : public QObject +class CalendarItem; + +class CalendarItems : public QAbstractListModel { Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: explicit CalendarItems(QObject *parent = nullptr); -signals: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; -public slots: + Q_INVOKABLE void addCalendarItem(CalendarItem* calendarItem); + Q_INVOKABLE void removeCalendarItem(int index); + + Q_INVOKABLE CalendarItem* createNewCalendarItem() const; + Q_INVOKABLE CalendarItem* get(int index) const; + +signals: + void countChanged(); + +private: + QList m_list; }; -#endif // CALENDARITEMS_H \ No newline at end of file +#endif // CALENDARITEMS_H diff --git a/libnymea-common/types/repeatingoption.cpp b/libnymea-common/types/repeatingoption.cpp new file mode 100644 index 00000000..c2bdc890 --- /dev/null +++ b/libnymea-common/types/repeatingoption.cpp @@ -0,0 +1,45 @@ +#include "repeatingoption.h" + +RepeatingOption::RepeatingOption(QObject *parent) : QObject(parent) +{ + +} + +RepeatingOption::RepeatingMode RepeatingOption::repeatingMode() const +{ + return m_repeatingMode; +} + +void RepeatingOption::setRepeatingMode(RepeatingOption::RepeatingMode repeatingMode) +{ + if (m_repeatingMode != repeatingMode) { + m_repeatingMode = repeatingMode; + emit repeatingModeChanged(); + } +} + +QVariantList RepeatingOption::weekDays() const +{ + return m_weekDays; +} + +void RepeatingOption::setWeekDays(const QVariantList &weekDays) +{ + if (m_weekDays != weekDays) { + m_weekDays = weekDays; + emit weekDaysChanged(); + } +} + +QVariantList RepeatingOption::monthDays() const +{ + return m_monthDays; +} + +void RepeatingOption::setMonthDays(const QVariantList &monthDays) +{ + if (m_monthDays != monthDays) { + m_monthDays = monthDays; + emit monthDaysChanged(); + } +} diff --git a/libnymea-common/types/repeatingoption.h b/libnymea-common/types/repeatingoption.h new file mode 100644 index 00000000..42aebbcf --- /dev/null +++ b/libnymea-common/types/repeatingoption.h @@ -0,0 +1,48 @@ +#ifndef REPEATINGOPTION_H +#define REPEATINGOPTION_H + +#include +#include + +class RepeatingOption: public QObject +{ + Q_OBJECT + Q_PROPERTY(RepeatingMode repeatingMode READ repeatingMode WRITE setRepeatingMode NOTIFY repeatingModeChanged) + Q_PROPERTY(QVariantList weekDays READ weekDays WRITE setWeekDays NOTIFY weekDaysChanged) + Q_PROPERTY(QVariantList monthDays READ monthDays WRITE setMonthDays NOTIFY monthDaysChanged) + +public: + enum RepeatingMode { + RepeatingModeNone, + RepeatingModeHourly, + RepeatingModeDaily, + RepeatingModeWeekly, + RepeatingModeMonthly, + RepeatingModeYearly + }; + Q_ENUM(RepeatingMode) + + explicit RepeatingOption(QObject *parent = nullptr); + + RepeatingMode repeatingMode() const; + void setRepeatingMode(RepeatingMode repeatingMode); + + QVariantList weekDays() const; + void setWeekDays(const QVariantList &weekDays); + + QVariantList monthDays() const; + void setMonthDays(const QVariantList &monthDays); + +signals: + void repeatingModeChanged(); + void weekDaysChanged(); + void monthDaysChanged(); + +private: + RepeatingMode m_repeatingMode = RepeatingModeDaily; + QVariantList m_weekDays; + QVariantList m_monthDays; +}; + + +#endif // REPEATINGOPTION_H diff --git a/libnymea-common/types/rule.cpp b/libnymea-common/types/rule.cpp index 1ab1b2ff..ae44e3c3 100644 --- a/libnymea-common/types/rule.cpp +++ b/libnymea-common/types/rule.cpp @@ -4,9 +4,16 @@ #include "eventdescriptors.h" #include "stateevaluator.h" #include "stateevaluators.h" +#include "statedescriptor.h" #include "ruleaction.h" #include "ruleactions.h" #include "timedescriptor.h" +#include "timeeventitems.h" +#include "timeeventitem.h" +#include "calendaritems.h" +#include "calendaritem.h" + +#include Rule::Rule(const QUuid &id, QObject *parent) : QObject(parent), @@ -14,9 +21,15 @@ Rule::Rule(const QUuid &id, QObject *parent) : m_eventDescriptors(new EventDescriptors(this)), // m_stateEvaluator(new StateEvaluator(this)), m_actions(new RuleActions(this)), - m_exitActions(new RuleActions(this)) + m_exitActions(new RuleActions(this)), + m_timeDescriptor(new TimeDescriptor(this)) { + qDebug() << "### Creating rule" << this; +} +Rule::~Rule() +{ + qDebug() << "### Destroying rule" << this; } QUuid Rule::id() const @@ -113,11 +126,15 @@ Rule *Rule::clone() const for (int i = 0; i < this->eventDescriptors()->rowCount(); i++) { ret->eventDescriptors()->addEventDescriptor(this->eventDescriptors()->get(i)->clone()); } -// ret->stateEvaluator()->setStateDescriptor(this->stateEvaluator()->stateDescriptor()->clone()); -// ret->stateEvaluator()->setStateOperator(this->stateEvaluator()->stateOperator()); -// for (int i = 0; i < this->stateEvaluator()->childEvaluators()->rowCount(); i++) { -// ret->stateEvaluator()->childEvaluators()-> -// } + for (int i = 0; i < this->timeDescriptor()->timeEventItems()->rowCount(); i++) { + ret->timeDescriptor()->timeEventItems()->addTimeEventItem(this->timeDescriptor()->timeEventItems()->get(i)->clone()); + } + for (int i = 0; i < this->timeDescriptor()->calendarItems()->rowCount(); i++) { + ret->timeDescriptor()->calendarItems()->addCalendarItem(this->timeDescriptor()->calendarItems()->get(i)->clone()); + } + if (this->stateEvaluator()) { + ret->setStateEvaluator(this->stateEvaluator()->clone()); + } for (int i = 0; i < this->actions()->rowCount(); i++) { ret->actions()->addRuleAction(this->actions()->get(i)->clone()); } diff --git a/libnymea-common/types/rule.h b/libnymea-common/types/rule.h index 0aa40db8..4c1f4c27 100644 --- a/libnymea-common/types/rule.h +++ b/libnymea-common/types/rule.h @@ -23,6 +23,7 @@ class Rule : public QObject Q_PROPERTY(TimeDescriptor* timeDescriptor READ timeDescriptor CONSTANT) public: explicit Rule(const QUuid &id = QUuid(), QObject *parent = nullptr); + ~Rule(); QUuid id() const; @@ -45,7 +46,7 @@ public: Q_INVOKABLE void createStateEvaluator(); - Rule *clone() const; + Q_INVOKABLE Rule *clone() const; signals: void nameChanged(); diff --git a/libnymea-common/types/stateevaluator.cpp b/libnymea-common/types/stateevaluator.cpp index 716d0294..2ba1444b 100644 --- a/libnymea-common/types/stateevaluator.cpp +++ b/libnymea-common/types/stateevaluator.cpp @@ -59,3 +59,17 @@ StateEvaluator* StateEvaluator::addChildEvaluator() m_childEvaluators->addStateEvaluator(stateEvaluator); return stateEvaluator; } + +StateEvaluator *StateEvaluator::clone() const +{ + StateEvaluator *ret = new StateEvaluator(); + ret->m_operator = this->m_operator; + ret->m_stateDescriptor->setDeviceId(this->m_stateDescriptor->deviceId()); + ret->m_stateDescriptor->setStateTypeId(this->m_stateDescriptor->stateTypeId()); + ret->m_stateDescriptor->setValueOperator(this->m_stateDescriptor->valueOperator()); + ret->m_stateDescriptor->setValue(this->m_stateDescriptor->value()); + for (int i = 0; i < this->m_childEvaluators->rowCount(); i++) { + ret->m_childEvaluators->addStateEvaluator(this->m_childEvaluators->get(i)->clone()); + } + return ret; +} diff --git a/libnymea-common/types/stateevaluator.h b/libnymea-common/types/stateevaluator.h index 437f6d08..0d830580 100644 --- a/libnymea-common/types/stateevaluator.h +++ b/libnymea-common/types/stateevaluator.h @@ -33,6 +33,8 @@ public: Q_INVOKABLE StateEvaluator* addChildEvaluator(); + StateEvaluator* clone() const; + signals: void stateOperatorChanged(); diff --git a/libnymea-common/types/timedescriptor.cpp b/libnymea-common/types/timedescriptor.cpp index 54c0f8f8..ac342634 100644 --- a/libnymea-common/types/timedescriptor.cpp +++ b/libnymea-common/types/timedescriptor.cpp @@ -1,6 +1,20 @@ #include "timedescriptor.h" +#include "timeeventitems.h" +#include "calendaritems.h" + TimeDescriptor::TimeDescriptor(QObject *parent) : QObject(parent) { - + m_timeEventItems = new TimeEventItems(this); + m_calendarItems = new CalendarItems(this); +} + +TimeEventItems *TimeDescriptor::timeEventItems() const +{ + return m_timeEventItems; +} + +CalendarItems *TimeDescriptor::calendarItems() const +{ + return m_calendarItems; } diff --git a/libnymea-common/types/timedescriptor.h b/libnymea-common/types/timedescriptor.h index 821fdb64..28366664 100644 --- a/libnymea-common/types/timedescriptor.h +++ b/libnymea-common/types/timedescriptor.h @@ -3,15 +3,28 @@ #include +#include + +class TimeEventItems; +class CalendarItems; + class TimeDescriptor : public QObject { Q_OBJECT + Q_PROPERTY(TimeEventItems* timeEventItems READ timeEventItems CONSTANT) + Q_PROPERTY(CalendarItems* calendarItems READ calendarItems CONSTANT) public: explicit TimeDescriptor(QObject *parent = nullptr); + TimeEventItems* timeEventItems() const; + CalendarItems* calendarItems() const; signals: public slots: + +private: + TimeEventItems* m_timeEventItems = nullptr; + CalendarItems* m_calendarItems = nullptr; }; -#endif // TIMEDESCRIPTOR_H \ No newline at end of file +#endif // TIMEDESCRIPTOR_H diff --git a/libnymea-common/types/timeeventitem.cpp b/libnymea-common/types/timeeventitem.cpp index cee22edf..67cacaf5 100644 --- a/libnymea-common/types/timeeventitem.cpp +++ b/libnymea-common/types/timeeventitem.cpp @@ -1,8 +1,11 @@ #include "timeeventitem.h" -TimeEventItem::TimeEventItem(QObject *parent) : QObject(parent) -{ +#include "repeatingoption.h" +TimeEventItem::TimeEventItem(QObject *parent): + QObject(parent), + m_repeatingOption(new RepeatingOption(this)) +{ } QDateTime TimeEventItem::dateTime() const @@ -30,3 +33,17 @@ void TimeEventItem::setTime(const QTime &time) emit timeChanged(); } } + +RepeatingOption *TimeEventItem::repeatingOption() const +{ + return m_repeatingOption; +} + +TimeEventItem *TimeEventItem::clone() const +{ + TimeEventItem* ret = new TimeEventItem(); + ret->m_dateTime = this->m_dateTime; + ret->m_time = this->m_time; + ret->m_repeatingOption = this->m_repeatingOption; + return ret; +} diff --git a/libnymea-common/types/timeeventitem.h b/libnymea-common/types/timeeventitem.h index ad48e090..9e481993 100644 --- a/libnymea-common/types/timeeventitem.h +++ b/libnymea-common/types/timeeventitem.h @@ -5,11 +5,14 @@ #include #include +class RepeatingOption; + class TimeEventItem : public QObject { Q_OBJECT Q_PROPERTY(QDateTime dateTime READ dateTime WRITE setDateTime NOTIFY dateTimeChanged) Q_PROPERTY(QTime time READ time WRITE setTime NOTIFY timeChanged) + Q_PROPERTY(RepeatingOption* repeatingOption READ repeatingOption CONSTANT) public: explicit TimeEventItem(QObject *parent = nullptr); @@ -20,6 +23,10 @@ public: QTime time() const; void setTime(const QTime &time); + RepeatingOption* repeatingOption() const; + + TimeEventItem* clone() const; + signals: void dateTimeChanged(); void timeChanged(); @@ -27,6 +34,7 @@ signals: private: QDateTime m_dateTime; QTime m_time; + RepeatingOption *m_repeatingOption = nullptr; }; diff --git a/libnymea-common/types/timeeventitems.cpp b/libnymea-common/types/timeeventitems.cpp index 0449efe1..3c90ac87 100644 --- a/libnymea-common/types/timeeventitems.cpp +++ b/libnymea-common/types/timeeventitems.cpp @@ -1,6 +1,53 @@ #include "timeeventitems.h" -TimeEventItems::TimeEventItems(QObject *parent) : QObject(parent) +#include "timeeventitem.h" + +TimeEventItems::TimeEventItems(QObject *parent): + QAbstractListModel(parent) { } + +int TimeEventItems::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant TimeEventItems::data(const QModelIndex &index, int role) const +{ + return QVariant(); +} + +void TimeEventItems::addTimeEventItem(TimeEventItem *timeEventItem) +{ + timeEventItem->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(timeEventItem); + endInsertRows(); + emit countChanged(); +} + +void TimeEventItems::removeTimeEventItem(int index) +{ + if (index < 0 || index > m_list.count()) { + return; + } + beginRemoveRows(QModelIndex(), index, index); + m_list.takeAt(index)->deleteLater(); + endRemoveRows(); + emit countChanged(); +} + +TimeEventItem *TimeEventItems::get(int index) const +{ + if (index < 0 || index > m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +TimeEventItem *TimeEventItems::createNewTimeEventItem() const +{ + return new TimeEventItem(); +} diff --git a/libnymea-common/types/timeeventitems.h b/libnymea-common/types/timeeventitems.h index c99fc250..96c30d2c 100644 --- a/libnymea-common/types/timeeventitems.h +++ b/libnymea-common/types/timeeventitems.h @@ -1,17 +1,32 @@ #ifndef TIMEEVENTITEMS_H #define TIMEEVENTITEMS_H -#include +#include -class TimeEventItems : public QObject +class TimeEventItem; + +class TimeEventItems: public QAbstractListModel { Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: - explicit TimeEventItems(QObject *parent = nullptr); + TimeEventItems(QObject *parent); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + Q_INVOKABLE void addTimeEventItem(TimeEventItem *timeEventItem); + Q_INVOKABLE void removeTimeEventItem(int index); + + Q_INVOKABLE TimeEventItem* get(int index) const; + Q_INVOKABLE TimeEventItem* createNewTimeEventItem() const; + signals: + void countChanged(); -public slots: +private: + QList m_list; }; -#endif // TIMEEVENTITEMS_H \ No newline at end of file +#endif // TIMEEVENTITEMS_H diff --git a/mea/mea.pro b/mea/mea.pro index 03ce7ec2..b8f753fe 100644 --- a/mea/mea.pro +++ b/mea/mea.pro @@ -13,7 +13,7 @@ win32:Debug:LIBS += -L$$top_builddir/libmea-core/debug \ win32:Release:LIBS += -L$$top_builddir/libmea-core/release \ -L$$top_builddir/libnymea-common/release linux:!android:LIBS += -lavahi-client -lavahi-common -PRE_TARGETDEPS += ../libmea-core +PRE_TARGETDEPS += ../libmea-core ../libnymea-common HEADERS += \ stylecontroller.h diff --git a/mea/resources.qrc b/mea/resources.qrc index def3d19e..4a132cf9 100644 --- a/mea/resources.qrc +++ b/mea/resources.qrc @@ -187,5 +187,16 @@ ../LICENSE ui/images/Built_with_Qt_RGB_logo.svg ui/images/Built_with_Qt_RGB_logo_vertical.svg + ui/magic/TimeEventDelegate.qml + ui/magic/EditTimeEventItemPage.qml + ui/magic/EventDescriptorDelegate.qml + ui/components/MeaDialog.qml + ui/magic/RuleActionDelegate.qml + ui/magic/EditCalendarItemPage.qml + ui/magic/CalendarItemDelegate.qml + ui/images/alarm-clock.svg + ui/images/action.svg + ui/images/event.svg + ui/images/state.svg diff --git a/mea/ui/LoginPage.qml b/mea/ui/LoginPage.qml index 28d0d1a6..82b8cbe2 100644 --- a/mea/ui/LoginPage.qml +++ b/mea/ui/LoginPage.qml @@ -23,20 +23,23 @@ Page { popup.open(); } onCreateUserFailed: { - print("create user failed") - var text + print("createUser failed") + var message; switch (error) { case "UserErrorInvalidUserId": - text = qsTr("The email you've entered isn't valid."); + message = qsTr("The email you've entered isn't valid.") + break; + case "UserErrorDuplicateUserId": + message = qsTr("The email you've entered is already userd.") break; case "UserErrorBadPassword": - text = qsTr("The password you've chose is too weak."); + message = qsTr("The password you've chose is too weak.") + break; + case "UserErrorBackendError": + message = qsTr("An error happened with the user storage. Please make sure your %1 box is installed correctly.") break; - default: - text = qsTr("An error happened creating the user."); } -// var popup = errorDialog.createObject(root, {title: qsTr("Error creating user"), text: text}) - var popup = errorDialog.createObject(root, {title: "Error creating user", text: text}) + var popup = errorDialog.createObject(root, {text: message}); popup.open(); } } diff --git a/mea/ui/MagicPage.qml b/mea/ui/MagicPage.qml index daacbe57..2b4ae3cb 100644 --- a/mea/ui/MagicPage.qml +++ b/mea/ui/MagicPage.qml @@ -13,30 +13,45 @@ Page { HeaderButton { imageSource: Qt.resolvedUrl("images/add.svg") onClicked: { - var newRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: Engine.ruleManager.createNewRule() }); - newRulePage.onAccept.connect(function() { - Engine.ruleManager.addRule(newRulePage.rule); + d.editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: Engine.ruleManager.createNewRule() }); + d.editRulePage.StackView.onRemoved.connect(function() { + d.editRulePage.rule.destroy() + d.editRulePage = null; + }) + d.editRulePage.onAccept.connect(function() { + d.editRulePage.busy = true; + Engine.ruleManager.addRule(d.editRulePage.rule); + }) + d.editRulePage.onCancel.connect(function() { + pageStack.pop(); }) } } } + QtObject { + id: d + property var editRulePage: null + } + Connections { target: Engine.ruleManager onAddRuleReply: { + d.editRulePage.busy = false; if (ruleError == "RuleErrorNoError") { pageStack.pop(); } else { - var popup = errorDialog.createObject(root, {text: ruleError }) + var popup = errorDialog.createObject(app, {errorCode: ruleError }) popup.open(); } } onEditRuleReply: { + d.editRulePage.busy = false; if (ruleError == "RuleErrorNoError") { pageStack.pop(); } else { - var popup = errorDialog.createObject(root, {text: ruleError }) + var popup = errorDialog.createObject(app, {errorCode: ruleError }) popup.open(); } } @@ -66,10 +81,18 @@ Page { } onClicked: { - var editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: Engine.ruleManager.rules.get(index) }) - editRulePage.onAccept.connect(function() { + d.editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: Engine.ruleManager.rules.get(index).clone()}) + d.editRulePage.StackView.onRemoved.connect(function() { + d.editRulePage.rule.destroy(); + d.editRulePage = null + }) + d.editRulePage.onAccept.connect(function() { + d.editRulePage.busy = true; Engine.ruleManager.editRule(editRulePage.rule); }) + d.editRulePage.onCancel.connect(function() { + pageStack.pop(); + }) } swipe.right: MouseArea { diff --git a/mea/ui/Mea.qml b/mea/ui/Mea.qml index 6c59e052..17a7db5f 100644 --- a/mea/ui/Mea.qml +++ b/mea/ui/Mea.qml @@ -22,6 +22,8 @@ ApplicationWindow { property int iconSize: 30 property int delegateHeight: 60 + property bool landscape: app.width > app.height + property var settings: Settings { property string lastConnectedHost: "" property int viewMode: ApplicationWindow.Maximized diff --git a/mea/ui/WirelessControlerPage.qml b/mea/ui/WirelessControlerPage.qml index c74c7d39..d038c4b8 100644 --- a/mea/ui/WirelessControlerPage.qml +++ b/mea/ui/WirelessControlerPage.qml @@ -38,8 +38,9 @@ Page { target: networkManger.manager onErrorOccured: { print("Error occured", errorMessage) - errorDialog.errorText = errorMessage - errorDialog.open() + var errorDialog = Qt.createComponent(Qt.resolvedUrl("components/ErrorDialog.qml")); + var popup = errorDialog.createObject(app, {text: errorMessage}) + popup.open() } onWirelessStatusChanged: { @@ -287,28 +288,6 @@ Page { } - Dialog { - id: errorDialog - width: Math.min(parent.width * .9, 400) - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - standardButtons: Dialog.Ok - - property string errorText - - ColumnLayout { - anchors { left: parent.left; right: parent.right; top: parent.top } - spacing: app.margins - - Label { - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: errorDialog.errorText - } - } - - } - Component { id: settingsPage diff --git a/mea/ui/components/ErrorDialog.qml b/mea/ui/components/ErrorDialog.qml index 64fb3cf6..34a2b7fb 100644 --- a/mea/ui/components/ErrorDialog.qml +++ b/mea/ui/components/ErrorDialog.qml @@ -2,51 +2,15 @@ import QtQuick 2.8 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 -Dialog { +MeaDialog { id: root - width: Math.min(parent.width * .8, contentLabel.implicitWidth) - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - modal: true - title: qsTr("Error") - property alias text: contentLabel.text + title: qsTr("Oh snap!") + headerIcon: "../images/dialog-error-symbolic.svg" - standardButtons: Dialog.Ok + property string errorCode: "" - header: Item { - implicitHeight: headerRow.height + app.margins * 2 - implicitWidth: parent.width - RowLayout { - id: headerRow - anchors { left: parent.left; right: parent.right; top: parent.top; margins: app.margins } - spacing: app.margins - ColorIcon { - Layout.preferredHeight: app.iconSize * 2 - Layout.preferredWidth: height - name: "../images/dialog-error-symbolic.svg" - color: app.guhAccent - } + text: qsTr("An unexpected error happened. We're sorry for that.") + + (errorCode.length > 0 ? "\n\n" + qsTr("Error code: %1").arg(errorCode) : "") - Label { - id: titleLabel - Layout.fillWidth: true - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - text: root.title - color: app.guhAccent - font.pixelSize: app.largeFont - } - } - } - - ColumnLayout { - id: content - anchors { left: parent.left; top: parent.top; right: parent.right } - - Label { - id: contentLabel - Layout.fillWidth: true - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - } - } } diff --git a/mea/ui/components/MeaDialog.qml b/mea/ui/components/MeaDialog.qml new file mode 100644 index 00000000..6e12bee4 --- /dev/null +++ b/mea/ui/components/MeaDialog.qml @@ -0,0 +1,54 @@ +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 + +Dialog { + id: root + width: Math.min(parent.width * .8, Math.max(contentLabel.implicitWidth, 400)) + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + property alias headerIcon: headerColorIcon.name + property alias text: contentLabel.text + default property alias children: content.children + + standardButtons: Dialog.Ok + + header: Item { + implicitHeight: headerRow.height + app.margins * 2 + implicitWidth: parent.width + RowLayout { + id: headerRow + anchors { left: parent.left; right: parent.right; top: parent.top; margins: app.margins } + spacing: app.margins + ColorIcon { + id: headerColorIcon + Layout.preferredHeight: app.iconSize * 2 + Layout.preferredWidth: height + color: app.guhAccent + visible: name.length > 0 + } + + Label { + id: titleLabel + Layout.fillWidth: true + Layout.margins: app.margins + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: root.title + color: app.guhAccent + font.pixelSize: app.largeFont + } + } + } + ColumnLayout { + id: content + anchors { left: parent.left; top: parent.top; right: parent.right } + + Label { + id: contentLabel + Layout.fillWidth: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + visible: text.length > 0 + } + } +} diff --git a/mea/ui/customviews/GenericTypeLogView.qml b/mea/ui/customviews/GenericTypeLogView.qml index 6af096da..47dd1d7d 100644 --- a/mea/ui/customviews/GenericTypeLogView.qml +++ b/mea/ui/customviews/GenericTypeLogView.qml @@ -39,60 +39,72 @@ Item { model: logsModel clip: true onCountChanged: positionViewAtEnd() - delegate: ItemDelegate { + delegate: SwipeDelegate { + id: logEntryDelegate width: parent.width + implicitHeight: app.delegateHeight contentItem: RowLayout { + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + name: "../images/event.svg" + color: app.guhAccent + } + ColumnLayout { - Layout.fillWidth: true - RowLayout { + Label { + id: timeStampLabel Layout.fillWidth: true - - ColorIcon { - Layout.preferredHeight: timeStampLabel.height - Layout.preferredWidth: height - name: "../images/clock-app-symbolic.svg" - } - - Label { - id: timeStampLabel - Layout.fillWidth: true - text: Qt.formatDateTime(model.timestamp,"dd.MM.yy - hh:mm:ss") - } + text: Qt.formatDateTime(model.timestamp,"dd.MM.yy - hh:mm:ss") } - RowLayout { + Label { Layout.fillWidth: true - Label { - text: qsTr("Data:") - } - Label { - Layout.fillWidth: true - text: model.value.trim() - elide: Text.ElideRight - } + text: qsTr("Data: %1").arg(model.value.trim()) + elide: Text.ElideRight + font.pixelSize: app.smallFont } } - HeaderButton { - imageSource: "../images/magic.svg" - color: { - for (var i = 0; i < rulesFilterModel.count; i++) { - var rule = rulesFilterModel.get(i); - for (var j = 0; j < rule.eventDescriptors.count; j++) { - var eventDescriptor = rule.eventDescriptors.get(j); - if (eventDescriptor.eventTypeId === root.logsModel.typeId) { - var matching = true; - for (var k = 0; k < eventDescriptor.paramDescriptors.count; k++) { - var paramDescriptor = eventDescriptor.paramDescriptors.get(k); - if (paramDescriptor.value === model.value) { - return app.guhAccent; - } - } - } - } - } - return keyColor; - } +// ColorIcon { +// Layout.preferredWidth: app.iconSize +// Layout.preferredHeight: width +// name: "../images/magic.svg" +// color: { +// for (var i = 0; i < rulesFilterModel.count; i++) { +// var rule = rulesFilterModel.get(i); +// for (var j = 0; j < rule.eventDescriptors.count; j++) { +// var eventDescriptor = rule.eventDescriptors.get(j); +// if (eventDescriptor.eventTypeId === root.logsModel.typeId) { +// var matching = true; +// for (var k = 0; k < eventDescriptor.paramDescriptors.count; k++) { +// var paramDescriptor = eventDescriptor.paramDescriptors.get(k); +// if (paramDescriptor.value === model.value) { +// return app.guhAccent; +// } +// } +// } +// } +// } +// return keyColor; +// } - onClicked: root.addRuleClicked(model.value) +// } + } + swipe.right: MouseArea { + height: logEntryDelegate.height + width: height + anchors.right: parent.right + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/magic.svg" + } + onClicked: root.addRuleClicked(model.value) + } + onClicked: { + if (swipe.complete) { + swipe.close() + } else { + swipe.open(SwipeDelegate.Right) } } } diff --git a/mea/ui/devicepages/ConfigureThingPage.qml b/mea/ui/devicepages/ConfigureThingPage.qml index c5dff614..7e981fda 100644 --- a/mea/ui/devicepages/ConfigureThingPage.qml +++ b/mea/ui/devicepages/ConfigureThingPage.qml @@ -51,7 +51,7 @@ Page { popup.open(); return; default: - var popup = errorDialog.createObject(root, {text: qsTr("Remove device error: %1").arg(JSON.stringify(params.deviceError)) }) + var popup = errorDialog.createObject(root, {errorCode: params.deviceError}) popup.open(); } } diff --git a/mea/ui/images/action.svg b/mea/ui/images/action.svg new file mode 100644 index 00000000..025a1207 --- /dev/null +++ b/mea/ui/images/action.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/mea/ui/images/alarm-clock.svg b/mea/ui/images/alarm-clock.svg new file mode 100644 index 00000000..0d8ea470 --- /dev/null +++ b/mea/ui/images/alarm-clock.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/mea/ui/images/event.svg b/mea/ui/images/event.svg new file mode 100644 index 00000000..032d268a --- /dev/null +++ b/mea/ui/images/event.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/mea/ui/images/state.svg b/mea/ui/images/state.svg new file mode 100644 index 00000000..16fa3ae6 --- /dev/null +++ b/mea/ui/images/state.svg @@ -0,0 +1,19 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/mea/ui/magic/CalendarItemDelegate.qml b/mea/ui/magic/CalendarItemDelegate.qml new file mode 100644 index 00000000..1a5e8612 --- /dev/null +++ b/mea/ui/magic/CalendarItemDelegate.qml @@ -0,0 +1,111 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import Mea 1.0 +import "../components" + +SwipeDelegate { + id: root + implicitHeight: app.delegateHeight + + property var calendarItem: null + + readonly property bool isDateBased: calendarItem.repeatingOption.repeatingMode === RepeatingOption.RepeatingModeNone || + calendarItem.repeatingOption.repeatingMode === RepeatingOption.RepeatingModeYearly + + signal removeCalendarItem(); + + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "../images/clock-app-symbolic.svg" + color: app.guhAccent + } + + ColumnLayout { + Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: qsTr("From %1 to %2") + .arg(root.isDateBased ? Qt.formatDateTime(root.calendarItem.dateTime) : Qt.formatTime(root.calendarItem.startTime)) + .arg(root.isDateBased ? Qt.formatDateTime(new Date(root.calendarItem.dateTime.getTime() + root.calendarItem.duration * 60000)) : Qt.formatTime(new Date(root.calendarItem.startTime.getTime() + root.calendarItem.duration * 60000))) + } + + Label { + Layout.fillWidth: true + elide: Text.ElideRight + font.pixelSize: app.smallFont + text: qsTr("repeated %3") + .arg(repeatingString) + + property string repeatingString: { + switch (root.calendarItem.repeatingOption.repeatingMode) { + case RepeatingOption.RepeatingModeNone: + return qsTr("never"); + case RepeatingOption.RepeatingModeHourly: + return qsTr("hourly"); + case RepeatingOption.RepeatingModeDaily: + return qsTr("daily"); + case RepeatingOption.RepeatingModeWeekly: + var weekdays = [] + for (var i = 0; i < root.calendarItem.repeatingOption.weekDays.length; i++) { + switch (root.calendarItem.repeatingOption.weekDays[i]) { + case 1: + weekdays.push(qsTr("Mon")); + break; + case 2: + weekdays.push(qsTr("Tue")); + break; + case 3: + weekdays.push(qsTr("Wed")); + break; + case 4: + weekdays.push(qsTr("Thu")); + break; + case 5: + weekdays.push(qsTr("Fri")); + break; + case 6: + weekdays.push(qsTr("Sat")); + break; + case 7: + weekdays.push(qsTr("Sun")); + break; + } + } + + return qsTr("weekly on %1").arg(weekdays.join(', ')); + case RepeatingOption.RepeatingModeMonthly: + return qsTr("monthly on the %1").arg(root.calendarItem.repeatingOption.monthDays.join(', ')); + case RepeatingOption.RepeatingModeYearly: + return qsTr("every year"); + } + } + } + } + } + + swipe.right: MouseArea { + height: root.height + width: height + anchors.right: parent.right + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/delete.svg" + color: "red" + } + onClicked: root.removeCalendarItem() + } + + onClicked: { + var page = pageStack.push(Qt.resolvedUrl("EditCalendarItemPage.qml"), {calendarItem: root.calendarItem}) + page.onBackPressed.connect(function() {pageStack.pop()}) + page.onDone.connect(function() { + pageStack.pop() + print("calendarItem.time is now", root.calendarItem.time) + }) + } +} diff --git a/mea/ui/magic/DeviceRulesPage.qml b/mea/ui/magic/DeviceRulesPage.qml index 4147a8b2..be0852e6 100644 --- a/mea/ui/magic/DeviceRulesPage.qml +++ b/mea/ui/magic/DeviceRulesPage.qml @@ -21,14 +21,23 @@ Page { } // Rule is optional and might be initialized with anything wanted. A new, empty one will be created if null + // This Page will take ownership of the rule and delete it eventually. function addRule(rule) { if (rule === null || rule === undefined) { rule = Engine.ruleManager.createNewRule(); } - var page = pageStack.push(Qt.resolvedUrl("EditRulePage.qml"), {rule: rule}); - page.onAccept.connect(function() { + d.editRulePage = pageStack.push(Qt.resolvedUrl("EditRulePage.qml"), {rule: rule}); + d.editRulePage.StackView.onRemoved.connect(function() { + d.editRulePage.rule.destroy(); + d.editRulePage = null + }) + d.editRulePage.onAccept.connect(function() { + d.editRulePage.busy = true; Engine.ruleManager.addRule(page.rule); }) + d.editRulePage.onCancel.connect(function() { + pageStack.pop(); + }) // if (rule.eventDescriptors.count === 0) { // var eventDescriptor = rule.eventDescriptors.createNewEventDescriptor(); @@ -38,18 +47,32 @@ Page { } + QtObject { + id: d + property var editRulePage: null + } + Connections { target: Engine.ruleManager onAddRuleReply: { + d.editRulePage.busy = false; if (ruleError == "RuleErrorNoError") { pageStack.pop(); + } else { + var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); + var popup = errorDialog.createObject(root, {errorCode: ruleError }) + popup.open(); } } onEditRuleReply: { - print("have add rule reply") + d.editRulePage.busy = false; if (ruleError == "RuleErrorNoError") { pageStack.pop(); + } else { + var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); + var popup = errorDialog.createObject(root, {errorCode: ruleError }) + popup.open(); } } } @@ -72,7 +95,7 @@ Page { height: app.iconSize width: height name: "../images/magic.svg" - color: !model.enabled ? "gray" : (model.active ? "red" : app.guhAccent) + color: !model.enabled ? "red" : (model.active ? app.guhAccent : "grey") } Label { @@ -82,9 +105,16 @@ Page { } onClicked: { - var editRulePage = pageStack.push(Qt.resolvedUrl("EditRulePage.qml"), {rule: rulesFilterModel.get(index) }) - editRulePage.onAccept.connect(function() { - Engine.ruleManager.editRule(editRulePage.rule); + d.editRulePage = pageStack.push(Qt.resolvedUrl("EditRulePage.qml"), {rule: rulesFilterModel.get(index).clone() }) + d.editRulePage.StackView.onRemoved.connect(function() { + d.editRulePage.rule.destroy(); + }) + d.editRulePage.onAccept.connect(function() { + d.editRulePage.busy = true + Engine.ruleManager.editRule(d.editRulePage.rule); + }) + d.editRulePage.onCancel.connect(function() { + pageStack.pop(); }) } diff --git a/mea/ui/magic/EditCalendarItemPage.qml b/mea/ui/magic/EditCalendarItemPage.qml new file mode 100644 index 00000000..0e4ea449 --- /dev/null +++ b/mea/ui/magic/EditCalendarItemPage.qml @@ -0,0 +1,402 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import Qt.labs.calendar 1.0 +import QtQuick.Layouts 1.3 +import "../components" +import Mea 1.0 + +Page { + id: root + + property var calendarItem: null + + signal done() + signal backPressed() + + readonly property bool isDateBased: repeatingBox.currentIndex === 0 || + repeatingBox.currentIndex === 5 + readonly property bool isWeekDayBased: repeatingBox.currentIndex === 3 + readonly property bool isMonthDayBased: repeatingBox.currentIndex === 4 + + header: GuhHeader { + text: qsTr("Pick a time frame") + onBackPressed: root.backPressed(); + } + + Component.onCompleted: { + var date = root.isDateBased ? root.calendarItem.dateTime : root.calendarItem.startTime + print("starting with time:", root.calendarItem.startTime, root.calendarItem.dateTime) + if (isNaN(date)) { + console.log("Date in rule not valid, using current datetime"); + date = new Date(); + } + hourBox.currentIndex = date.getHours(); + minuteBox.currentIndex = date.getMinutes(); + dayBox.currentIndex = date.getDate() - 1; + monthBox.currentIndex = date.getMonth(); + print("should set year to", date.getFullYear()) + yearBox.currentIndex = date.getFullYear() - 1970; + + var endDate = new Date(date.getTime() + root.calendarItem.duration * 60000); + toHourBox.currentIndex = endDate.getHours(); + toMinuteBox.currentIndex = endDate.getMinutes(); + toDayBox.currentIndex = endDate.getDate() - 1; + toMonthBox.currentIndex = endDate.getMonth(); + toYearBox.currentIndex = endDate.getFullYear() - 1970 + } + + function pad(num, size) { + var s = "000000000" + num; + return s.substr(s.length-size); + } + + Flickable { + anchors.fill: parent + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + anchors { left: parent.left; top: parent.top; right: parent.right } + + GridLayout { + columns: app.landscape ? 2 : 1 + Layout.alignment: app.landscape ? Qt.AlignHCenter : Qt.AlignLeft + Layout.margins: app.margins + Layout.fillWidth: !app.landscape + + Label { + text: qsTr("From") + } + + RowLayout { + ComboBox { + id: hourBox + model: { + if (!enabled) { + return ["--"] + } + + var ret = []; + for (var i = 0; i < 24; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + enabled: repeatingBox.currentIndex !== 1 + } + Label { + text: ":" + } + ComboBox { + id: minuteBox + model: { + var ret = []; + for (var i = 0; i < 60; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + } + } + + RowLayout { + Layout.fillHeight: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + visible: root.isDateBased + ComboBox { + id: dayBox + Layout.fillWidth: true + model: { + var ret = []; + for (var i = 1; i < 31; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + } + ComboBox { + id: monthBox + Layout.fillWidth: true + model: [ + qsTr("Jan"), + qsTr("Feb"), + qsTr("Mar"), + qsTr("Apr"), + qsTr("May"), + qsTr("Jun"), + qsTr("Jul"), + qsTr("Aug"), + qsTr("Sep"), + qsTr("Oct"), + qsTr("Nov"), + qsTr("Dez") + ] + } + ComboBox { + id: yearBox + Layout.fillWidth: true + model: { + var ret = []; + for (var i = 1970; i < 2100; i++) { + ret.push(i); + } + return ret; + } + } + } + + Label { + text: qsTr("To") + } + + RowLayout { + ComboBox { + id: toHourBox + model: { + if (!enabled) { + return ["--"] + } + + var ret = []; + for (var i = 0; i < 24; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + enabled: repeatingBox.currentIndex !== 1 + } + Label { + text: ":" + } + ComboBox { + id: toMinuteBox + model: { + var ret = []; + for (var i = 0; i < 60; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + } + } + + RowLayout { + Layout.fillHeight: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + visible: root.isDateBased + ComboBox { + id: toDayBox + Layout.fillWidth: true + model: { + var ret = []; + for (var i = 1; i < 31; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + onActivated: { + var date = root.calendarItem.dateTime + date.setDate(index) + root.calendarItem.dateTime = date; + } + } + ComboBox { + id: toMonthBox + Layout.fillWidth: true + model: [ + qsTr("Jan"), + qsTr("Feb"), + qsTr("Mar"), + qsTr("Apr"), + qsTr("May"), + qsTr("Jun"), + qsTr("Jul"), + qsTr("Aug"), + qsTr("Sep"), + qsTr("Oct"), + qsTr("Nov"), + qsTr("Dez") + ] + } + ComboBox { + id: toYearBox + Layout.fillWidth: true + model: { + var ret = []; + for (var i = 1970; i < 2100; i++) { + ret.push(i); + } + return ret; + } + } + } + + Label { + text: qsTr("Repeat") + Layout.topMargin: app.margins + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: app.landscape ? app.margins : 0 + + ComboBox { + id: repeatingBox + model: [qsTr("never"), qsTr("hourly"), qsTr("daily"), qsTr("weekly"), qsTr("monthly"), qsTr("yearly")] + + currentIndex: { + switch (root.calendarItem.repeatingOption.repeatingMode) { + case RepeatingOption.RepeatingModeNone: + return 0; + case RepeatingOption.RepeatingModeHourly: + return 1; + case RepeatingOption.RepeatingModeDaily: + return 2; + case RepeatingOption.RepeatingModeWeekly: + return 3; + case RepeatingOption.RepeatingModeMonthly: + return 4; + case RepeatingOption.RepeatingModeYearly: + return 5; + } + return 0; + } + } + } + + + + Label { + text: qsTr("Weekdays") + Layout.topMargin: app.margins + visible: root.isWeekDayBased + } + + DayOfWeekRow { + id: weekDayRow + property var weekDays: root.calendarItem.repeatingOption.weekDays + visible: root.isWeekDayBased + Layout.fillWidth: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + delegate: ToolButton { + text: model.shortName + checked: weekDayRow.weekDays.indexOf(index + 1) >= 0 + onClicked: { + var copy = weekDayRow.weekDays + var idx = copy.indexOf(index + 1); + if (idx >= 0) { + copy.splice(idx, 1); + } else { + copy.push(index + 1) + } + weekDayRow.weekDays = copy + } + } + } + + + Label { + text: qsTr("Day of month") + Layout.topMargin: app.margins + visible: root.isMonthDayBased + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + } + + GridLayout { + id: monthDayGrid + columns: Math.sqrt(children.length) + Layout.fillWidth: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + visible: root.isMonthDayBased + property var monthDays: root.calendarItem.repeatingOption.monthDays + Repeater { + model: 31 + delegate: ToolButton { + Layout.fillWidth: true + checked: monthDayGrid.monthDays.indexOf(index + 1) >= 0 + text: modelData + 1 + onClicked: { + var copy = monthDayGrid.monthDays + var idx = copy.indexOf(index + 1); + if (idx >= 0) { + copy.splice(idx, 1); + } else { + copy.push(index + 1) + } + monthDayGrid.monthDays = copy + } + } + } + } + + } + + Button { + Layout.fillWidth: !app.landscape + Layout.margins: app.margins + Layout.alignment: Qt.AlignRight + text: qsTr("OK") + onClicked: { + if (root.isDateBased) { + var date = isNaN(root.calendarItem.dateTime) ? new Date() : root.calendarItem.dateTime + date.setHours(hourBox.currentIndex); + date.setMinutes(minuteBox.currentIndex); + date.setDate(dayBox.currentIndex + 1); + date.setMonth(monthBox.currentIndex); + date.setFullYear(yearBox.currentText); + root.calendarItem.dateTime = date; + + var endDate = new Date(); + endDate.setHours(toHourBox.currentIndex); + endDate.setMinutes(toMinuteBox.currentIndex); + endDate.setDate(toDayBox.currentIndex + 1); + endDate.setMilliseconds(toMonthBox.currentIndex); + endDate.setFullYear(toYearBox.currentText); + root.calendarItem.duration = (endDate.getTime() - date.getTime()) / 60000; + } else { + var time = isNaN(root.calendarItem.startTime) ? new Date() : root.calendarItem.startTime + time.setHours(hourBox.currentIndex); + time.setMinutes(minuteBox.currentIndex) + root.calendarItem.startTime = time; + + var endTime = new Date(time); + endTime.setHours(toHourBox.currentIndex); + endTime.setMinutes(toMinuteBox.currentIndex); + root.calendarItem.duration = (endTime.getTime() - time.getTime()) / 60000; + print("duration is", endTime.getTime() - time.getTime(), time, endTime, toMinuteBox.currentIndex) + } + + switch (repeatingBox.currentIndex) { + case 0: + root.calendarItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeNone; + break; + case 1: + root.calendarItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeHourly; + break; + case 2: + root.calendarItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeDaily; + break; + case 3: + root.calendarItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeWeekly; + break; + case 4: + root.calendarItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeMonthly; + break; + case 5: + root.calendarItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeYearly; + break; + } + + if (root.isWeekDayBased) { + root.calendarItem.repeatingOption.weekDays = weekDayRow.weekDays; + } + if (root.isMonthDayBased) { + root.calendarItem.repeatingOption.monthDays = monthDayGrid.monthDays; + } + + root.done() + } + } + } + } +} diff --git a/mea/ui/magic/EditRulePage.qml b/mea/ui/magic/EditRulePage.qml index 4522bfd0..cfb740d4 100644 --- a/mea/ui/magic/EditRulePage.qml +++ b/mea/ui/magic/EditRulePage.qml @@ -8,15 +8,17 @@ Page { id: root property var rule: null + property bool busy: false - signal accept(); - - onAccept: busyOverlay.opacity = 1 - - readonly property bool isStateBased: rule.eventDescriptors.count === 0 - readonly property bool actionsVisible: rule.eventDescriptors.count > 0 || rule.stateEvaluator !== null + readonly property bool isEventBased: rule.eventDescriptors.count > 0 || rule.timeDescriptor.timeEventItems.count > 0 + readonly property bool isStateBased: (rule.stateEvaluator !== null || rule.timeDescriptor.calendarItems.count > 0) && !isEventBased + readonly property bool actionsVisible: !isEmpty readonly property bool exitActionsVisible: actionsVisible && isStateBased readonly property bool hasExitActions: rule.exitActions.count > 0 + readonly property bool isEmpty: !isEventBased && !isStateBased + + signal accept(); + signal cancel(); function addEventDescriptor() { var eventDescriptor = root.rule.eventDescriptors.createNewEventDescriptor(); @@ -41,6 +43,32 @@ Page { }) } + function addTimeEventItem() { + var timeEventItem = root.rule.timeDescriptor.timeEventItems.createNewTimeEventItem(); + var page = pageStack.push(Qt.resolvedUrl("EditTimeEventItemPage.qml"), {timeEventItem: timeEventItem}); + page.onBackPressed.connect(function() { + pageStack.pop() + timeEventItem.destroy(); + }) + page.onDone.connect(function() { + root.rule.timeDescriptor.timeEventItems.addTimeEventItem(timeEventItem); + pageStack.pop(); + }) + } + + function addCalendarItem() { + var calendarItem = root.rule.timeDescriptor.calendarItems.createNewCalendarItem(); + var page = pageStack.push(Qt.resolvedUrl("EditCalendarItemPage.qml"), {calendarItem: calendarItem}); + page.onBackPressed.connect(function() { + pageStack.pop(); + calendarItem.destroy(); + }) + page.onDone.connect(function() { + root.rule.timeDescriptor.calendarItems.addCalendarItem(calendarItem); + pageStack.pop(); + }) + } + function editStateEvaluator() { print("opening page", root.rule.stateEvaluator) var page = pageStack.push(Qt.resolvedUrl("EditStateEvaluatorPage.qml"), { stateEvaluator: root.rule.stateEvaluator }) @@ -91,15 +119,14 @@ Page { } header: GuhHeader { - text: qsTr("New rule") - onBackPressed: pageStack.pop() + text: root.rule.name.length === 0 ? qsTr("Add new magic") : qsTr("Edit %1").arg(root.rule.name) + onBackPressed: root.cancel() + HeaderButton { imageSource: "../images/tick.svg" enabled: actionsRepeater.count > 0 && root.rule.name.length > 0 opacity: enabled ? 1 : .3 - onClicked: { - root.accept() - } + onClicked: root.accept() } } @@ -109,24 +136,47 @@ Page { ColumnLayout { id: contentColumn - anchors { left: parent.left; top: parent.top; right: parent.right; topMargin: app.margins } + anchors { left: parent.left; top: parent.top; right: parent.right; } ColumnLayout { + id: ruleSettings Layout.fillWidth: true - Layout.margins: app.margins - Label { - Layout.fillWidth: true - text: qsTr("Rule name") - } - TextField { - Layout.fillWidth: true - text: root.rule.name - onTextChanged: { - root.rule.name = text; + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + Layout.topMargin: app.margins + + property bool showDetails: false - } - } RowLayout { Layout.fillWidth: true + spacing: app.margins + + Label { + text: qsTr("Name:") + } + + TextField { + Layout.fillWidth: true + text: root.rule.name + onTextChanged: root.rule.name = text; + } + + ColorIcon { + name: "../images/settings.svg" + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + MouseArea { + anchors.fill: parent + onClicked: ruleSettings.showDetails = !ruleSettings.showDetails + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: ruleSettings.showDetails ? implicitHeight : 0 + opacity: ruleSettings.showDetails ? 1 : 0 + Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad} } + Behavior on opacity { NumberAnimation {duration: 200; easing.type: Easing.InOutQuad } } Label { Layout.fillWidth: true text: qsTr("This rule is enabled") @@ -140,83 +190,49 @@ Page { } } - ThinDivider { visible: !root.hasExitActions } + ThinDivider { visible: !root.isStateBased } Label { Layout.fillWidth: true Layout.margins: app.margins font.pixelSize: app.mediumFont - text: qsTr("Events triggering this rule") - visible: !root.hasExitActions + wrapMode: Text.WordWrap + text: eventsRepeater.count === 0 && timeEventRepeater.count === 0 ? + qsTr("Execute actions when something happens.") : + qsTr("When any of these events happen...") + visible: !root.isStateBased + font.bold: true } + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + font.italic: true + text: qsTr("Examples:\n• When a button is pressed...\n• When the temperature changes...\n• At 7 am...") + visible: root.isEmpty + } + Repeater { id: eventsRepeater model: root.hasExitActions ? null : root.rule.eventDescriptors - delegate: SwipeDelegate { - id: eventDelegate + delegate: EventDescriptorDelegate { Layout.fillWidth: true - readonly property var eventDescriptor: root.rule.eventDescriptors.get(index) - property var device: Engine.deviceManager.devices.getDevice(eventDescriptor.deviceId) - property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null - property var iface: eventDescriptor.interfaceName ? Interfaces.findByName(eventDescriptor.interfaceName) : null - property var eventType: deviceClass ? deviceClass.eventTypes.getEventType(eventDescriptor.eventTypeId) - : iface ? iface.eventTypes.findByName(eventDescriptor.interfaceEvent) : null - contentItem: ColumnLayout { - Label { - text: qsTr("%1 - %2").arg(eventDelegate.device ? eventDelegate.device.name : eventDelegate.iface.displayName).arg(eventDelegate.eventType.displayName) - Layout.fillWidth: true - elide: Text.ElideRight - } - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Repeater { - model: eventDelegate.eventDescriptor.paramDescriptors - Label { - text: { - var ret = eventDelegate.eventType.paramTypes.getParamType(model.id).displayName - switch (model.operator) { - case ParamDescriptor.ValueOperatorEquals: - ret += " = "; - break; - case ParamDescriptor.ValueOperatorNotEquals: - ret += " != "; - break; - case ParamDescriptor.ValueOperatorGreater: - ret += " > "; - break; - case ParamDescriptor.ValueOperatorGreaterOrEqual: - ret += " >= "; - break; - case ParamDescriptor.ValueOperatorLess: - ret += " < "; - break; - case ParamDescriptor.ValueOperatorLessOrEqual: - ret += " <= "; - break; - default: - ret += " ? "; - } + eventDescriptor: root.rule.eventDescriptors.get(index) + onRemoveEventDescriptor: root.rule.eventDescriptors.removeEventDescriptor(index) + } + } - ret += model.value - return ret; - } - } - } - } - } - swipe.right: MouseArea { - height: eventDelegate.height - width: height - anchors.right: parent.right - ColorIcon { - anchors.fill: parent - anchors.margins: app.margins - name: "../images/delete.svg" - color: "red" - } - onClicked: root.rule.eventDescriptors.removeEventDescriptor(index) + Repeater { + id: timeEventRepeater + model: root.rule.timeDescriptor.timeEventItems + delegate: TimeEventDelegate { + Layout.fillWidth: true + timeEventItem: root.rule.timeDescriptor.timeEventItems.get(index) + onRemoveTimeEventItem: { + root.rule.timeDescriptor.timeEventItems.removeTimeEventItem(index); } } } @@ -224,18 +240,58 @@ Page { Button { Layout.fillWidth: true Layout.margins: app.margins - text: eventsRepeater.count == 0 ? qsTr("Add an event...") : qsTr("Add another event...") - onClicked: root.addEventDescriptor(); - visible: !root.hasExitActions + text: eventsRepeater.count == 0 && timeEventRepeater.count === 0 ? qsTr("Configure...") : qsTr("Add another...") + visible: !root.isStateBased + onClicked: { + if (root.rule.timeDescriptor.calendarItems.count > 0) { + root.addEventDescriptor() + } else { + var popup = eventQuestionDialogComponent.createObject(root) + popup.open(); + } + } } ThinDivider {} Label { - text: qsTr("Conditions to be met") + text: root.isEmpty ? + qsTr("Do something while a condition is met.") : + root.isEventBased ? + qsTr("...but only if those conditions are met...") : + qsTr("When this condition...") font.pixelSize: app.mediumFont Layout.fillWidth: true Layout.margins: app.margins + font.bold: true + visible: { + if (root.isEventBased) { + if (root.rule.timeDescriptor.calendarItems.count === 0) { + return true; + } + + if (root.rule.stateEvaluator === null) { + return false; + } + } else { + if (root.rule.stateEvaluator === null && root.rule.timeDescriptor.calendarItems.count > 0) { + return false; + } + } + + return true; + } + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + wrapMode: Text.WordWrap + font.pixelSize: app.smallFont + font.italic: true + text: qsTr("Examples:\n• While I'm at home...\n• When the temperature is below 0...\n• Between 9 am and 6 pm...") + visible: root.isEmpty } StateEvaluatorDelegate { @@ -244,70 +300,68 @@ Page { visible: root.rule.stateEvaluator !== null } + Label { + text: root.rule.stateEvaluator === null && !root.isEventBased ? + qsTr("When time is in...") : + qsTr("...during this time...") + font.pixelSize: app.mediumFont + Layout.fillWidth: true + Layout.margins: app.margins + font.bold: true + visible: root.rule.timeDescriptor.timeEventItems.count === 0 && (root.rule.timeDescriptor.calendarItems.count > 0 || root.rule.stateEvaluator !== null) + } + + Repeater { + model: root.rule.timeDescriptor.calendarItems + delegate: CalendarItemDelegate { + Layout.fillWidth: true + calendarItem: root.rule.timeDescriptor.calendarItems.get(index) + onRemoveCalendarItem: { + root.rule.timeDescriptor.calendarItems.removeCalendarItem(index) + } + } + } + Button { Layout.fillWidth: true Layout.margins: app.margins - text: qsTr("Add a condition") - visible: root.rule.stateEvaluator === null + text: root.rule.stateEvaluator === null && root.rule.timeDescriptor.calendarItems.count === 0 ? + qsTr("Configure...") : + qsTr("Add another...") + visible: root.rule.timeDescriptor.timeEventItems.count === 0 || root.rule.stateEvaluator === null onClicked: { - root.rule.createStateEvaluator(); -// root.editStateEvaluator() + if (root.rule.timeDescriptor.timeEventItems.count > 0) { + root.rule.createStateEvaluator() + } else if (root.rule.stateEvaluator !== null) { + root.addCalendarItem(); + } else { + var popup = stateQuestionDialogComponent.createObject(root) + popup.open() + } } } ThinDivider { visible: root.actionsVisible } Label { - text: root.isStateBased ? qsTr("Active state enter actions") : qsTr("Actions to execute") + text: root.isStateBased ? + (root.rule.stateEvaluator === 0 ? qsTr("...come true, execute those actions:") : qsTr("...comes true, execute those actions:")) : + qsTr("...execute those actions:") font.pixelSize: app.mediumFont Layout.fillWidth: true Layout.margins: app.margins + wrapMode: Text.WordWrap visible: root.actionsVisible + font.bold: true } Repeater { id: actionsRepeater model: root.actionsVisible ? root.rule.actions : null - delegate: SwipeDelegate { - id: actionDelegate + delegate: RuleActionDelegate { Layout.fillWidth: true - property var ruleAction: root.rule.actions.get(index) - property var device: ruleAction.deviceId ? Engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null - property var iface: ruleAction.interfaceName ? Interfaces.findByName(ruleAction.interfaceName) : null - property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null - property var actionType: deviceClass ? deviceClass.actionTypes.getActionType(ruleAction.actionTypeId) - : iface ? iface.actionTypes.findByName(ruleAction.interfaceAction) : null - contentItem: ColumnLayout { - Label { - Layout.fillWidth: true - text: qsTr("%1 - %2").arg(actionDelegate.device ? actionDelegate.device.name : actionDelegate.iface.displayName).arg(actionDelegate.actionType.displayName) - } - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Repeater { - model: actionDelegate.ruleAction.ruleActionParams - Label { - text: actionDelegate.actionType.paramTypes.getParamType(model.paramTypeId).displayName + " -> " + - (model.eventParamTypeId.length > 0 ? qsTr("value from event") : model.value) - font.pixelSize: app.smallFont - } - } - } - } - swipe.right: MouseArea { - height: actionDelegate.height - width: height - anchors.right: parent.right - ColorIcon { - anchors.fill: parent - anchors.margins: app.margins - name: "../images/delete.svg" - color: "red" - } - onClicked: root.rule.actions.removeRuleAction(index) - } + ruleAction: root.rule.actions.get(index) + onRemoveRuleAction: root.rule.actions.removeRuleAction(index) } } @@ -322,56 +376,23 @@ Page { ThinDivider { visible: root.exitActionsVisible } Label { - text: qsTr("Active state exit actions") - font.pixelSize: app.mediumFont + text: qsTr("...isn't met any more, execute those actions:") Layout.fillWidth: true Layout.margins: app.margins + wrapMode: Text.WordWrap visible: root.exitActionsVisible + font.pixelSize: app.mediumFont + font.bold: true } Repeater { id: exitActionsRepeater model: root.exitActionsVisible ? root.rule.exitActions : null - delegate: SwipeDelegate { - id: exitActionDelegate + delegate: RuleActionDelegate { Layout.fillWidth: true - property var ruleAction: root.rule.exitActions.get(index) - property var device: ruleAction.deviceId ? Engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null - property var iface: ruleAction.interfaceName ? Interfaces.findByName(ruleAction.interfaceName) : null - property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null - property var actionType: deviceClass ? deviceClass.actionTypes.getActionType(ruleAction.actionTypeId) - : iface ? iface.actionTypes.findByName(ruleAction.interfaceAction) : null - contentItem: ColumnLayout { - Label { - Layout.fillWidth: true - text: qsTr("%1 - %2").arg(exitActionDelegate.device ? exitActionDelegate.device.name : exitActionDelegate.iface.displayName).arg(exitActionDelegate.actionType.displayName) - } - - RowLayout { - Layout.fillWidth: true - spacing: app.margins - Repeater { - model: exitActionDelegate.ruleAction.ruleActionParams - Label { - text: exitActionDelegate.actionType.paramTypes.getParamType(model.paramTypeId).displayName + " -> " + model.value - font.pixelSize: app.smallFont - } - } - } - } - swipe.right: MouseArea { - height: exitActionDelegate.height - width: height - anchors.right: parent.right - ColorIcon { - anchors.fill: parent - anchors.margins: app.margins - name: "../images/delete.svg" - color: "red" - } - onClicked: root.rule.exitActions.removeRuleAction(index) - } + ruleAction: root.rule.exitActions.get(index) + onClicked: root.rule.exitActions.removeRuleAction(index) } } @@ -389,11 +410,145 @@ Page { id: busyOverlay anchors.fill: parent color: "#55000000" - opacity: 0 + opacity: root.busy ? 1 : 0 Behavior on opacity { NumberAnimation {duration: 200 } } BusyIndicator { anchors.centerIn: parent running: parent.opacity > 0 } } + + Component { + id: eventQuestionDialogComponent + MeaDialog { + id: questionDialog + title: qsTr("Add event...") + standardButtons: Dialog.Cancel + + Button { + Layout.fillWidth: true + Layout.preferredHeight: (app.largeFont * 2) + (app.margins * 3) + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + name: "../images/event.svg" + color: "black" + } + Label { + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("When one of my things triggers an event") + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + } + } + onClicked: { + root.addEventDescriptor() + questionDialog.close() + } + } + + Label { + text: qsTr("or") + Layout.fillWidth: true + Layout.margins: app.margins + horizontalAlignment: Text.AlignHCenter + } + + Button { + Layout.fillWidth: true + Layout.preferredHeight: (app.largeFont * 2) + (app.margins * 3) + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + name: "../images/alarm-clock.svg" + color: "black" + } + Label { + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("At a particular time or date") + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + } + } + onClicked: { + root.addTimeEventItem() + questionDialog.close() + } + } + } + } + + Component { + id: stateQuestionDialogComponent + MeaDialog { + id: questionDialog + title: qsTr("Add condition...") + standardButtons: Dialog.Cancel + + + Button { + Layout.fillWidth: true + Layout.preferredHeight: (app.largeFont * 2) + (app.margins * 3) + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + name: "../images/state.svg" + color: "black" + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("When one of my things is in a certain state") + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + } + } + onClicked: { + root.rule.createStateEvaluator() + questionDialog.close() + } + } + + Label { + text: qsTr("or") + Layout.fillWidth: true + Layout.margins: app.margins + horizontalAlignment: Text.AlignHCenter + } + + Button { + Layout.fillWidth: true + Layout.preferredHeight: (app.largeFont * 2) + (app.margins * 3) + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: height + name: "../images/clock-app-symbolic.svg" + color: "black" + } + Label { + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("During a given time") + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + } + } + onClicked: { + root.addCalendarItem() + questionDialog.close() + } + } + } + } } diff --git a/mea/ui/magic/EditTimeEventItemPage.qml b/mea/ui/magic/EditTimeEventItemPage.qml new file mode 100644 index 00000000..6f305546 --- /dev/null +++ b/mea/ui/magic/EditTimeEventItemPage.qml @@ -0,0 +1,301 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import Qt.labs.calendar 1.0 +import QtQuick.Layouts 1.3 +import "../components" +import Mea 1.0 + +Page { + id: root + + property var timeEventItem: null + + signal done() + signal backPressed() + + readonly property bool isDateBased: repeatingBox.currentIndex === 0 || + repeatingBox.currentIndex === 5 + readonly property bool isWeekDayBased: repeatingBox.currentIndex === 3 + readonly property bool isMonthDayBased: repeatingBox.currentIndex === 4 + + header: GuhHeader { + text: qsTr("Pick a time") + onBackPressed: root.backPressed(); + } + + Component.onCompleted: { + var date = root.isDateBased ? root.timeEventItem.dateTime : root.timeEventItem.time + print("starting with time:", root.timeEventItem.time, root.timeEventItem.dateTime) + if (isNaN(date)) { + console.log("Date in rule not valid, using current datetime"); + date = new Date(); + } + hourBox.currentIndex = date.getHours(); + minuteBox.currentIndex = date.getMinutes(); + dayBox.currentIndex = date.getDate() - 1; + monthBox.currentIndex = date.getMonth(); + print("should set year to", date.getFullYear()) + yearBox.currentIndex = date.getFullYear() - 1970; + } + + function pad(num, size) { + var s = "000000000" + num; + return s.substr(s.length-size); + } + + Flickable { + anchors.fill: parent + contentHeight: mainColumn.implicitHeight + + ColumnLayout { + id: mainColumn + anchors { left: parent.left; top: parent.top; right: parent.right } + + GridLayout { + columns: app.landscape ? 2 : 1 + Layout.alignment: app.landscape ? Qt.AlignHCenter : Qt.AlignLeft + Layout.margins: app.margins + Layout.fillWidth: !app.landscape + + Label { + text: qsTr("Time") + } + + RowLayout { + ComboBox { + id: hourBox + model: { + if (!enabled) { + return ["--"] + } + + var ret = []; + for (var i = 0; i < 24; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + enabled: repeatingBox.currentIndex !== 1 + } + Label { + text: ":" + } + ComboBox { + id: minuteBox + model: { + var ret = []; + for (var i = 0; i < 60; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + } + } + + Label { + text: qsTr("Repeat") + Layout.topMargin: app.margins + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: app.landscape ? app.margins : 0 + + ComboBox { + id: repeatingBox + model: [qsTr("never"), qsTr("hourly"), qsTr("daily"), qsTr("weekly"), qsTr("monthly"), qsTr("yearly")] + + currentIndex: { + switch (root.timeEventItem.repeatingOption.repeatingMode) { + case RepeatingOption.RepeatingModeNone: + return 0; + case RepeatingOption.RepeatingModeHourly: + return 1; + case RepeatingOption.RepeatingModeDaily: + return 2; + case RepeatingOption.RepeatingModeWeekly: + return 3; + case RepeatingOption.RepeatingModeMonthly: + return 4; + case RepeatingOption.RepeatingModeYearly: + return 5; + } + return 0; + } + } + } + + + Label { + text: qsTr("Date") + Layout.topMargin: app.margins + visible: root.isDateBased + } + + RowLayout { + Layout.fillHeight: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + visible: root.isDateBased + ComboBox { + id: dayBox + Layout.fillWidth: true + model: { + var ret = []; + for (var i = 1; i < 31; i++) { + ret.push(pad(i, 2)); + } + return ret; + } + } + ComboBox { + id: monthBox + Layout.fillWidth: true + model: [ + qsTr("Jan"), + qsTr("Feb"), + qsTr("Mar"), + qsTr("Apr"), + qsTr("May"), + qsTr("Jun"), + qsTr("Jul"), + qsTr("Aug"), + qsTr("Sep"), + qsTr("Oct"), + qsTr("Nov"), + qsTr("Dez") + ] + } + ComboBox { + id: yearBox + Layout.fillWidth: true + model: { + var ret = []; + for (var i = 1970; i < 2100; i++) { + ret.push(i); + } + return ret; + } + } + } + + Label { + text: qsTr("Weekdays") + Layout.topMargin: app.margins + visible: root.isWeekDayBased + } + + DayOfWeekRow { + id: weekDayRow + property var weekDays: root.timeEventItem.repeatingOption.weekDays + visible: root.isWeekDayBased + Layout.fillWidth: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + delegate: ToolButton { + text: model.shortName + checked: weekDayRow.weekDays.indexOf(index + 1) >= 0 + onClicked: { + var copy = weekDayRow.weekDays + var idx = copy.indexOf(index + 1); + if (idx >= 0) { + copy.splice(idx, 1); + } else { + copy.push(index + 1) + } + weekDayRow.weekDays = copy + } + } + } + + + Label { + text: qsTr("Day of month") + Layout.topMargin: app.margins + visible: root.isMonthDayBased + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + } + + GridLayout { + id: monthDayGrid + columns: Math.sqrt(children.length) + Layout.fillWidth: !app.landscape + Layout.topMargin: app.landscape ? app.margins : 0 + visible: root.isMonthDayBased + property var monthDays: root.timeEventItem.repeatingOption.monthDays + Repeater { + model: 31 + delegate: ToolButton { + Layout.fillWidth: true + checked: monthDayGrid.monthDays.indexOf(index + 1) >= 0 + text: modelData + 1 + onClicked: { + var copy = monthDayGrid.monthDays + var idx = copy.indexOf(index + 1); + if (idx >= 0) { + copy.splice(idx, 1); + } else { + copy.push(index + 1) + } + monthDayGrid.monthDays = copy + } + } + } + } + + } + + Button { + Layout.fillWidth: !app.landscape + Layout.margins: app.margins + Layout.alignment: Qt.AlignRight + text: qsTr("OK") + onClicked: { + if (root.isDateBased) { + var date = isNaN(root.timeEventItem.dateTime) ? new Date() : root.timeEventItem.dateTime + date.setHours(hourBox.currentIndex); + date.setMinutes(minuteBox.currentIndex); + date.setDate(dayBox.currentIndex + 1); + date.setMonth(monthBox.currentIndex); + date.setYear(yearBox.currentText); + root.timeEventItem.dateTime = date; + } else { + var time = isNaN(root.timeEventItem.time) ? new Date() : root.timeEventItem.time + time.setHours(hourBox.currentIndex); + time.setMinutes(minuteBox.currentIndex) + root.timeEventItem.time = time; + } + + switch (repeatingBox.currentIndex) { + case 0: + root.timeEventItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeNone; + break; + case 1: + root.timeEventItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeHourly; + break; + case 2: + root.timeEventItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeDaily; + break; + case 3: + root.timeEventItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeWeekly; + break; + case 4: + root.timeEventItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeMonthly; + break; + case 5: + root.timeEventItem.repeatingOption.repeatingMode = RepeatingOption.RepeatingModeYearly; + break; + } + + if (root.isWeekDayBased) { + root.timeEventItem.repeatingOption.weekDays = weekDayRow.weekDays; + } + if (root.isMonthDayBased) { + root.timeEventItem.repeatingOption.monthDays = monthDayGrid.monthDays; + } + + root.done() + } + } + } + } + +} diff --git a/mea/ui/magic/EventDescriptorDelegate.qml b/mea/ui/magic/EventDescriptorDelegate.qml new file mode 100644 index 00000000..27e99d7c --- /dev/null +++ b/mea/ui/magic/EventDescriptorDelegate.qml @@ -0,0 +1,99 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import Mea 1.0 +import "../components" + +SwipeDelegate { + id: root + implicitHeight: app.delegateHeight + + property var eventDescriptor: null + readonly property var device: eventDescriptor ? Engine.deviceManager.devices.getDevice(eventDescriptor.deviceId) : null + readonly property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + readonly property var iface: eventDescriptor.interfaceName ? Interfaces.findByName(eventDescriptor.interfaceName) : null + readonly property var eventType: deviceClass ? deviceClass.eventTypes.getEventType(eventDescriptor.eventTypeId) + : iface ? iface.eventTypes.findByName(eventDescriptor.interfaceEvent) : null + + signal removeEventDescriptor() + + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "../images/event.svg" + color: app.guhAccent + } + + ColumnLayout { + Label { + text: qsTr("%1 - %2").arg(root.device ? root.device.name : root.iface.displayName).arg(root.eventType.displayName) + Layout.fillWidth: true + elide: Text.ElideRight + } + Label { + Layout.fillWidth: true + elide: Text.ElideRight + font.pixelSize: app.smallFont + text: { + var ret = qsTr("anytime"); + for (var i = 0; i < root.eventDescriptor.paramDescriptors.count; i++) { + var paramDescriptor = root.eventDescriptor.paramDescriptors.get(i) + var operatorString; + switch (paramDescriptor.operatorType) { + case ParamDescriptor.ValueOperatorEquals: + operatorString = " = "; + break; + case ParamDescriptor.ValueOperatorNotEquals: + operatorString = " != "; + break; + case ParamDescriptor.ValueOperatorGreater: + operatorString = " > "; + break; + case ParamDescriptor.ValueOperatorGreaterOrEqual: + operatorString = " >= "; + break; + case ParamDescriptor.ValueOperatorLess: + operatorString = " < "; + break; + case ParamDescriptor.ValueOperatorLessOrEqual: + operatorString = " <= "; + break; + default: + operatorString = " ? "; + } + + if (i === 0) { + // TRANSLATORS: example: "only if temperature > 5" + ret = qsTr("only if %1 %2 %3") + .arg(root.eventType.paramTypes.getParamType(paramDescriptor.paramTypeId).displayName) + .arg(operatorString) + .arg(paramDescriptor.value) + } else { + // TRANSLATORS: example: "and temperature > 5" + ret += " " + qsTr("and %1 %2 %3") + .arg(root.eventType.paramTypes.getParamType(paramDescriptor.paramTypeId).displayName) + .arg(operatorString) + .arg(model.value) + } + } + + return ret; + } + } + } + } + swipe.right: MouseArea { + height: root.height + width: height + anchors.right: parent.right + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/delete.svg" + color: "red" + } + onClicked: root.removeEventDescriptor() + } +} diff --git a/mea/ui/magic/RuleActionDelegate.qml b/mea/ui/magic/RuleActionDelegate.qml new file mode 100644 index 00000000..e357a97c --- /dev/null +++ b/mea/ui/magic/RuleActionDelegate.qml @@ -0,0 +1,66 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import Mea 1.0 +import "../components" + +SwipeDelegate { + id: root + implicitHeight: app.delegateHeight + property var ruleAction: null + + property var device: ruleAction.deviceId ? Engine.deviceManager.devices.getDevice(ruleAction.deviceId) : null + property var iface: ruleAction.interfaceName ? Interfaces.findByName(ruleAction.interfaceName) : null + property var deviceClass: device ? Engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null + property var actionType: deviceClass ? deviceClass.actionTypes.getActionType(ruleAction.actionTypeId) + : iface ? iface.actionTypes.findByName(ruleAction.interfaceAction) : null + + signal removeRuleAction() + + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "../images/action.svg" + color: app.guhAccent + } + + ColumnLayout { + Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: qsTr("%1 - %2").arg(root.device ? root.device.name : root.iface.displayName).arg(root.actionType.displayName) + } + Label { + Layout.fillWidth: true + elide: Text.ElideRight + font.pixelSize: app.smallFont + text: { + var ret = []; + for (var i = 0; i < root.ruleAction.ruleActionParams.count; i++) { + var ruleActionParam = root.ruleAction.ruleActionParams.get(i) + var paramString = qsTr("%1: %2") + .arg(root.actionType.paramTypes.getParamType(ruleActionParam.paramTypeId).displayName) + .arg(ruleActionParam.eventParamTypeId.length > 0 ? qsTr("value from event") : ruleActionParam.value) + ret.push(paramString) + } + return ret.join(', ') + } + + } + } + } + swipe.right: MouseArea { + height: root.height + width: height + anchors.right: parent.right + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/delete.svg" + color: "red" + } + onClicked: root.removeRuleAction() + } +} diff --git a/mea/ui/magic/SelectRuleActionParamsPage.qml b/mea/ui/magic/SelectRuleActionParamsPage.qml index 9829c847..7f8b09ac 100644 --- a/mea/ui/magic/SelectRuleActionParamsPage.qml +++ b/mea/ui/magic/SelectRuleActionParamsPage.qml @@ -96,6 +96,7 @@ Page { anchors.fill: parent anchors.margins: app.margins text: eventDescriptorParamsFilterModel.device.name + " - " + eventDescriptorParamsFilterModel.eventType.displayName + " - " + eventDescriptorParamsFilterModel.paramDescriptor.displayName + elide: Text.ElideRight } } diff --git a/mea/ui/magic/TimeEventDelegate.qml b/mea/ui/magic/TimeEventDelegate.qml new file mode 100644 index 00000000..b5e70b9b --- /dev/null +++ b/mea/ui/magic/TimeEventDelegate.qml @@ -0,0 +1,109 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import Mea 1.0 +import "../components" + +SwipeDelegate { + id: root + implicitHeight: app.delegateHeight + + property var timeEventItem: null + + readonly property bool isDateBased: timeEventItem.repeatingOption.repeatingMode === RepeatingOption.RepeatingModeNone || + timeEventItem.repeatingOption.repeatingMode === RepeatingOption.RepeatingModeYearly + + signal removeTimeEventItem(); + + contentItem: RowLayout { + spacing: app.margins + ColorIcon { + Layout.preferredHeight: app.iconSize + Layout.preferredWidth: app.iconSize + name: "../images/alarm-clock.svg" + color: app.guhAccent + } + + ColumnLayout { + + Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: qsTr("At %1").arg(root.isDateBased ? Qt.formatDateTime(root.timeEventItem.dateTime) : Qt.formatTime(root.timeEventItem.time)) + } + + Label { + Layout.fillWidth: true + text: qsTr("repeated %1").arg(repeatingString) + elide: Text.ElideRight + font.pixelSize: app.smallFont + + property string repeatingString: { + switch (root.timeEventItem.repeatingOption.repeatingMode) { + case RepeatingOption.RepeatingModeNone: + return qsTr("never"); + case RepeatingOption.RepeatingModeHourly: + return qsTr("hourly"); + case RepeatingOption.RepeatingModeDaily: + return qsTr("daily"); + case RepeatingOption.RepeatingModeWeekly: + var weekdays = [] + for (var i = 0; i < root.timeEventItem.repeatingOption.weekDays.length; i++) { + switch (root.timeEventItem.repeatingOption.weekDays[i]) { + case 1: + weekdays.push(qsTr("Mon")); + break; + case 2: + weekdays.push(qsTr("Tue")); + break; + case 3: + weekdays.push(qsTr("Wed")); + break; + case 4: + weekdays.push(qsTr("Thu")); + break; + case 5: + weekdays.push(qsTr("Fri")); + break; + case 6: + weekdays.push(qsTr("Sat")); + break; + case 7: + weekdays.push(qsTr("Sun")); + break; + } + } + + return qsTr("weekly on %1").arg(weekdays.join(', ')); + case RepeatingOption.RepeatingModeMonthly: + return qsTr("monthly on the %1").arg(root.timeEventItem.repeatingOption.monthDays.join(', ')); + case RepeatingOption.RepeatingModeYearly: + return qsTr("every year"); + } + } + } + } + } + + swipe.right: MouseArea { + height: root.height + width: height + anchors.right: parent.right + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/delete.svg" + color: "red" + } + onClicked: root.removeTimeEventItem() + } + + onClicked: { + var page = pageStack.push(Qt.resolvedUrl("EditTimeEventItemPage.qml"), {timeEventItem: root.timeEventItem}) + page.onBackPressed.connect(function() {pageStack.pop()}) + page.onDone.connect(function() { + pageStack.pop() + print("timeeventItem.time is now", root.timeEventItem.time) + }) + } +} diff --git a/mea/ui/system/PluginParamsPage.qml b/mea/ui/system/PluginParamsPage.qml index 65560116..970f256c 100644 --- a/mea/ui/system/PluginParamsPage.qml +++ b/mea/ui/system/PluginParamsPage.qml @@ -30,7 +30,7 @@ Page { pageStack.pop(); } else { console.warn("Error saving plugin params:", JSON.stringify(params)) - var dialog = errorDialog.createObject(root, {title: "Error", text: qsTr("Error saving params: ") + JSON.stringify(params.params.deviceError)}); + var dialog = errorDialog.createObject(root, {errorCode: params.params.deviceError}); dialog.open(); } }