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