1// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4import QtQuick 2.15
5import QtQuick.Controls 2.15 as QQC2
6import QtQuick.Layouts 1.15
7import QtQuick.Dialogs 1.0
8import QtLocation 5.15
9import Qt.labs.qmlmodels 1.0
10import org.kde.kitemmodels 1.0
11import org.kde.kirigami 2.15 as Kirigami
12import org.kde.kalendar 1.0
13import "labelutils.js" as LabelUtils
14
15Kirigami.ScrollablePage {
16    id: root
17
18    signal added(IncidenceWrapper incidenceWrapper)
19    signal edited(IncidenceWrapper incidenceWrapper)
20    signal cancel
21
22    // Setting the incidenceWrapper here and now causes some *really* weird behaviour.
23    // Set it after this component has already been instantiated.
24    property var incidenceWrapper
25
26    property bool editMode: false
27    property bool validDates: {
28        if(incidenceWrapper && incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo) {
29            return editorLoader.active && editorLoader.item.validEndDate
30        } else if (incidenceWrapper) {
31            return editorLoader.active && editorLoader.item.validFormDates &&
32                (incidenceWrapper.allDay || incidenceWrapper.incidenceStart <= incidenceWrapper.incidenceEnd)
33        } else {
34            return false;
35        }
36    }
37
38    title: if (incidenceWrapper) {
39        editMode ? i18nc("%1 is incidence type", "Edit %1", incidenceWrapper.incidenceTypeStr) :
40            i18nc("%1 is incidence type", "Add %1", incidenceWrapper.incidenceTypeStr);
41    } else {
42        "";
43    }
44
45    footer: QQC2.DialogButtonBox {
46        standardButtons: QQC2.DialogButtonBox.Cancel
47
48        QQC2.Button {
49            icon.name: editMode ? "document-save" : "list-add"
50            text: editMode ? i18n("Save") : i18n("Add")
51            enabled: root.validDates && incidenceWrapper.summary && incidenceWrapper.collectionId
52            QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
53        }
54
55        onRejected: cancel()
56        onAccepted: submitAction.trigger()
57    }
58
59    QQC2.Action {
60        id: submitAction
61        enabled: root.validDates && incidenceWrapper.summary && incidenceWrapper.collectionId
62        shortcut: "Return"
63        onTriggered: {
64            if (editMode) {
65                edited(incidenceWrapper);
66            } else if (root.validDates) {
67                added(incidenceWrapper);
68                if(root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo) {
69                    Config.lastUsedTodoCollection = root.incidenceWrapper.collectionId;
70                } else {
71                    Config.lastUsedEventCollection = root.incidenceWrapper.collectionId;
72                }
73                Config.save();
74            }
75            cancel(); // Easy way to close the editor
76        }
77    }
78
79    Component {
80        id: contactsPage
81        ContactsPage {
82            attendeeAkonadiIds: root.incidenceWrapper.attendeesModel.attendeesAkonadiIds
83
84            onAddAttendee: {
85                root.incidenceWrapper.attendeesModel.addAttendee(itemId, email);
86                root.flickable.contentY = editorLoader.item.attendeesColumnY;
87            }
88            onRemoveAttendee: {
89                root.incidenceWrapper.attendeesModel.deleteAttendeeFromAkonadiId(itemId)
90                root.flickable.contentY = editorLoader.item.attendeesColumnY;
91            }
92        }
93    }
94
95    Loader {
96        id: editorLoader
97        Layout.fillWidth: true
98        Layout.fillHeight: true
99
100        active: incidenceWrapper !== undefined
101        sourceComponent: ColumnLayout {
102
103            Layout.fillWidth: true
104            Layout.fillHeight: true
105
106            property bool validStartDate: incidenceForm.isTodo ?
107                incidenceStartDateCombo.validDate || !incidenceStartCheckBox.checked :
108                incidenceStartDateCombo.validDate
109            property bool validEndDate: incidenceForm.isTodo ?
110                incidenceEndDateCombo.validDate || !incidenceEndCheckBox.checked :
111                incidenceEndDateCombo.validDate
112            property bool validFormDates: validStartDate && (validEndDate || incidenceWrapper.allDay)
113
114            property alias attendeesColumnY: attendeesColumn.y
115
116            Kirigami.InlineMessage {
117                id: invalidDateMessage
118
119                Layout.fillWidth: true
120                visible: !root.validDates
121                type: Kirigami.MessageType.Error
122                // Specify what the problem is to aid user
123                text: root.incidenceWrapper.incidenceStart < root.incidenceWrapper.incidenceEnd ?
124                      i18n("Invalid dates provided.") : i18n("End date cannot be before start date.")
125            }
126
127            Kirigami.FormLayout {
128                id: incidenceForm
129
130                property date todayDate: new Date()
131                property bool isTodo: root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo
132                property bool isJournal: root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeJournal
133
134                QQC2.ComboBox {
135                    id: calendarCombo
136
137                    Kirigami.FormData.label: i18n("Calendar:")
138                    Layout.fillWidth: true
139
140                    // Not using a property from the incidenceWrapper object makes currentIndex send old incidenceWrapper to function
141                    property int collectionId: root.incidenceWrapper.collectionId
142
143                    textRole: "display"
144                    valueRole: "collectionId"
145                    currentIndex: model && collectionId !== -1 ? CalendarManager.getCalendarSelectableIndex(root.incidenceWrapper) : -1
146
147                    model: KDescendantsProxyModel {
148                        displayAncestorData: true
149                        model: {
150                            if(root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeEvent) {
151                                return CalendarManager.selectableEventCalendars;
152                            } else if (root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo) {
153                                return CalendarManager.selectableTodoCalendars;
154                            }
155                        }
156                    }
157                    delegate: DelegateChooser {
158                        role: 'kDescendantExpandable'
159
160                        DelegateChoice {
161                            roleValue: true
162                            Item {}
163                        }
164
165                        DelegateChoice {
166                            roleValue: false
167
168                            Kirigami.BasicListItem {
169                                label: display
170                                icon: decoration
171                                onClicked: root.incidenceWrapper.collectionId = collectionId
172                            }
173                        }
174                    }
175
176                    popup.z: 1000
177                }
178                QQC2.TextField {
179                    id: summaryField
180
181                    Kirigami.FormData.label: i18n("Summary:")
182                    placeholderText: i18n(`Add a title for your ${incidenceWrapper.incidenceTypeStr.toLowerCase()}`)
183                    text: root.incidenceWrapper.summary
184                    onTextChanged: root.incidenceWrapper.summary = text
185                }
186
187                Kirigami.Separator {
188                    Kirigami.FormData.isSection: true
189                }
190
191                RowLayout {
192                    Kirigami.FormData.label: i18n("Completion:")
193                    Layout.fillWidth: true
194                    visible: incidenceForm.isTodo && root.editMode
195
196                    QQC2.Slider {
197                        Layout.fillWidth: true
198                        orientation: Qt.Horizontal
199                        from: 0
200                        to: 100.0
201                        stepSize: 10.0
202                        value: root.incidenceWrapper.todoPercentComplete
203                        onValueChanged: root.incidenceWrapper.todoPercentComplete = value
204                    }
205                    QQC2.Label {
206                        text: String(root.incidenceWrapper.todoPercentComplete) + "\%"
207                    }
208                }
209
210                QQC2.ComboBox {
211                    Kirigami.FormData.label: i18n("Priority:")
212
213                    Layout.fillWidth: true
214                    currentIndex: root.incidenceWrapper.priority
215                    onCurrentValueChanged: root.incidenceWrapper.priority = currentValue
216                    textRole: "display"
217                    valueRole: "value"
218                    model: [
219                        {display: i18n("Unassigned"), value: 0},
220                        {display: i18n("1 (Highest Priority)"), value: 1},
221                        {display: i18n("2"), value: 2},
222                        {display: i18n("3"), value: 3},
223                        {display: i18n("4"), value: 4},
224                        {display: i18n("5 (Medium Priority)"), value: 5},
225                        {display: i18n("6"), value: 6},
226                        {display: i18n("7"), value: 7},
227                        {display: i18n("8"), value: 8},
228                        {display: i18n("9 (Lowest Priority)"), value: 9}
229                    ]
230                    visible: incidenceForm.isTodo
231                }
232
233                Kirigami.Separator {
234                    Kirigami.FormData.isSection: true
235                    visible: incidenceForm.isTodo
236                }
237
238                QQC2.CheckBox {
239                    id: allDayCheckBox
240
241                    text: i18n("All day")
242                    enabled: !incidenceForm.isTodo || !isNaN(root.incidenceWrapper.incidenceStart.getTime()) || !isNaN(root.incidenceWrapper.incidenceEnd.getTime())
243                    onEnabledChanged: if (!enabled) root.incidenceWrapper.allDay = false
244                    checked: root.incidenceWrapper.allDay
245                    onClicked: root.incidenceWrapper.allDay = checked
246                }
247
248                Connections {
249                    target: root.incidenceWrapper
250                    function onIncidenceStartChanged() {
251                        incidenceStartDateCombo.dateTime = root.incidenceWrapper.incidenceStart;
252                        incidenceStartTimeCombo.dateTime = root.incidenceWrapper.incidenceStart;
253                        incidenceStartDateCombo.display = root.incidenceWrapper.incidenceStartDateDisplay;
254                        incidenceStartTimeCombo.display = root.incidenceWrapper.incidenceStartTimeDisplay;
255                    }
256
257                    function onIncidenceEndChanged() {
258                        incidenceEndDateCombo.dateTime = root.incidenceWrapper.incidenceEnd;
259                        incidenceEndTimeCombo.dateTime = root.incidenceWrapper.incidenceEnd;
260                        incidenceEndDateCombo.display = root.incidenceWrapper.incidenceEndDateDisplay;
261                        incidenceEndTimeCombo.display = root.incidenceWrapper.incidenceEndTimeDisplay;
262                    }
263                }
264
265                RowLayout {
266                    id: incidenceStartLayout
267
268                    Kirigami.FormData.label: i18n("Start:")
269                    Layout.fillWidth: true
270                    visible: !incidenceForm.isTodo || (incidenceForm.isTodo && !isNaN(root.incidenceWrapper.incidenceStart.getTime()))
271
272                    QQC2.CheckBox {
273                        id: incidenceStartCheckBox
274
275                        property var oldDate
276
277                        checked: !isNaN(root.incidenceWrapper.incidenceStart.getTime())
278                        onClicked: {
279                            if (!checked && incidenceForm.isTodo) {
280                                oldDate = root.incidenceWrapper.incidenceStart
281                                root.incidenceWrapper.incidenceStart = new Date(undefined)
282                            } else if(incidenceForm.isTodo && oldDate) {
283                                root.incidenceWrapper.incidenceStart = oldDate
284                            } else if(incidenceForm.isTodo) {
285                                root.incidenceWrapper.incidenceEnd = new Date()
286                            }
287                        }
288                        visible: incidenceForm.isTodo
289                    }
290
291
292                    DateCombo {
293                        id: incidenceStartDateCombo
294
295                        Layout.fillWidth: true
296                        display: root.incidenceWrapper.incidenceStartDateDisplay
297                        dateTime: root.incidenceWrapper.incidenceStart
298                        onNewDateChosen: root.incidenceWrapper.setIncidenceStartDate(day, month, year)
299                    }
300                    TimeCombo {
301                        id: incidenceStartTimeCombo
302
303                        Layout.fillWidth: true
304                        timeZoneOffset: root.incidenceWrapper.startTimeZoneUTCOffsetMins
305                        display: root.incidenceWrapper.incidenceEndTimeDisplay
306                        dateTime: root.incidenceWrapper.incidenceStart
307                        onNewTimeChosen: root.incidenceWrapper.setIncidenceStartTime(hours, minutes)
308                        enabled: !allDayCheckBox.checked && (!incidenceForm.isTodo || incidenceStartCheckBox.checked)
309                        visible: !allDayCheckBox.checked
310                    }
311                }
312                RowLayout {
313                    id: incidenceEndLayout
314
315                    Kirigami.FormData.label: incidenceForm.isTodo ? i18n("Due:") : i18n("End:")
316                    Layout.fillWidth: true
317                    visible: !incidenceForm.isJournal || incidenceForm.isTodo
318
319                    QQC2.CheckBox {
320                        id: incidenceEndCheckBox
321
322                        property var oldDate
323
324                        checked: !isNaN(root.incidenceWrapper.incidenceEnd.getTime())
325                        onClicked: { // If we use onCheckedChanged this will change the date during init
326                            if(!checked && incidenceForm.isTodo) {
327                                oldDate = root.incidenceWrapper.incidenceEnd
328                                root.incidenceWrapper.incidenceEnd = new Date(undefined)
329                            } else if(incidenceForm.isTodo && oldDate) {
330                                root.incidenceWrapper.incidenceEnd = oldDate
331                            } else if(incidenceForm.isTodo) {
332                                let start = new Date();
333                                let startInMsecsSinceEpoch = start.getTime();
334                                const quarterHourInMsecs = 15 * 60 * 1000;
335                                const nearestQuarterHourStart = startInMsecsSinceEpoch + (quarterHourInMsecs - startInMsecsSinceEpoch % quarterHourInMsecs);
336                                root.incidenceWrapper.incidenceEnd = new Date(nearestQuarterHourStart);
337                            }
338                        }
339                        visible: incidenceForm.isTodo
340                    }
341
342                    DateCombo {
343                        id: incidenceEndDateCombo
344
345                        Layout.fillWidth: true
346                        display: root.incidenceWrapper.incidenceEndDateDisplay
347                        dateTime: root.incidenceWrapper.incidenceEnd
348                        onNewDateChosen: root.incidenceWrapper.setIncidenceEndDate(day, month, year)
349                        enabled: !incidenceForm.isTodo || (incidenceForm.isTodo && incidenceEndCheckBox.checked)
350                    }
351                    TimeCombo {
352                        id: incidenceEndTimeCombo
353
354                        Layout.fillWidth: true
355                        timeZoneOffset: root.incidenceWrapper.endTimeZoneUTCOffsetMins
356                        display: root.incidenceWrapper.incidenceEndTimeDisplay
357                        dateTime: root.incidenceWrapper.incidenceEnd
358                        onNewTimeChosen: root.incidenceWrapper.setIncidenceEndTime(hours, minutes)
359                        enabled: (!incidenceForm.isTodo && !allDayCheckBox.checked) || (incidenceForm.isTodo && incidenceEndCheckBox.checked)
360                        visible: !allDayCheckBox.checked
361                    }
362                }
363
364                QQC2.ComboBox {
365                    id: timeZoneComboBox
366                    Kirigami.FormData.label: i18n("Timezone:")
367                    Layout.fillWidth: true
368
369                    model: TimeZoneListModel {
370                        id: timeZonesModel
371                    }
372
373                    textRole: "display"
374                    valueRole: "id"
375                    currentIndex: model ? timeZonesModel.getTimeZoneRow(root.incidenceWrapper.timeZone) : -1
376                    delegate: Kirigami.BasicListItem {
377                        label: model.display
378                        onClicked: root.incidenceWrapper.timeZone = model.id
379                    }
380                    enabled: !incidenceForm.isTodo || (incidenceForm.isTodo && incidenceEndCheckBox.checked)
381                }
382
383                QQC2.ComboBox {
384                    id: repeatComboBox
385                    Kirigami.FormData.label: i18n("Repeat:")
386                    Layout.fillWidth: true
387
388                    enabled: !incidenceForm.isTodo || !isNaN(root.incidenceWrapper.incidenceStart.getTime()) || !isNaN(root.incidenceWrapper.incidenceEnd.getTime())
389                    textRole: "display"
390                    valueRole: "interval"
391                    onCurrentIndexChanged: if(currentIndex === 0) { root.incidenceWrapper.clearRecurrences() }
392                    currentIndex: {
393                        switch(root.incidenceWrapper.recurrenceData.type) {
394                            case 0:
395                                return root.incidenceWrapper.recurrenceData.type;
396                            case 3: // Daily
397                                return root.incidenceWrapper.recurrenceData.frequency === 1 ?
398                                    root.incidenceWrapper.recurrenceData.type - 2 : 5
399                            case 4: // Weekly
400                                return root.incidenceWrapper.recurrenceData.frequency === 1 ?
401                                    (root.incidenceWrapper.recurrenceData.weekdays.filter(x => x === true).length === 0 ?
402                                    root.incidenceWrapper.recurrenceData.type - 2 : 5) : 5
403                            case 5: // Monthly on position (e.g. third Monday)
404                            case 8: // Yearly on day
405                            case 9: // Yearly on position
406                            case 10: // Other
407                                return 5;
408                            case 6: // Monthly on day (1st of month)
409                                return 3;
410                            case 7: // Yearly on month
411                                return 4;
412                        }
413                    }
414                    model: [
415                        {key: "never", display: i18n("Never"), interval: -1},
416                        {key: "daily", display: i18n("Daily"), interval: IncidenceWrapper.Daily},
417                        {key: "weekly", display: i18n("Weekly"), interval: IncidenceWrapper.Weekly},
418                        {key: "monthly", display: i18n("Monthly"), interval: IncidenceWrapper.Monthly},
419                        {key: "yearly", display: i18n("Yearly"), interval: IncidenceWrapper.Yearly},
420                        {key: "custom", display: i18n("Custom"), interval: -1}
421                    ]
422                    delegate: Kirigami.BasicListItem {
423                        text: modelData.display
424                        onClicked: if (modelData.interval >= 0) {
425                            root.incidenceWrapper.setRegularRecurrence(modelData.interval)
426                        } else {
427                            root.incidenceWrapper.clearRecurrences();
428                        }
429                    }
430                    popup.z: 1000
431                }
432
433                Kirigami.FormLayout {
434                    id: customRecurrenceLayout
435
436                    Layout.fillWidth: true
437                    Layout.leftMargin: Kirigami.Units.largeSpacing
438                    visible: repeatComboBox.currentIndex > 0 // Not "Never" index
439
440                    function setOccurrence() {
441                        root.incidenceWrapper.setRegularRecurrence(recurScaleRuleCombobox.currentValue, recurFreqRuleSpinbox.value);
442
443                        if(recurScaleRuleCombobox.currentValue === IncidenceWrapper.Weekly) {
444                            weekdayCheckboxRepeater.setWeekdaysRepeat();
445                        }
446                    }
447
448                    // Custom controls
449                    RowLayout {
450                        Layout.fillWidth: true
451                        Kirigami.FormData.label: i18n("Every:")
452                        visible: repeatComboBox.currentIndex === 5
453
454                        QQC2.SpinBox {
455                            id: recurFreqRuleSpinbox
456
457                            Layout.fillWidth: true
458                            from: 1
459                            value: root.incidenceWrapper.recurrenceData.frequency
460                            onValueChanged: if(visible) { root.incidenceWrapper.setRecurrenceDataItem("frequency", value) }
461                        }
462                        QQC2.ComboBox {
463                            id: recurScaleRuleCombobox
464
465                            Layout.fillWidth: true
466                            visible: repeatComboBox.currentIndex === 5
467                            // Make sure it defaults to something
468                            onVisibleChanged: if(visible && currentIndex < 0) { currentIndex = 0; customRecurrenceLayout.setOccurrence(); }
469
470                            textRole: "display"
471                            valueRole: "interval"
472                            onCurrentValueChanged: if(visible) {
473                                customRecurrenceLayout.setOccurrence();
474                                repeatComboBox.currentIndex = 5; // Otherwise resets to default daily/weekly/etc.
475                            }
476                            currentIndex: {
477                                if(root.incidenceWrapper.recurrenceData.type === undefined) {
478                                    return -1;
479                                }
480
481                                switch(root.incidenceWrapper.recurrenceData.type) {
482                                    case 3: // Daily
483                                    case 4: // Weekly
484                                        return root.incidenceWrapper.recurrenceData.type - 3
485                                    case 5: // Monthly on position (e.g. third Monday)
486                                    case 6: // Monthly on day (1st of month)
487                                        return 2;
488                                    case 7: // Yearly on month
489                                    case 8: // Yearly on day
490                                    case 9: // Yearly on position
491                                        return 3;
492                                    default:
493                                        return -1;
494                                }
495                            }
496
497                            model: [
498                                {key: "day", display: i18np("day", "days", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Daily},
499                                {key: "week", display: i18np("week", "weeks", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Weekly},
500                                {key: "month", display: i18np("month", "months", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Monthly},
501                                {key: "year", display: i18np("year", "years", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Yearly},
502                            ]
503                            delegate: Kirigami.BasicListItem {
504                                text: modelData.display
505                                onClicked: {
506                                    customRecurrenceLayout.setOccurrence();
507                                    repeatComboBox.currentIndex = 5; // Otherwise resets to default daily/weekly/etc.
508                                }
509                            }
510
511                            popup.z: 1000
512                        }
513                    }
514
515                    // Custom controls specific to weekly
516                    GridLayout {
517                        id: recurWeekdayRuleLayout
518                        Layout.fillWidth: true
519
520                        columns: 7
521                        visible: recurScaleRuleCombobox.currentIndex === 1 && repeatComboBox.currentIndex === 5 // "week"/"weeks" index
522
523                        Repeater {
524                            model: 7
525                            delegate: QQC2.Label {
526                                Layout.fillWidth: true
527                                horizontalAlignment: Text.AlignHCenter
528                                text: Qt.locale().dayName(Qt.locale().firstDayOfWeek + index, Locale.ShortFormat)
529                            }
530                        }
531
532                        Repeater {
533                            id: weekdayCheckboxRepeater
534
535                            property var checkboxes: []
536                            function setWeekdaysRepeat() {
537                                let selectedDays = new Array(7)
538                                for(let checkbox of checkboxes) {
539                                    // C++ func takes 7 bit array
540                                    selectedDays[checkbox.dayNumber] = checkbox.checked
541                                }
542                                root.incidenceWrapper.setRecurrenceDataItem("weekdays", selectedDays);
543                            }
544
545                            model: 7
546                            delegate: QQC2.CheckBox {
547                                Layout.alignment: Qt.AlignHCenter
548                                // We make sure we get dayNumber per the day of the week number used by C++ Qt
549                                property int dayNumber: Qt.locale().firstDayOfWeek + index > 7 ?
550                                                        Qt.locale().firstDayOfWeek + index - 1 - 7 :
551                                                        Qt.locale().firstDayOfWeek + index - 1
552
553                                checked: if(root.incidenceWrapper.recurrenceData) root.incidenceWrapper.recurrenceData.weekdays[dayNumber]
554                                onClicked: {
555                                    let newWeekdays = [...root.incidenceWrapper.recurrenceData.weekdays];
556                                    newWeekdays[dayNumber] = !root.incidenceWrapper.recurrenceData.weekdays[dayNumber];
557                                    root.incidenceWrapper.setRecurrenceDataItem("weekdays", newWeekdays);
558                                }
559                            }
560                        }
561                    }
562
563                    // Controls specific to monthly recurrence
564                    QQC2.ButtonGroup {
565                        buttons: monthlyRecurRadioColumn.children
566                    }
567
568                    ColumnLayout {
569                        id: monthlyRecurRadioColumn
570
571                        Kirigami.FormData.label: i18n("On:")
572
573                        Layout.fillWidth: true
574                        visible: recurScaleRuleCombobox.currentIndex === 2 && repeatComboBox.currentIndex === 5 // "month/months" index
575
576                        QQC2.RadioButton {
577                            property int dateOfMonth: incidenceStartDateCombo.dateFromText.getDate()
578
579                            text: i18nc("%1 is the day number of month", "The %1 of each month", LabelUtils.numberToString(dateOfMonth))
580
581                            checked: root.incidenceWrapper.recurrenceData.type === 6 // Monthly on day (1st of month)
582                            onClicked: customRecurrenceLayout.setOccurrence()
583                        }
584                        QQC2.RadioButton {
585                            property int dayOfWeek: incidenceStartDateCombo.dateFromText.getDay() > 0 ?
586                                                    incidenceStartDateCombo.dateFromText.getDay() - 1 :
587                                                    7 // C++ Qt day of week index goes Mon-Sun, 0-7
588                            property int weekOfMonth: Math.ceil((incidenceStartDateCombo.dateFromText.getDate() + 6 - incidenceStartDateCombo.dateFromText.getDay())/7);
589                            property string dayOfWeekString: Qt.locale().dayName(incidenceStartDateCombo.dateFromText.getDay())
590
591                            text: i18nc("the weekOfMonth dayOfWeekString of each month", "The %1 %2 of each month", LabelUtils.numberToString(weekOfMonth), dayOfWeekString)
592                            checked: root.incidenceWrapper.recurrenceData.type === 5 // Monthly on position
593                            onTextChanged: if(checked) { root.incidenceWrapper.setMonthlyPosRecurrence(weekOfMonth, dayOfWeek); }
594                            onClicked: root.incidenceWrapper.setMonthlyPosRecurrence(weekOfMonth, dayOfWeek)
595                        }
596                    }
597
598
599                    // Repeat end controls (visible on all recurrences)
600                    RowLayout {
601                        Layout.fillWidth: true
602                        Kirigami.FormData.label: i18n("Ends:")
603
604                        QQC2.ComboBox {
605                            id: endRecurType
606
607                            Layout.fillWidth: true
608                            // Recurrence duration returns -1 for never ending and 0 when the recurrence
609                            // end date is set. Any number larger is the set number of recurrences
610                            currentIndex: root.incidenceWrapper.recurrenceData.duration <= 0 ?
611                                root.incidenceWrapper.recurrenceData.duration + 1 : 2
612
613                            textRole: "display"
614                            valueRole: "duration"
615                            model: [
616                                {display: i18n("Never"), duration: -1},
617                                {display: i18n("On"), duration: 0},
618                                {display: i18n("After"), duration: 1}
619                            ]
620                            delegate: Kirigami.BasicListItem {
621                                text: modelData.display
622                                onClicked: root.incidenceWrapper.setRecurrenceDataItem("duration", modelData.duration)
623                            }
624                            popup.z: 1000
625                        }
626                        DateCombo {
627                            id: recurEndDateCombo
628
629                            Layout.fillWidth: true
630                            visible: endRecurType.currentIndex === 1
631                            onVisibleChanged: if (visible && isNaN(root.incidenceWrapper.recurrenceData.endDateTime.getTime())) {
632                                root.incidenceWrapper.setRecurrenceDataItem("endDateTime", new Date());
633                            }
634
635                            display: root.incidenceWrapper.recurrenceData.endDateTimeDisplay
636                            dateTime: root.incidenceWrapper.recurrenceData.endDateTime
637                            onNewDateChosen: root.incidenceWrapper.setRecurrenceDataItem("endDateTime", new Date(year, month, day));
638                        }
639
640                        RowLayout {
641                            Layout.fillWidth: true
642                            visible: endRecurType.currentIndex === 2
643                            onVisibleChanged: if (visible) { root.incidenceWrapper.setRecurrenceOccurrences(recurOccurrenceEndSpinbox.value) }
644
645                            QQC2.SpinBox {
646                                id: recurOccurrenceEndSpinbox
647
648                                Layout.fillWidth: true
649                                from: 1
650                                value: root.incidenceWrapper.recurrenceData.duration
651                                onValueChanged: if (visible) { root.incidenceWrapper.setRecurrenceOccurrences(value) }
652                            }
653                            QQC2.Label {
654                                text: i18np("occurrence", "occurrences", recurOccurrenceEndSpinbox.value)
655                            }
656                        }
657                    }
658
659                    ColumnLayout {
660                        Kirigami.FormData.label: i18n("Exceptions:")
661                        Layout.fillWidth: true
662
663                        QQC2.ComboBox {
664                            id: exceptionAddButton
665                            Layout.fillWidth: true
666                            displayText: i18n("Add Recurrence Exception")
667
668                            popup: QQC2.Popup {
669                                id: recurExceptionPopup
670
671                                width: Kirigami.Units.gridUnit * 18
672                                height: Kirigami.Units.gridUnit * 18
673                                y: parent.y + parent.height
674                                z: 1000
675
676                                DatePicker {
677                                    id: recurExceptionPicker
678                                    anchors.fill: parent
679                                    onDatePicked: {
680                                        root.incidenceWrapper.recurrenceExceptionsModel.addExceptionDateTime(pickedDate)
681                                        recurExceptionPopup.close()
682                                    }
683                                }
684                            }
685                        }
686
687                        Repeater {
688                            id: exceptionsRepeater
689                            model: root.incidenceWrapper.recurrenceExceptionsModel
690                            delegate: RowLayout {
691                                Kirigami.BasicListItem {
692                                    Layout.fillWidth: true
693                                    text: date.toLocaleDateString(Qt.locale())
694                                }
695                                QQC2.Button {
696                                    icon.name: "edit-delete-remove"
697                                    onClicked: root.incidenceWrapper.recurrenceExceptionsModel.deleteExceptionDateTime(date)
698                                }
699                            }
700                        }
701                    }
702                }
703
704                Kirigami.Separator {
705                    Kirigami.FormData.isSection: true
706                }
707
708                RowLayout {
709                    Kirigami.FormData.label: i18n("Location:")
710                    Layout.fillWidth: true
711
712                    QQC2.TextField {
713                        id: locationField
714
715                        property bool typed: false
716
717                        Layout.fillWidth: true
718                        placeholderText: i18n("Optional")
719                        text: root.incidenceWrapper.location
720                        onTextChanged: root.incidenceWrapper.location = text
721                        Keys.onPressed: locationsMenu.open()
722
723                        QQC2.BusyIndicator {
724                            height: parent.height
725                            anchors.right: parent.right
726                            running: locationsModel.status === GeocodeModel.Loading
727                            visible: locationsModel.status === GeocodeModel.Loading
728                        }
729
730                        QQC2.Menu {
731                            id: locationsMenu
732                            width: parent.width
733                            y: parent.height // Y is relative to parent
734                            focus: false
735
736                            Repeater {
737                                model: GeocodeModel {
738                                    id: locationsModel
739                                    plugin: locationPlugin
740                                    query: root.incidenceWrapper.location
741                                    autoUpdate: true
742                                }
743                                delegate: QQC2.MenuItem {
744                                    text: locationData.address.text
745                                    onClicked: root.incidenceWrapper.location = locationData.address.text
746                                }
747                            }
748
749                            Plugin {
750                                id: locationPlugin
751                                name: "osm"
752                            }
753                        }
754                    }
755                    QQC2.CheckBox {
756                        id: mapVisibleCheckBox
757                        text: i18n("Show map")
758                        visible: Config.enableMaps
759                    }
760                }
761
762                ColumnLayout {
763                    id: mapLayout
764                    Layout.fillWidth: true
765                    visible: Config.enableMaps && mapVisibleCheckBox.checked
766
767                    Loader {
768                        id: mapLoader
769
770                        Layout.fillWidth: true
771                        height: Kirigami.Units.gridUnit * 16
772                        asynchronous: true
773                        active: visible
774
775                        sourceComponent: LocationMap {
776                            id: map
777                            selectMode: true
778                            query: root.incidenceWrapper.location
779                            onSelectedLocationAddress: root.incidenceWrapper.location = address
780                        }
781                    }
782                }
783
784                // Restrain the descriptionTextArea from getting too chonky
785                ColumnLayout {
786                    Layout.fillWidth: true
787                    Layout.maximumWidth: incidenceForm.wideMode ? Kirigami.Units.gridUnit * 25 : -1
788                    Kirigami.FormData.label: i18n("Description:")
789
790                    QQC2.TextArea {
791                        id: descriptionTextArea
792
793                        Layout.fillWidth: true
794                        placeholderText: i18n("Optional")
795                        text: root.incidenceWrapper.description
796                        wrapMode: Text.Wrap
797                        onTextChanged: root.incidenceWrapper.description = text
798                        Keys.onReturnPressed: {
799                            if (event.modifiers & Qt.ShiftModifier) {
800                                submitAction.trigger();
801                            } else {
802                                event.accepted = false;
803                            }
804                        }
805                    }
806                }
807
808                RowLayout {
809                    Kirigami.FormData.label: i18n("Tags:")
810                    Layout.fillWidth: true
811
812                    QQC2.ComboBox {
813                        Layout.fillWidth: true
814
815                        enabled: count > 0
816                        model: TagManager.tagModel
817                        displayText: root.incidenceWrapper.categories.length > 0 ?
818                            root.incidenceWrapper.categories.join(i18nc("List separator", ", ")) :
819                            Kirigami.Settings.tabletMode ? i18n("Tap to set tags…") : i18n("Click to set tags…")
820
821                        delegate: Kirigami.CheckableListItem {
822                            label: model.display
823                            reserveSpaceForIcon: false
824                            checked: root.incidenceWrapper.categories.includes(model.display)
825                            action: QQC2.Action {
826                                onTriggered: {
827                                    checked = !checked;
828                                    root.incidenceWrapper.categories.includes(model.display) ?
829                                        root.incidenceWrapper.categories = root.incidenceWrapper.categories.filter(tag => tag !== model.display) :
830                                        root.incidenceWrapper.categories = [...root.incidenceWrapper.categories, model.display]
831                                }
832                            }
833                        }
834                    }
835                    QQC2.Button {
836                        text: i18n("Manage tags…")
837                        onClicked: KalendarApplication.action("open_tag_manager").trigger()
838                    }
839                }
840
841                Kirigami.Separator {
842                    Kirigami.FormData.isSection: true
843                }
844                ColumnLayout {
845                    id: remindersColumn
846
847                    Kirigami.FormData.label: i18n("Reminders:")
848                    Kirigami.FormData.labelAlignment: remindersRepeater.count ? Qt.AlignTop : Qt.AlignVCenter
849                    Layout.fillWidth: true
850
851                    Repeater {
852                        id: remindersRepeater
853
854                        Layout.fillWidth: true
855
856                        model: root.incidenceWrapper.remindersModel
857                        // All of the alarms are handled within the delegates.
858
859                        delegate: RowLayout {
860                            Layout.fillWidth: true
861
862                            QQC2.ComboBox {
863                                // There is also a chance here to add a feature for the user to pick reminder type.
864                                Layout.fillWidth: true
865
866                                property var selectedIndex: 0
867
868                                displayText: LabelUtils.secondsToReminderLabel(startOffset)
869                                //textRole: "DisplayNameRole"
870                                onCurrentValueChanged: root.incidenceWrapper.remindersModel.setData(root.incidenceWrapper.remindersModel.index(index, 0),
871                                                                                                            currentValue,
872                                                                                                            root.incidenceWrapper.remindersModel.dataroles.startOffset)
873                                onCountChanged: selectedIndex = currentIndex // Gets called *just* before modelChanged
874                                onModelChanged: currentIndex = selectedIndex
875
876                                model: [0, // We times by -1 to make times be before incidence
877                                        -1 * 5 * 60, // 5 minutes
878                                        -1 * 10 * 60,
879                                        -1 * 15 * 60,
880                                        -1 * 30 * 60,
881                                        -1 * 45 * 60,
882                                        -1 * 1 * 60 * 60, // 1 hour
883                                        -1 * 2 * 60 * 60,
884                                        -1 * 1 * 24 * 60 * 60, // 1 day
885                                        -1 * 2 * 24 * 60 * 60,
886                                        -1 * 5 * 24 * 60 * 60]
887                                        // All these times are in seconds.
888                                delegate: Kirigami.BasicListItem {
889                                    text: LabelUtils.secondsToReminderLabel(modelData)
890                                }
891
892                                popup.z: 1000
893                            }
894
895                            QQC2.Button {
896                                icon.name: "edit-delete-remove"
897                                onClicked: root.incidenceWrapper.remindersModel.deleteAlarm(model.index);
898                            }
899                        }
900                    }
901
902                    QQC2.Button {
903                        id: remindersButton
904
905                        text: i18n("Add Reminder")
906                        Layout.fillWidth: true
907
908                        onClicked: root.incidenceWrapper.remindersModel.addAlarm();
909                    }
910                }
911
912                Kirigami.Separator {
913                    Kirigami.FormData.isSection: true
914                }
915
916                ColumnLayout {
917                    id: attendeesColumn
918
919                    Kirigami.FormData.label: i18n("Attendees:")
920                    Kirigami.FormData.labelAlignment: attendeesRepeater.count ? Qt.AlignTop : Qt.AlignVCenter
921                    Layout.fillWidth: true
922
923                    Repeater {
924                        id: attendeesRepeater
925                        model: root.incidenceWrapper.attendeesModel
926                        // All of the alarms are handled within the delegates.
927                        Layout.fillWidth: true
928
929                        delegate: Kirigami.AbstractCard {
930
931                            topPadding: Kirigami.Units.smallSpacing
932                            bottomPadding: Kirigami.Units.smallSpacing
933
934                            contentItem: Item {
935                                implicitWidth: attendeeCardContent.implicitWidth
936                                implicitHeight: attendeeCardContent.implicitHeight
937
938                                GridLayout {
939                                    id: attendeeCardContent
940
941                                    anchors {
942                                        left: parent.left
943                                        top: parent.top
944                                        right: parent.right
945                                        //IMPORTANT: never put the bottom margin
946                                    }
947
948                                    columns: 6
949                                    rows: 4
950
951                                    QQC2.Label{
952                                        Layout.row: 0
953                                        Layout.column: 0
954                                        text: i18n("Name:")
955                                    }
956                                    QQC2.TextField {
957                                        Layout.fillWidth: true
958                                        Layout.row: 0
959                                        Layout.column: 1
960                                        Layout.columnSpan: 4
961                                        placeholderText: i18n("Optional")
962                                        text: model.name
963                                        onTextChanged: root.incidenceWrapper.attendeesModel.setData(root.incidenceWrapper.attendeesModel.index(index, 0),
964                                                                                                    text,
965                                                                                                    AttendeesModel.NameRole)
966                                    }
967
968                                    QQC2.Button {
969                                        Layout.alignment: Qt.AlignTop
970                                        Layout.column: 5
971                                        Layout.row: 0
972                                        icon.name: "edit-delete-remove"
973                                        onClicked: root.incidenceWrapper.attendeesModel.deleteAttendee(index);
974                                    }
975
976                                    QQC2.Label {
977                                        Layout.row: 1
978                                        Layout.column: 0
979                                        text: i18n("Email:")
980                                    }
981                                    QQC2.TextField {
982                                        Layout.fillWidth: true
983                                        Layout.row: 1
984                                        Layout.column: 1
985                                        Layout.columnSpan: 4
986                                        placeholderText: i18n("Required")
987                                        text: model.email
988                                        onTextChanged: root.incidenceWrapper.attendeesModel.setData(root.incidenceWrapper.attendeesModel.index(index, 0),
989                                                                                                    text,
990                                                                                                    AttendeesModel.EmailRole)
991                                    }
992                                    QQC2.Label {
993                                        Layout.row: 2
994                                        Layout.column: 0
995                                        text: i18n("Status:")
996                                        visible: root.editMode
997                                    }
998                                    QQC2.ComboBox {
999                                        Layout.fillWidth: true
1000                                        Layout.row: 2
1001                                        Layout.column: 1
1002                                        Layout.columnSpan: 2
1003                                        model: root.incidenceWrapper.attendeesModel.attendeeStatusModel
1004                                        textRole: "display"
1005                                        valueRole: "value"
1006                                        currentIndex: status // role of parent
1007                                        onCurrentValueChanged: root.incidenceWrapper.attendeesModel.setData(root.incidenceWrapper.attendeesModel.index(index, 0),
1008                                                                                                            currentValue,
1009                                                                                                            AttendeesModel.StatusRole)
1010
1011                                        popup.z: 1000
1012                                        visible: root.editMode
1013                                    }
1014                                    QQC2.CheckBox {
1015                                        Layout.fillWidth: true
1016                                        Layout.row: 2
1017                                        Layout.column: 3
1018                                        Layout.columnSpan: 2
1019                                        text: i18n("Request RSVP")
1020                                        checked: model.rsvp
1021                                        onCheckedChanged: root.incidenceWrapper.attendeesModel.setData(root.incidenceWrapper.attendeesModel.index(index, 0),
1022                                                                                                       checked,
1023                                                                                                       AttendeesModel.RSVPRole)
1024                                        visible: root.editMode
1025                                    }
1026                                }
1027                            }
1028                        }
1029                    }
1030
1031                    QQC2.Button {
1032                        id: attendeesButton
1033                        text: i18n("Add Attendee")
1034                        Layout.fillWidth: true
1035
1036                        onClicked: attendeeAddChoices.open()
1037
1038                        QQC2.Menu {
1039                            id: attendeeAddChoices
1040                            width: attendeesButton.width
1041                            y: parent.height // Y is relative to parent
1042
1043                            QQC2.MenuItem {
1044                                text: i18n("Choose from Contacts")
1045                                onClicked: pageStack.push(contactsPage)
1046                            }
1047                            QQC2.MenuItem {
1048                                text: i18n("Fill in Manually")
1049                                onClicked: root.incidenceWrapper.attendeesModel.addAttendee();
1050                            }
1051                        }
1052                    }
1053                }
1054
1055                Kirigami.Separator {
1056                    Kirigami.FormData.isSection: true
1057                }
1058
1059                ColumnLayout {
1060                    id: attachmentsColumn
1061
1062                    Kirigami.FormData.label: i18n("Attachments:")
1063                    Kirigami.FormData.labelAlignment: attachmentsRepeater.count ? Qt.AlignTop : Qt.AlignVCenter
1064                    Layout.fillWidth: true
1065
1066                    Repeater {
1067                        id: attachmentsRepeater
1068                        model: root.incidenceWrapper.attachmentsModel
1069                        delegate: RowLayout {
1070                            Kirigami.BasicListItem {
1071                                Layout.fillWidth: true
1072                                icon: iconName // Why isn't this icon.name??
1073                                label: attachmentLabel
1074                                onClicked: Qt.openUrlExternally(uri)
1075                            }
1076                            QQC2.Button {
1077                                icon.name: "edit-delete-remove"
1078                                onClicked: root.incidenceWrapper.attachmentsModel.deleteAttachment(uri)
1079                            }
1080                        }
1081                    }
1082
1083                    QQC2.Button {
1084                        id: attachmentsButton
1085                        text: i18n("Add Attachment")
1086                        Layout.fillWidth: true
1087                        onClicked: attachmentFileDialog.open();
1088
1089                        FileDialog {
1090                            id: attachmentFileDialog
1091
1092                            title: "Add an attachment"
1093                            folder: shortcuts.home
1094                            onAccepted: root.incidenceWrapper.attachmentsModel.addAttachment(fileUrls)
1095                        }
1096                    }
1097                }
1098            }
1099        }
1100    }
1101}
1102