IncidenceEditor.qml 53.3 KB
Newer Older
1
2
3
4
5
6
// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-or-later

import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
7
import QtQuick.Dialogs 1.0
8
import QtLocation 5.15
Claudio Cambra's avatar
Claudio Cambra committed
9
10
import Qt.labs.qmlmodels 1.0
import org.kde.kitemmodels 1.0
11
import org.kde.kirigami 2.15 as Kirigami
12
import org.kde.kalendar 1.0
13
import "labelutils.js" as LabelUtils
14

15
Kirigami.ScrollablePage {
16
    id: root
17

18
19
    signal added(IncidenceWrapper incidenceWrapper)
    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
27
    property bool editMode: false
    property bool validDates: {
Claudio Cambra's avatar
Claudio Cambra committed
28
29
30
31
32
        if(incidenceWrapper && incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo) {
            return editorLoader.active && editorLoader.item.validEndDate
        } else if (incidenceWrapper) {
            return editorLoader.active && editorLoader.item.validFormDates &&
                (incidenceWrapper.allDay || incidenceWrapper.incidenceStart <= incidenceWrapper.incidenceEnd)
33
        } else {
Claudio Cambra's avatar
Claudio Cambra committed
34
            return false;
35
36
        }
    }
37

38
39
40
41
42
43
    title: if (incidenceWrapper) {
        editMode ? i18nc("%1 is incidence type", "Edit %1", incidenceWrapper.incidenceTypeStr) :
            i18nc("%1 is incidence type", "Add %1", incidenceWrapper.incidenceTypeStr);
    } else {
        "";
    }
44
45

    footer: QQC2.DialogButtonBox {
46
        standardButtons: QQC2.DialogButtonBox.Cancel
Claudio Cambra's avatar
Claudio Cambra committed
47

48
        QQC2.Button {
49
50
            icon.name: editMode ? "document-save" : "list-add"
            text: editMode ? i18n("Save") : i18n("Add")
51
            enabled: root.validDates && incidenceWrapper.summary && incidenceWrapper.collectionId
52
53
            QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
        }
Claudio Cambra's avatar
Claudio Cambra committed
54

55
        onRejected: cancel()
56
57
58
59
60
        onAccepted: submitAction.trigger()
    }

    QQC2.Action {
        id: submitAction
61
        enabled: root.validDates && incidenceWrapper.summary && incidenceWrapper.collectionId
62
63
        shortcut: "Return"
        onTriggered: {
64
            if (editMode) {
65
                edited(incidenceWrapper);
66
            } else if (root.validDates) {
67
                added(incidenceWrapper);
68
69
70
71
72
73
                if(root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo) {
                    Config.lastUsedTodoCollection = root.incidenceWrapper.collectionId;
                } else {
                    Config.lastUsedEventCollection = root.incidenceWrapper.collectionId;
                }
                Config.save();
74
            }
75
            cancel(); // Easy way to close the editor
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
        }
    }

    Component {
        id: contactsPage
        ContactsPage {
            attendeeAkonadiIds: root.incidenceWrapper.attendeesModel.attendeesAkonadiIds

            onAddAttendee: {
                root.incidenceWrapper.attendeesModel.addAttendee(itemId, email);
                root.flickable.contentY = editorLoader.item.attendeesColumnY;
            }
            onRemoveAttendee: {
                root.incidenceWrapper.attendeesModel.deleteAttendeeFromAkonadiId(itemId)
                root.flickable.contentY = editorLoader.item.attendeesColumnY;
91
92
            }
        }
93
94
    }

Claudio Cambra's avatar
Claudio Cambra committed
95
96
    Loader {
        id: editorLoader
97
98
99
        Layout.fillWidth: true
        Layout.fillHeight: true

100
        active: incidenceWrapper !== undefined
101
        sourceComponent: ColumnLayout {
102

103
104
105
            Layout.fillWidth: true
            Layout.fillHeight: true

106
107
108
109
110
111
112
            property bool validStartDate: incidenceForm.isTodo ?
                incidenceStartDateCombo.validDate || !incidenceStartCheckBox.checked :
                incidenceStartDateCombo.validDate
            property bool validEndDate: incidenceForm.isTodo ?
                incidenceEndDateCombo.validDate || !incidenceEndCheckBox.checked :
                incidenceEndDateCombo.validDate
            property bool validFormDates: validStartDate && (validEndDate || incidenceWrapper.allDay)
113

114
115
            property alias attendeesColumnY: attendeesColumn.y

116
117
118
119
            Kirigami.InlineMessage {
                id: invalidDateMessage

                Layout.fillWidth: true
120
                visible: !root.validDates
121
122
                type: Kirigami.MessageType.Error
                // Specify what the problem is to aid user
123
                text: root.incidenceWrapper.incidenceStart < root.incidenceWrapper.incidenceEnd ?
124
125
                      i18n("Invalid dates provided.") : i18n("End date cannot be before start date.")
            }
126

Claudio Cambra's avatar
Claudio Cambra committed
127
            Kirigami.FormLayout {
128
                id: incidenceForm
129

Claudio Cambra's avatar
Claudio Cambra committed
130
                property date todayDate: new Date()
131
132
                property bool isTodo: root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo
                property bool isJournal: root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeJournal
133

Claudio Cambra's avatar
Claudio Cambra committed
134
135
                QQC2.ComboBox {
                    id: calendarCombo
136

Claudio Cambra's avatar
Claudio Cambra committed
137
138
                    Kirigami.FormData.label: i18n("Calendar:")
                    Layout.fillWidth: true
139

140
141
142
                    // Not using a property from the incidenceWrapper object makes currentIndex send old incidenceWrapper to function
                    property int collectionId: root.incidenceWrapper.collectionId

Claudio Cambra's avatar
Claudio Cambra committed
143
144
                    textRole: "display"
                    valueRole: "collectionId"
145
                    currentIndex: model && collectionId !== -1 ? CalendarManager.getCalendarSelectableIndex(root.incidenceWrapper) : -1
146

Claudio Cambra's avatar
Claudio Cambra committed
147
148
149
150
151
152
153
154
                    model: KDescendantsProxyModel {
                        displayAncestorData: true
                        model: {
                            if(root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeEvent) {
                                return CalendarManager.selectableEventCalendars;
                            } else if (root.incidenceWrapper.incidenceType === IncidenceWrapper.TypeTodo) {
                                return CalendarManager.selectableTodoCalendars;
                            }
155
156
                        }
                    }
Claudio Cambra's avatar
Claudio Cambra committed
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
                    delegate: DelegateChooser {
                        role: 'kDescendantExpandable'

                        DelegateChoice {
                            roleValue: true
                            Item {}
                        }

                        DelegateChoice {
                            roleValue: false

                            Kirigami.BasicListItem {
                                label: display
                                icon: decoration
                                onClicked: root.incidenceWrapper.collectionId = collectionId
                            }
                        }
Claudio Cambra's avatar
Claudio Cambra committed
174
                    }
Claudio Cambra's avatar
Claudio Cambra committed
175

Claudio Cambra's avatar
Claudio Cambra committed
176
                    popup.z: 1000
177
                }
Claudio Cambra's avatar
Claudio Cambra committed
178
                QQC2.TextField {
179
                    id: summaryField
180

181
182
                    Kirigami.FormData.label: i18n("Summary:")
                    placeholderText: i18n(`Add a title for your ${incidenceWrapper.incidenceTypeStr.toLowerCase()}`)
183
184
                    text: root.incidenceWrapper.summary
                    onTextChanged: root.incidenceWrapper.summary = text
Claudio Cambra's avatar
Claudio Cambra committed
185
                }
186

Claudio Cambra's avatar
Claudio Cambra committed
187
188
189
                Kirigami.Separator {
                    Kirigami.FormData.isSection: true
                }
190

191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
                RowLayout {
                    Kirigami.FormData.label: i18n("Completion:")
                    Layout.fillWidth: true
                    visible: incidenceForm.isTodo && root.editMode

                    QQC2.Slider {
                        Layout.fillWidth: true
                        orientation: Qt.Horizontal
                        from: 0
                        to: 100.0
                        stepSize: 10.0
                        value: root.incidenceWrapper.todoPercentComplete
                        onValueChanged: root.incidenceWrapper.todoPercentComplete = value
                    }
                    QQC2.Label {
                        text: String(root.incidenceWrapper.todoPercentComplete) + "\%"
                    }
                }

                QQC2.ComboBox {
                    Kirigami.FormData.label: i18n("Priority:")

                    Layout.fillWidth: true
                    currentIndex: root.incidenceWrapper.priority
                    onCurrentValueChanged: root.incidenceWrapper.priority = currentValue
                    textRole: "display"
                    valueRole: "value"
                    model: [
                        {display: i18n("Unassigned"), value: 0},
220
                        {display: i18n("1 (Highest Priority)"), value: 1},
221
222
223
                        {display: i18n("2"), value: 2},
                        {display: i18n("3"), value: 3},
                        {display: i18n("4"), value: 4},
224
                        {display: i18n("5 (Medium Priority)"), value: 5},
225
226
227
                        {display: i18n("6"), value: 6},
                        {display: i18n("7"), value: 7},
                        {display: i18n("8"), value: 8},
228
                        {display: i18n("9 (Lowest Priority)"), value: 9}
229
230
231
232
233
234
235
236
237
                    ]
                    visible: incidenceForm.isTodo
                }

                Kirigami.Separator {
                    Kirigami.FormData.isSection: true
                    visible: incidenceForm.isTodo
                }

Claudio Cambra's avatar
Claudio Cambra committed
238
239
                QQC2.CheckBox {
                    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
249
250

                Connections {
                    target: root.incidenceWrapper
                    function onIncidenceStartChanged() {
251
252
                        incidenceStartDateCombo.dateTime = root.incidenceWrapper.incidenceStart;
                        incidenceStartTimeCombo.dateTime = root.incidenceWrapper.incidenceStart;
253
254
                        incidenceStartDateCombo.display = root.incidenceWrapper.incidenceStartDateDisplay;
                        incidenceStartTimeCombo.display = root.incidenceWrapper.incidenceStartTimeDisplay;
255
256
257
                    }

                    function onIncidenceEndChanged() {
258
259
                        incidenceEndDateCombo.dateTime = root.incidenceWrapper.incidenceEnd;
                        incidenceEndTimeCombo.dateTime = root.incidenceWrapper.incidenceEnd;
260
261
                        incidenceEndDateCombo.display = root.incidenceWrapper.incidenceEndDateDisplay;
                        incidenceEndTimeCombo.display = root.incidenceWrapper.incidenceEndTimeDisplay;
262
263
264
                    }
                }

265
                RowLayout {
266
                    id: incidenceStartLayout
267

Claudio Cambra's avatar
Claudio Cambra committed
268
                    Kirigami.FormData.label: i18n("Start:")
269
                    Layout.fillWidth: true
270
271
272
273
274
                    visible: !incidenceForm.isTodo || (incidenceForm.isTodo && !isNaN(root.incidenceWrapper.incidenceStart.getTime()))

                    QQC2.CheckBox {
                        id: incidenceStartCheckBox

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
285
                            } else if(incidenceForm.isTodo) {
                                root.incidenceWrapper.incidenceEnd = new Date()
286
287
288
289
290
                            }
                        }
                        visible: incidenceForm.isTodo
                    }

291

292
                    DateCombo {
293
                        id: incidenceStartDateCombo
294

Claudio Cambra's avatar
Claudio Cambra committed
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
305
                        timeZoneOffset: root.incidenceWrapper.startTimeZoneUTCOffsetMins
                        display: root.incidenceWrapper.incidenceEndTimeDisplay
306
                        dateTime: root.incidenceWrapper.incidenceStart
307
                        onNewTimeChosen: root.incidenceWrapper.setIncidenceStartTime(hours, minutes)
308
                        enabled: !allDayCheckBox.checked && (!incidenceForm.isTodo || incidenceStartCheckBox.checked)
Claudio Cambra's avatar
Claudio Cambra committed
309
                        visible: !allDayCheckBox.checked
310
311
                    }
                }
Claudio Cambra's avatar
Claudio Cambra committed
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
320
321

                    QQC2.CheckBox {
                        id: incidenceEndCheckBox

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
332
                            } else if(incidenceForm.isTodo) {
                                root.incidenceWrapper.incidenceEnd = new Date()
333
334
335
336
                            }
                        }
                        visible: incidenceForm.isTodo
                    }
337

338
                    DateCombo {
339
                        id: incidenceEndDateCombo
340

341
                        Layout.fillWidth: true
342
                        display: root.incidenceWrapper.incidenceEndDateDisplay
343
                        dateTime: root.incidenceWrapper.incidenceEnd
344
                        onNewDateChosen: root.incidenceWrapper.setIncidenceEndDate(day, month, year)
345
                        enabled: !incidenceForm.isTodo || (incidenceForm.isTodo && incidenceEndCheckBox.checked)
346
                    }
347
                    TimeCombo {
348
                        id: incidenceEndTimeCombo
349
350

                        Layout.fillWidth: true
351
352
                        timeZoneOffset: root.incidenceWrapper.endTimeZoneUTCOffsetMins
                        display: root.incidenceWrapper.incidenceEndTimeDisplay
353
                        dateTime: root.incidenceWrapper.incidenceEnd
354
                        onNewTimeChosen: root.incidenceWrapper.setIncidenceEndTime(hours, minutes)
355
                        enabled: (!incidenceForm.isTodo && !allDayCheckBox.checked) || (incidenceForm.isTodo && incidenceEndCheckBox.checked)
356
                        visible: !allDayCheckBox.checked
357
358
                    }
                }
