This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
powersync-app/mea/ui/magic/EditRulePage.qml

555 lines
21 KiB
QML

import QtQuick 2.8
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.1
import "../components"
import Mea 1.0
Page {
id: root
property var rule: null
property bool busy: false
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();
var page = pageStack.push(Qt.resolvedUrl("SelectThingPage.qml"));
page.onBackPressed.connect(function() { pageStack.pop(); });
page.onThingSelected.connect(function(device) {
eventDescriptor.deviceId = device.id;
selectEventDescriptorData(eventDescriptor)
})
page.onInterfaceSelected.connect(function(interfaceName) {
eventDescriptor.interfaceName = interfaceName;
selectEventDescriptorData(eventDescriptor)
})
}
function selectEventDescriptorData(eventDescriptor) {
var eventPage = pageStack.push(Qt.resolvedUrl("SelectEventDescriptorPage.qml"), {text: "Select event", eventDescriptor: eventDescriptor});
eventPage.onBackPressed.connect(function() {pageStack.pop()})
eventPage.onDone.connect(function() {
root.rule.eventDescriptors.addEventDescriptor(eventPage.eventDescriptor);
pageStack.pop(root)
})
}
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 })
}
function addAction() {
var ruleAction = root.rule.actions.createNewRuleAction();
var page = pageStack.push(Qt.resolvedUrl("SelectThingPage.qml"));
page.onBackPressed.connect(function() { pageStack.pop() })
page.onThingSelected.connect(function(device) {
print("thing selected", device.name, device.id)
ruleAction.deviceId = device.id;
selectRuleActionData(root.rule.actions, ruleAction)
})
page.onInterfaceSelected.connect(function(interfaceName) {
print("interface selected", interfaceName)
ruleAction.interfaceName = interfaceName;
selectRuleActionData(root.rule.actions, ruleAction)
})
}
function addExitAction() {
var ruleAction = root.rule.exitActions.createNewRuleAction();
var page = pageStack.push(Qt.resolvedUrl("SelectThingPage.qml"));
page.onBackPressed.connect(function() { pageStack.pop() })
page.onThingSelected.connect(function(device) {
print("thing selected", device.name, device.id)
ruleAction.deviceId = device.id;
selectRuleActionData(root.rule.exitActions, ruleAction)
})
page.onInterfaceSelected.connect(function(interfaceName) {
print("interface selected", interfaceName)
ruleAction.interfaceName = interfaceName;
selectRuleActionData(root.rule.exitActions, ruleAction)
})
}
function selectRuleActionData(ruleActions, ruleAction) {
print("opening with ruleAction", ruleAction)
var ruleActionPage = pageStack.push(Qt.resolvedUrl("SelectRuleActionPage.qml"), {text: "Select action", ruleAction: ruleAction, rule: rule });
ruleActionPage.onBackPressed.connect(function() {
pageStack.pop(root);
ruleAction.destroy();
})
ruleActionPage.onDone.connect(function() {
ruleActions.addRuleAction(ruleAction)
pageStack.pop(root);
})
}
header: GuhHeader {
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()
}
}
Flickable {
anchors.fill: parent
contentHeight: contentColumn.implicitHeight + app.margins
ColumnLayout {
id: contentColumn
anchors { left: parent.left; top: parent.top; right: parent.right; }
ColumnLayout {
id: ruleSettings
Layout.fillWidth: true
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")
}
CheckBox {
checked: root.rule.enabled
onClicked: {
root.rule.enabled = checked
}
}
}
}
ThinDivider { visible: !root.isStateBased }
Label {
Layout.fillWidth: true
Layout.margins: app.margins
font.pixelSize: app.mediumFont
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: EventDescriptorDelegate {
Layout.fillWidth: true
eventDescriptor: root.rule.eventDescriptors.get(index)
onRemoveEventDescriptor: 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);
}
}
}
Button {
Layout.fillWidth: true
Layout.margins: app.margins
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: 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 {
Layout.fillWidth: true
stateEvaluator: root.rule.stateEvaluator
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: 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: {
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 ?
(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: RuleActionDelegate {
Layout.fillWidth: true
ruleAction: root.rule.actions.get(index)
onRemoveRuleAction: root.rule.actions.removeRuleAction(index)
}
}
Button {
Layout.fillWidth: true
Layout.margins: app.margins
text: actionsRepeater.count == 0 ? qsTr("Add an action...") : qsTr("Add another action...")
onClicked: root.addAction();
visible: root.actionsVisible
}
ThinDivider { visible: root.exitActionsVisible }
Label {
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: RuleActionDelegate {
Layout.fillWidth: true
ruleAction: root.rule.exitActions.get(index)
onClicked: root.rule.exitActions.removeRuleAction(index)
}
}
Button {
Layout.fillWidth: true
Layout.margins: app.margins
text: actionsRepeater.count == 0 ? qsTr("Add an action...") : qsTr("Add another action...")
onClicked: root.addExitAction();
visible: root.exitActionsVisible
}
}
}
Rectangle {
id: busyOverlay
anchors.fill: parent
color: "#55000000"
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()
}
}
}
}
}