359

360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
                QQC2.ComboBox {
                    id: timeZoneComboBox
                    Kirigami.FormData.label: i18n("Timezone:")
                    Layout.fillWidth: true

                    model: TimeZoneListModel {
                        id: timeZonesModel
                    }

                    textRole: "display"
                    valueRole: "id"
                    currentIndex: model ? timeZonesModel.getTimeZoneRow(root.incidenceWrapper.timeZone) : -1
                    delegate: Kirigami.BasicListItem {
                        label: model.display
                        onClicked: root.incidenceWrapper.timeZone = model.id
                    }
                    enabled: !incidenceForm.isTodo || (incidenceForm.isTodo && incidenceEndCheckBox.checked)
                }

Claudio Cambra's avatar
Claudio Cambra committed
379
380
381
382
                QQC2.ComboBox {
                    id: repeatComboBox
                    Kirigami.FormData.label: i18n("Repeat:")
                    Layout.fillWidth: true
383

384
                    enabled: !incidenceForm.isTodo || !isNaN(root.incidenceWrapper.incidenceStart.getTime()) || !isNaN(root.incidenceWrapper.incidenceEnd.getTime())
Claudio Cambra's avatar
Claudio Cambra committed
385
386
                    textRole: "display"
                    valueRole: "interval"
387
                    onCurrentIndexChanged: if(currentIndex === 0) { root.incidenceWrapper.clearRecurrences() }
Claudio Cambra's avatar
Claudio Cambra committed
388
                    currentIndex: {
389
                        switch(root.incidenceWrapper.recurrenceData.type) {
390
                            case 0:
391
                                return root.incidenceWrapper.recurrenceData.type;
392
                            case 3: // Daily
393
394
                                return root.incidenceWrapper.recurrenceData.frequency === 1 ?
                                    root.incidenceWrapper.recurrenceData.type - 2 : 5
395
                            case 4: // Weekly
396
397
398
                                return root.incidenceWrapper.recurrenceData.frequency === 1 ?
                                    (root.incidenceWrapper.recurrenceData.weekdays.filter(x => x === true).length === 0 ?
                                    root.incidenceWrapper.recurrenceData.type - 2 : 5) : 5
Claudio Cambra's avatar
Claudio Cambra committed
399
                            case 5: // Monthly on position (e.g. third Monday)
400
                            case 8: // Yearly on day
Claudio Cambra's avatar
Claudio Cambra committed
401
402
403
404
405
                            case 9: // Yearly on position
                            case 10: // Other
                                return 5;
                            case 6: // Monthly on day (1st of month)
                                return 3;
406
                            case 7: // Yearly on month
Claudio Cambra's avatar
Claudio Cambra committed
407
408
                                return 4;
                        }
409
                    }
Claudio Cambra's avatar
Claudio Cambra committed
410
411
                    model: [
                        {key: "never", display: i18n("Never"), interval: -1},
412
413
414
415
                        {key: "daily", display: i18n("Daily"), interval: IncidenceWrapper.Daily},
                        {key: "weekly", display: i18n("Weekly"), interval: IncidenceWrapper.Weekly},
                        {key: "monthly", display: i18n("Monthly"), interval: IncidenceWrapper.Monthly},
                        {key: "yearly", display: i18n("Yearly"), interval: IncidenceWrapper.Yearly},
Claudio Cambra's avatar
Claudio Cambra committed
416
417
                        {key: "custom", display: i18n("Custom"), interval: -1}
                    ]
418
419
420
421
422
423
424
425
                    delegate: Kirigami.BasicListItem {
                        text: modelData.display
                        onClicked: if (modelData.interval > 0) {
                            root.incidenceWrapper.setRegularRecurrence(modelData.interval)
                        } else {
                            root.incidenceWrapper.clearRecurrences();
                        }
                    }
Claudio Cambra's avatar
Claudio Cambra committed
426
                    popup.z: 1000
427
                }
428

429
                Kirigami.FormLayout {
Claudio Cambra's avatar
Claudio Cambra committed
430
                    id: customRecurrenceLayout
431

Claudio Cambra's avatar
Claudio Cambra committed
432
433
434
                    Layout.fillWidth: true
                    Layout.leftMargin: Kirigami.Units.largeSpacing
                    visible: repeatComboBox.currentIndex > 0 // Not "Never" index
435

436
437
                    function setOcurrence() {
                        root.incidenceWrapper.setRegularRecurrence(recurScaleRuleCombobox.currentValue, recurFreqRuleSpinbox.value);
438

439
                        if(recurScaleRuleCombobox.currentValue === IncidenceWrapper.Weekly) {
Claudio Cambra's avatar
Claudio Cambra committed
440
441
                            weekdayCheckboxRepeater.setWeekdaysRepeat();
                        }
442
                    }
443

444
                    // Custom controls
445
                    RowLayout {
446
                        Layout.fillWidth: true
447
                        Kirigami.FormData.label: i18n("Every:")
448
449
                        visible: repeatComboBox.currentIndex === 5

450
451
                        QQC2.SpinBox {
                            id: recurFreqRuleSpinbox
452

453
454
                            Layout.fillWidth: true
                            from: 1
455
456
                            value: root.incidenceWrapper.recurrenceData.frequency
                            onValueChanged: if(visible) { root.incidenceWrapper.setRecurrenceDataItem("frequency", value) }
457
458
459
460
461
                        }
                        QQC2.ComboBox {
                            id: recurScaleRuleCombobox

                            Layout.fillWidth: true
462
463
464
465
466
                            visible: repeatComboBox.currentIndex === 5
                            // Make sure it defaults to something
                            onVisibleChanged: if(visible && currentIndex < 0) { currentIndex = 0; customRecurrenceLayout.setOcurrence(); }

                            textRole: "display"
467
                            valueRole: "interval"
468
469
470
471
                            onCurrentValueChanged: if(visible) {
                                customRecurrenceLayout.setOcurrence();
                                repeatComboBox.currentIndex = 5; // Otherwise resets to default daily/weekly/etc.
                            }
472
                            currentIndex: {
473
                                if(root.incidenceWrapper.recurrenceData.type === undefined) {
474
                                    return -1;
475
476
                                }

477
                                switch(root.incidenceWrapper.recurrenceData.type) {
478
479
                                    case 3: // Daily
                                    case 4: // Weekly
480
                                        return root.incidenceWrapper.recurrenceData.type - 3
481
482
483
484
485
486
487
488
489
490
                                    case 5: // Monthly on position (e.g. third Monday)
                                    case 6: // Monthly on day (1st of month)
                                        return 2;
                                    case 7: // Yearly on month
                                    case 8: // Yearly on day
                                    case 9: // Yearly on position
                                        return 3;
                                    default:
                                        return -1;
                                }
Claudio Cambra's avatar
Claudio Cambra committed
491
                            }
492

493
                            model: [
494
495
496
497
                                {key: "day", display: i18np("day", "days", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Daily},
                                {key: "week", display: i18np("week", "weeks", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Weekly},
                                {key: "month", display: i18np("month", "months", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Monthly},
                                {key: "year", display: i18np("year", "years", recurFreqRuleSpinbox.value), interval: IncidenceWrapper.Yearly},
498
499
                            ]
                            delegate: Kirigami.BasicListItem {
500
                                text: modelData.display
501
                                onClicked: {
502
                                    customRecurrenceLayout.setOcurrence();
503
504
505
                                    repeatComboBox.currentIndex = 5; // Otherwise resets to default daily/weekly/etc.
                                }
                            }
506

507
                            popup.z: 1000
Claudio Cambra's avatar
Claudio Cambra committed
508
                        }
509
                    }
510

Claudio Cambra's avatar
Claudio Cambra committed
511
512
513
                    // Custom controls specific to weekly
                    GridLayout {
                        id: recurWeekdayRuleLayout
514
                        Layout.fillWidth: true
515

Claudio Cambra's avatar
Claudio Cambra committed
516
517
                        columns: 7
                        visible: recurScaleRuleCombobox.currentIndex === 1 && repeatComboBox.currentIndex === 5 // "week"/"weeks" index
518

Claudio Cambra's avatar
Claudio Cambra committed
519
520
521
522
523
524
525
                        Repeater {
                            model: 7
                            delegate: QQC2.Label {
                                Layout.fillWidth: true
                                horizontalAlignment: Text.AlignHCenter
                                text: Qt.locale().dayName(Qt.locale().firstDayOfWeek + index, Locale.ShortFormat)
                            }
526
527
                        }

Claudio Cambra's avatar
Claudio Cambra committed
528
529
                        Repeater {
                            id: weekdayCheckboxRepeater
530

531
532
533
534
535
536
537
                            property var checkboxes: []
                            function setWeekdaysRepeat() {
                                let selectedDays = new Array(7)
                                for(let checkbox of checkboxes) {
                                    // C++ func takes 7 bit array
                                    selectedDays[checkbox.dayNumber] = checkbox.checked
                                }
538
                                root.incidenceWrapper.setRecurrenceDataItem("weekdays", selectedDays);
539
540
                            }

Claudio Cambra's avatar
Claudio Cambra committed
541
542
543
544
545
546
547
                            model: 7
                            delegate: QQC2.CheckBox {
                                Layout.alignment: Qt.AlignHCenter
                                // We make sure we get dayNumber per the day of the week number used by C++ Qt
                                property int dayNumber: Qt.locale().firstDayOfWeek + index > 7 ?
                                                        Qt.locale().firstDayOfWeek + index - 1 - 7 :
                                                        Qt.locale().firstDayOfWeek + index - 1
548

549
                                checked: if(root.incidenceWrapper.recurrenceData) root.incidenceWrapper.recurrenceData.weekdays[dayNumber]
550
551
552
553
554
                                onClicked: {
                                    let newWeekdays = [...root.incidenceWrapper.recurrenceData.weekdays];
                                    newWeekdays[dayNumber] = !root.incidenceWrapper.recurrenceData.weekdays[dayNumber];
                                    root.incidenceWrapper.setRecurrenceDataItem("weekdays", newWeekdays);
                                }
555
                            }
556
                        }
Claudio Cambra's avatar
Claudio Cambra committed
557
                    }
558

Claudio Cambra's avatar
Claudio Cambra committed
559
560
561
562
                    // Controls specific to monthly recurrence
                    QQC2.ButtonGroup {
                        buttons: monthlyRecurRadioColumn.children
                    }
563

Claudio Cambra's avatar
Claudio Cambra committed
564
565
                    ColumnLayout {
                        id: monthlyRecurRadioColumn
566

567
                        Kirigami.FormData.label: i18n("On:")
568

Claudio Cambra's avatar
Claudio Cambra committed
569
570
                        Layout.fillWidth: true
                        visible: recurScaleRuleCombobox.currentIndex === 2 && repeatComboBox.currentIndex === 5 // "month/months" index
571

Claudio Cambra's avatar
Claudio Cambra committed
572
                        QQC2.RadioButton {
573
                            property int dateOfMonth: incidenceStartDateCombo.dateFromText.getDate()
574

575
                            text: i18nc("%1 is the day number of month", "The %1 of each month", LabelUtils.numberToString(dateOfMonth))
576
577
578

                            checked: root.incidenceWrapper.recurrenceData.type === 6 // Monthly on day (1st of month)
                            onClicked: customRecurrenceLayout.setOcurrence()
579
580
                        }
                        QQC2.RadioButton {
581
582
                            property int dayOfWeek: incidenceStartDateCombo.dateFromText.getDay() > 0 ?
                                                    incidenceStartDateCombo.dateFromText.getDay() - 1 :
583
                                                    7 // C++ Qt day of week index goes Mon-Sun, 0-7
584
585
                            property int weekOfMonth: Math.ceil((incidenceStartDateCombo.dateFromText.getDate() + 6 - incidenceStartDateCombo.dateFromText.getDay())/7);
                            property string dayOfWeekString: Qt.locale().dayName(incidenceStartDateCombo.dateFromText.getDay())
586

587
                            text: i18nc("the weekOfMonth dayOfWeekString of each month", "The %1 %2 of each month", LabelUtils.numberToString(weekOfMonth), dayOfWeekString)
588
589
590
                            checked: root.incidenceWrapper.recurrenceData.type === 5 // Monthly on position
                            onTextChanged: if(checked) { root.incidenceWrapper.setMonthlyPosRecurrence(weekOfMonth, dayOfWeek); }
                            onClicked: root.incidenceWrapper.setMonthlyPosRecurrence(weekOfMonth, dayOfWeek)
Claudio Cambra's avatar
Claudio Cambra committed
591
                        }
592
593
594
                    }


Claudio Cambra's avatar
Claudio Cambra committed
595
                    // Repeat end controls (visible on all recurrences)
596
                    RowLayout {
597
                        Layout.fillWidth: true
598
599
600
601
602
603
                        Kirigami.FormData.label: i18n("Ends:")

                        QQC2.ComboBox {
                            id: endRecurType

                            Layout.fillWidth: true
604
605
606
607
608
                            // Recurrence duration returns -1 for never ending and 0 when the recurrence
                            // end date is set. Any number larger is the set number of recurrences
                            currentIndex: root.incidenceWrapper.recurrenceData.duration <= 0 ?
                                root.incidenceWrapper.recurrenceData.duration + 1 : 2

609
610
611
612
613
614
615
616
617
                            textRole: "display"
                            valueRole: "duration"
                            model: [
                                {display: i18n("Never"), duration: -1},
                                {display: i18n("On"), duration: 0},
                                {display: i18n("After"), duration: 1}
                            ]
                            delegate: Kirigami.BasicListItem {
                                text: modelData.display
618
                                onClicked: root.incidenceWrapper.setRecurrenceDataItem("duration", modelData.duration)
619
620
                            }
                            popup.z: 1000
Claudio Cambra's avatar
Claudio Cambra committed
621
                        }
622
623
                        QQC2.ComboBox {
                            id: recurEndDateCombo
624

625
                            Layout.fillWidth: true
626
                            visible: endRecurType.currentIndex === 1
627
                            onVisibleChanged: if (visible && isNaN(root.incidenceWrapper.recurrenceData.endDateTime.getTime())) { root.incidenceWrapper.setRecurrenceDataItem("endDateTime", new Date()); }
628
                            editable: true
629
                            editText: root.incidenceWrapper.recurrenceData.endDateTime.toLocaleDateString(Qt.locale(), Locale.NarrowFormat);
630

631
                            inputMethodHints: Qt.ImhDate
632

633
634
                            property date dateFromText: Date.fromLocaleDateString(Qt.locale(), editText, Locale.NarrowFormat)
                            property bool validDate: !isNaN(dateFromText.getTime())
635

636
                            onDateFromTextChanged: {
637
                                const datePicker = recurEndDatePicker;
638
639
640
641
642
                                if (validDate && activeFocus) {
                                    datePicker.selectedDate = dateFromText;
                                    datePicker.clickedDate = dateFromText;

                                    if (visible) {
643
                                    root.incidenceWrapper.setRecurrenceDataItem("endDateTime", dateFromText);
644
                                    }
645
646
                                }
                            }
647

648
649
                            popup: QQC2.Popup {
                                id: recurEndDatePopup
650

651
652
653
654
                                width: Kirigami.Units.gridUnit * 18
                                height: Kirigami.Units.gridUnit * 18
                                y: parent.y + parent.height
                                z: 1000
655

656
657
658
659
                                DatePicker {
                                    id: recurEndDatePicker
                                    anchors.fill: parent
                                    onDatePicked: {
660
661
                                        root.incidenceWrapper.setRecurrenceDataItem("endDateTime", pickedDate);
                                        recurEndDatePopup.close();
662
                                    }
663
664
665
666
                                }
                            }
                        }

667
                        RowLayout {
668
                            Layout.fillWidth: true
669
                            visible: endRecurType.currentIndex === 2
670
                            onVisibleChanged: if (visible) { root.incidenceWrapper.setRecurrenceOcurrences(recurOcurrenceEndSpinbox.value) }
671
672
673

                            QQC2.SpinBox {
                                id: recurOcurrenceEndSpinbox
674

675
676
                                Layout.fillWidth: true
                                from: 1
677
678
                                value: root.incidenceWrapper.recurrenceData.duration
                                onValueChanged: if (visible) { root.incidenceWrapper.setRecurrenceOcurrences(value) }
679
680
681
682
                            }
                            QQC2.Label {
                                text: i18np("occurrence", "occurrences", recurOcurrenceEndSpinbox.value)
                            }
683
684
685
686
                        }
                    }

                    ColumnLayout {
687
688
689
                        Kirigami.FormData.label: i18n("Exceptions:")
                        Layout.fillWidth: true

690
691
692
                        QQC2.ComboBox {
                            id: exceptionAddButton
                            Layout.fillWidth: true
693
                            displayText: i18n("Add Recurrence Exception")
694
695
696
697
698
699
700
701
702
703
704
705

                            popup: QQC2.Popup {
                                id: recurExceptionPopup

                                width: Kirigami.Units.gridUnit * 18
                                height: Kirigami.Units.gridUnit * 18
                                y: parent.y + parent.height
                                z: 1000

                                DatePicker {
                                    id: recurExceptionPicker
                                    anchors.fill: parent
706
                                    selectedDate: incidenceStartDateCombo.dateFromText
707
                                    onDatePicked: {
708
                                        root.incidenceWrapper.recurrenceExceptionsModel.addExceptionDateTime(pickedDate)
709
710
711
                                        recurExceptionPopup.close()
                                    }
                                }
712
                            }
Claudio Cambra's avatar
Claudio Cambra committed
713
714
715
716
                        }

                        Repeater {
                            id: exceptionsRepeater
717
                            model: root.incidenceWrapper.recurrenceExceptionsModel
Claudio Cambra's avatar
Claudio Cambra committed
718
                            delegate: RowLayout {
719
                                Kirigami.BasicListItem {
Claudio Cambra's avatar
Claudio Cambra committed
720
721
722
723
724
                                    Layout.fillWidth: true
                                    text: date.toLocaleDateString(Qt.locale())
                                }
                                QQC2.Button {
                                    icon.name: "edit-delete-remove"
725
                                    onClicked: root.incidenceWrapper.recurrenceExceptionsModel.deleteExceptionDateTime(date)
Claudio Cambra's avatar
Claudio Cambra committed
726
                                }
727
728
729
730
                            }
                        }
                    }
                }
731

Claudio Cambra's avatar
Claudio Cambra committed
732
733
                Kirigami.Separator {
                    Kirigami.FormData.isSection: true
734
                }
735

736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
                RowLayout {
                    Kirigami.FormData.label: i18n("Location:")
                    Layout.fillWidth: true

                    QQC2.TextField {
                        id: locationField

                        property bool typed: false

                        Layout.fillWidth: true
                        placeholderText: i18n("Optional")
                        text: root.incidenceWrapper.location
                        onTextChanged: root.incidenceWrapper.location = text
                        Keys.onPressed: locationsMenu.open()

                        QQC2.BusyIndicator {
                            height: parent.height
                            anchors.right: parent.right
                            running: locationsModel.status === GeocodeModel.Loading
                            visible: locationsModel.status === GeocodeModel.Loading
                        }

                        QQC2.Menu {
                            id: locationsMenu
                            width: parent.width
                            y: parent.height // Y is relative to parent
                            focus: false

                            Repeater {
                                model: GeocodeModel {
                                    id: locationsModel
                                    plugin: locationPlugin
                                    query: root.incidenceWrapper.location
                                    autoUpdate: true
                                }
                                delegate: QQC2.MenuItem {
                                    text: locationData.address.text
                                    onClicked: root.incidenceWrapper.location = locationData.address.text
                                }
                            }

                            Plugin {
                                id: locationPlugin
                                name: "osm"
                            }
                        }
                    }
                    QQC2.CheckBox {
                        id: mapVisibleCheckBox
                        text: i18n("Show map")
                        visible: Config.enableMaps
                    }
                }

                ColumnLayout {
                    id: mapLayout
                    Layout.fillWidth: true
                    visible: Config.enableMaps && mapVisibleCheckBox.checked

                    Loader {
                        id: mapLoader

                        Layout.fillWidth: true
                        height: Kirigami.Units.gridUnit * 16
                        asynchronous: true
                        active: visible

                        sourceComponent: LocationMap {
                            id: map
                            selectMode: true
                            query: root.incidenceWrapper.location
                            onSelectedLocationAddress: root.incidenceWrapper.location = address
                        }
                    }
                }

                // Restrain the descriptionTextArea from getting too chonky
                ColumnLayout {
                    Layout.fillWidth: true
                    Layout.maximumWidth: incidenceForm.wideMode ? Kirigami.Units.gridUnit * 25 : -1
                    Kirigami.FormData.label: i18n("Description:")

                    QQC2.TextArea {
                        id: descriptionTextArea

                        Layout.fillWidth: true
                        placeholderText: i18n("Optional")
                        text: root.incidenceWrapper.description
824
                        wrapMode: Text.Wrap
825
826
827
828
                        onTextChanged: root.incidenceWrapper.description = text
                    }
                }

Claudio Cambra's avatar
Claudio Cambra committed
829
                RowLayout {
830
831
832
                    Kirigami.FormData.label: i18n("Tags:")
                    Layout.fillWidth: true

Claudio Cambra's avatar
Claudio Cambra committed
833
834
835
836
837
838
                    QQC2.ComboBox {
                        Layout.fillWidth: true

                        model: TagManager.tagModel
                        displayText: root.incidenceWrapper.categories.length > 0 ?
                            root.incidenceWrapper.categories.join(i18nc("List separator", ", ")) :
839
                            Kirigami.Settings.tabletMode ? i18n("Tap to set tags…") : i18n("Click to set tags…")
Claudio Cambra's avatar
Claudio Cambra committed
840
841
842
843
844
845
846
847
848
849
850
851

                        delegate: Kirigami.CheckableListItem {
                            label: model.display
                            reserveSpaceForIcon: false
                            checked: root.incidenceWrapper.categories.includes(model.display)
                            action: QQC2.Action {
                                onTriggered: {
                                    checked = !checked;
                                    root.incidenceWrapper.categories.includes(model.display) ?
                                        root.incidenceWrapper.categories = root.incidenceWrapper.categories.filter(tag => tag !== model.display) :
                                        root.incidenceWrapper.categories = [...root.incidenceWrapper.categories, model.display]
                                }
852
853
854
                            }
                        }
                    }
Claudio Cambra's avatar
Claudio Cambra committed
855
                    QQC2.Button {
856
                        text: i18n("Manage tags…")
Claudio Cambra's avatar
Claudio Cambra committed
857
858
                        onClicked: KalendarApplication.action("open_tag_manager").trigger()
                    }
859
860
861
862
863
                }

                Kirigami.Separator {
                    Kirigami.FormData.isSection: true
                }
Claudio Cambra's avatar
Claudio Cambra committed
864
865
                ColumnLayout {
                    id: remindersColumn
866

867
868
                    Kirigami.FormData.label: i18n("Reminders:")
                    Kirigami.FormData.labelAlignment: remindersRepeater.count ? Qt.AlignTop : Qt.AlignVCenter
869
870
                    Layout.fillWidth: true

Claudio Cambra's avatar
Claudio Cambra committed
871
872
                    Repeater {
                        id: remindersRepeater
873
874
875

                        Layout.fillWidth: true

876
                        model: root.incidenceWrapper.remindersModel
Claudio Cambra's avatar
Claudio Cambra committed
877
878
879
                        // All of the alarms are handled within the delegates.

                        delegate: RowLayout {
880
881
                            Layout.fillWidth: true

Claudio Cambra's avatar
Claudio Cambra committed
882
883
884
                            QQC2.ComboBox {
                                // There is also a chance here to add a feature for the user to pick reminder type.
                                Layout.fillWidth: true
885

Claudio Cambra's avatar
Claudio Cambra committed
886
887
888
889
                                property var selectedIndex: 0

                                displayText: LabelUtils.secondsToReminderLabel(startOffset)
                                //textRole: "DisplayNameRole"
890
                                onCurrentValueChanged: root.incidenceWrapper.remindersModel.setData(root.incidenceWrapper.remindersModel.index(index, 0),
Claudio Cambra's avatar
Claudio Cambra committed
891
                                                                                                            currentValue,
892
                                                                                                            root.incidenceWrapper.remindersModel.dataroles.startOffset)
Claudio Cambra's avatar
Claudio Cambra committed
893
894
895
                                onCountChanged: selectedIndex = currentIndex // Gets called *just* before modelChanged
                                onModelChanged: currentIndex = selectedIndex

896
                                model: [0, // We times by -1 to make times be before incidence
Claudio Cambra's avatar
Claudio Cambra committed
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
                                        -1 * 5 * 60, // 5 minutes
                                        -1 * 10 * 60,
                                        -1 * 15 * 60,
                                        -1 * 30 * 60,
                                        -1 * 45 * 60,
                                        -1 * 1 * 60 * 60, // 1 hour
                                        -1 * 2 * 60 * 60,
                                        -1 * 1 * 24 * 60 * 60, // 1 day
                                        -1 * 2 * 24 * 60 * 60,
                                        -1 * 5 * 24 * 60 * 60]
                                        // All these times are in seconds.
                                delegate: Kirigami.BasicListItem {
                                    text: LabelUtils.secondsToReminderLabel(modelData)
                                }

                                popup.z: 1000
                            }
914

Claudio Cambra's avatar
Claudio Cambra committed
915
916
                            QQC2.Button {
                                icon.name: "edit-delete-remove"
917
                                onClicked: root.incidenceWrapper.remindersModel.deleteAlarm(model.index);
Claudio Cambra's avatar
Claudio Cambra committed
918
                            }
919
                        }
920
                    }
921
922
923
924

                    QQC2.Button {
                        id: remindersButton

925
                        text: i18n("Add Reminder")
926
927
                        Layout.fillWidth: true

928
                        onClicked: root.incidenceWrapper.remindersModel.addAlarm();
929
930
931
932
933
                    }
                }

                Kirigami.Separator {
                    Kirigami.FormData.isSection: true
934
                }
935

Claudio Cambra's avatar
Claudio Cambra committed
936
937
                ColumnLayout {
                    id: attendeesColumn
938

Claudio Cambra's avatar
Claudio Cambra committed
939
                    Kirigami.FormData.label: i18n("Attendees:")
940
                    Kirigami.FormData.labelAlignment: attendeesRepeater.count ? Qt.AlignTop : Qt.AlignVCenter
941
942
                    Layout.fillWidth: true

Claudio Cambra's avatar
Claudio Cambra committed
943
                    Repeater {
944
                        id: attendeesRepeater
945
                        model: root.incidenceWrapper.attendeesModel
Claudio Cambra's avatar
Claudio Cambra committed
946
947
                        // All of the alarms are handled within the delegates.
                        Layout.fillWidth: true
948

949
                        delegate: Kirigami.AbstractCard {
950

951
952
                            topPadding: Kirigami.Units.smallSpacing
                            bottomPadding: Kirigami.Units.smallSpacing
953

954
955
956
                            contentItem: Item {
                                implicitWidth: attendeeCardContent.implicitWidth
                                implicitHeight: attendeeCardContent.implicitHeight