Kirigami-addons

DatePicker.qml
1// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
2// SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5import QtQuick
6import QtQuick.Controls as QQC2
7import QtQuick.Layouts
8import org.kde.kirigami as Kirigami
9import org.kde.kirigamiaddons.dateandtime
10import org.kde.kirigamiaddons.components as Components
11import org.kde.kirigamiaddons.delegates as Delegates
12
13QQC2.Control {
14 id: root
15
16 signal datePicked(date pickedDate)
17
18 property date selectedDate: new Date() // Decides calendar span
19 readonly property int year: selectedDate.getFullYear()
20 readonly property int month: selectedDate.getMonth()
21 readonly property int day: selectedDate.getDate()
22 property bool showDays: true
23 property bool showControlHeader: true
24
25 /**
26 * This property holds the minimum date (inclusive) that the user can select.
27 *
28 * By default, no limit is applied to the date selection.
29 */
30 property date minimumDate
31
32 /**
33 * This property holds the maximum date (inclusive) that the user can select.
34 *
35 * By default, no limit is applied to the date selection.
36 */
37 property date maximumDate
38
39 topPadding: Kirigami.Units.largeSpacing
40 rightPadding: Kirigami.Units.largeSpacing
41 bottomPadding: Kirigami.Units.largeSpacing
42 leftPadding: Kirigami.Units.largeSpacing
43
44 onActiveFocusChanged: if (activeFocus) {
45 dateSegmentedButton.forceActiveFocus();
46 }
47
48 property bool _completed: false
49 property bool _runSetDate: false
50
51 onSelectedDateChanged: if (selectedDate !== null && _completed) {
52 setToDate(selectedDate)
53 }
54
55 Component.onCompleted: {
56 _completed = true;
57 if (selectedDate) {
58 setToDate(selectedDate);
59 }
60 }
61 onShowDaysChanged: if (!showDays) pickerView.currentIndex = 1;
62
63 function setToDate(date) {
64 if (_runSetDate) {
65 return;
66 }
67 _runSetDate = true;
68
69 if (root.minimumDate.valueOf() && date.valueOf() < minimumDate.valueOf()) {
70 date = minimumDate;
71 }
72
73 if (root.maximumDate.valueOf() && date.valueOf() > maximumDate.valueOf()) {
74 date = maximumDate;
75 }
76
77 const yearDiff = date.getFullYear() - yearPathView.currentItem.startDate.getFullYear();
78 // For the decadeDiff we add one to the input date year so that we use e.g. 2021, making the pathview move to the grid that contains the 2020 decade
79 // instead of staying within the 2010 decade, which contains a 2020 cell at the very end
80 const decadeDiff = Math.floor((date.getFullYear() + 1 - decadePathView.currentItem.startDate.getFullYear()) / 12); // 12 years in one decade grid
81
82 let newYearIndex = yearPathView.currentIndex + yearDiff;
83 let newDecadeIndex = decadePathView.currentIndex + decadeDiff;
84
85 let firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
86 let lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 2,0), InfiniteCalendarViewModel.StartDateRole);
87 let firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
88 let lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
89
90 if(showDays) { // Set to correct index, including creating new dates in model if needed, for the month view
91 const monthDiff = date.getMonth() - monthPathView.currentItem.firstDayOfMonth.getMonth() + (12 * (date.getFullYear() - monthPathView.currentItem.firstDayOfMonth.getFullYear()));
92 let newMonthIndex = monthPathView.currentIndex + monthDiff;
93 let firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
94 let lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
95
96 while(firstMonthItemDate >= date) {
97 monthPathView.model.addDates(false)
98 firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
99 newMonthIndex = 0;
100 }
101 if(firstMonthItemDate < date && newMonthIndex === 0) {
102 newMonthIndex = date.getMonth() - firstMonthItemDate.getMonth() + (12 * (date.getFullYear() - firstMonthItemDate.getFullYear())) + 1;
103 }
104
105 while(lastMonthItemDate <= date) {
106 monthPathView.model.addDates(true)
107 lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
108 }
109
110 monthPathView.currentIndex = newMonthIndex;
111 }
112
113 // Set to index and create dates if needed for year view
114 while(firstYearItemDate >= date) {
115 yearPathView.model.addDates(false)
116 firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
117 newYearIndex = 0;
118 }
119 if(firstYearItemDate < date && newYearIndex === 0) {
120 newYearIndex = date.getFullYear() - firstYearItemDate.getFullYear() + 1;
121 }
122
123 while(lastYearItemDate <= date) {
124 yearPathView.model.addDates(true)
125 lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
126 }
127
128 // Set to index and create dates if needed for decade view
129 while(firstDecadeItemDate >= date) {
130 decadePathView.model.addDates(false)
131 firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
132 newDecadeIndex = 0;
133 }
134 if(firstDecadeItemDate < date && newDecadeIndex === 0) {
135 newDecadeIndex = date.getFullYear() - firstDecadeItemDate.getFullYear() + 1;
136 }
137
138 while(lastDecadeItemDate.getFullYear() <= date.getFullYear()) {
139 decadePathView.model.addDates(true)
140 lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
141 }
142
143 yearPathView.currentIndex = newYearIndex;
144 decadePathView.currentIndex = newDecadeIndex;
145
146 _runSetDate = false;
147 }
148
149 function goToday() {
150 selectedDate = new Date()
151 }
152
153 function prevMonth() {
154 const newDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() - 1, selectedDate.getDate());
155 if (root.minimumDate.valueOf() && newDate.valueOf() < minimumDate.valueOf()) {
156 if (selectedDate == minimumDate) {
157 return;
158 }
159 selectedDate = minimumDate;
160 } else {
161 selectedDate = newDate;
162 }
163 }
164
165 function nextMonth() {
166 const newDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, selectedDate.getDate());
167 if (root.maximumDate.valueOf() && newDate.valueOf() > maximumDate.valueOf()) {
168 if (selectedDate == maximumDate) {
169 return;
170 }
171 selectedDate = maximumDate;
172 return;
173 } else {
174 selectedDate = newDate;
175 }
176 }
177
178 function prevYear() {
179 const newDate = new Date(selectedDate.getFullYear() - 1, selectedDate.getMonth(), selectedDate.getDate())
180 if (root.minimumDate.valueOf() && newDate.valueOf() < minimumDate.valueOf()) {
181 if (selectedDate == minimumDate) {
182 return;
183 }
184 selectedDate = minimumDate;
185 } else {
186 selectedDate = newDate;
187 }
188 }
189
190 function nextYear() {
191 const newDate = new Date(selectedDate.getFullYear() + 1, selectedDate.getMonth(), selectedDate.getDate());
192 if (root.maximumDate && newDate.valueOf() > maximumDate.valueOf()) {
193 if (selectedDate == maximumDate) {
194 return;
195 }
196 selectedDate = maximumDate;
197 } else {
198 selectedDate = newDate;
199 }
200 }
201
202 function prevDecade() {
203 const newDate = new Date(selectedDate.getFullYear() - 10, selectedDate.getMonth(), selectedDate.getDate());
204 if (root.minimumDate.valueOf() && newDate.valueOf() < minimumDate.valueOf()) {
205 if (selectedDate == minimumDate) {
206 return;
207 }
208 selectedDate = minimumDate;
209 } else {
210 selectedDate = newDate;
211 }
212 }
213
214 function nextDecade() {
215 const newDate = new Date(selectedDate.getFullYear() + 10, selectedDate.getMonth(), selectedDate.getDate())
216 if (root.maximumDate && newDate.valueOf() > maximumDate.valueOf()) {
217 if (selectedDate == maximumDate) {
218 return;
219 }
220 selectedDate = maximumDate;
221 } else {
222 selectedDate = newDate;
223 }
224 }
225
226 contentItem: ColumnLayout {
227 id: pickerLayout
228
229 RowLayout {
230 id: headingRow
231 Layout.fillWidth: true
232 Layout.bottomMargin: Kirigami.Units.smallSpacing
233
234 Components.SegmentedButton {
235 id: dateSegmentedButton
236
237 actions: [
238 Kirigami.Action {
239 id: dayAction
240 text: root.selectedDate.getDate()
241 onTriggered: pickerView.currentIndex = 0 // dayGrid is first item in pickerView
242 checked: pickerView.currentIndex === 0
243 },
244 Kirigami.Action {
245 id: monthAction
246 text: root.selectedDate.toLocaleDateString(Qt.locale(), "MMMM")
247 onTriggered: pickerView.currentIndex = 1
248 checked: pickerView.currentIndex === 1
249 },
250 Kirigami.Action {
251 id: yearsViewCheck
252 text: root.selectedDate.getFullYear()
253 onTriggered: pickerView.currentIndex = 2
254 checked: pickerView.currentIndex === 2
255 }
256 ]
257 }
258
259 Instantiator {
260 model:dateSegmentedButton.children
261 Item {
262 required property Item modelData
263 parent: modelData
264 anchors.fill: parent
265 Accessible.ignored: !modelData.action
266 Accessible.role: Accessible.Dial
267 Accessible.focusable: true
268 Accessible.focused: parent.activeFocus
269 Accessible.name: {
270 if (modelData.action === dayAction) {
271 return i18nd("kirigami-addons6", "Day")
272 }
273 if (modelData.action === monthAction) {
274 return i18nd("kirigami-addons6", "Month")
275 }
276 if (modelData.action === yearsViewCheck) {
277 return i18nd("kirigami-addons6", "Year")
278 }
279 return ""
280 }
281 property int maximumValue: {
282 if (modelData.action === dayAction) {
283 if (maximumDate.valueOf() && root.year === maximumDate.getYear() && root.month === maximumDate.getMonth()) {
284 return maximumDate.getDate()
285 }
286 return 31
287 }
288 if (modelData.action === monthAction) {
289 if (maximumDate.valueOf() && root.year === maximumDate.getYear() ) {
290 return maximumDate.month() + 1
291 }
292 return 12
293 }
294 if (modelData.action === yearsViewCheck) {
295 if (maximumDate.valueOf()) {
296 return maximumDate.getYear()
297 }
298 return 9999
299 }
300 return 0
301 }
302 property int minimumValue: {
303 if (modelData.action === dayAction) {
304 if (minimumDate.valueOf() && root.year === minimumDate.getYear() && root.month === minimumDate.getMonth()) {
305 return minimumDate.getDate()
306 }
307 return 1
308 }
309 if (modelData.action === monthAction) {
310 if (minimumDate.valueOf() && root.year === minimumDate.getYear() ) {
311 return minimumDate.month() + 1
312 }
313 return 1
314 }
315 if (modelData.action === yearsViewCheck) {
316 if (minimumDate.valueOf()) {
317 return minimumDate.getYear()
318 }
319 return -9999
320 }
321 return 0
322 }
323 property int stepSize: 1
324 property int value: {
325 if (modelData.action === dayAction) {
326 return root.day
327 }
328 if (modelData.action === monthAction) {
329 return root.month + 1
330 }
331 if (modelData.action === yearsViewCheck) {
332 return root.year
333 }
334 return 0
335 }
336 onValueChanged: {
337 if (modelData.action === dayAction) {
338 selectedDate.setDate(value)
339 }
340 if (modelData.action === monthAction) {
341 selectedDate.setMonth(value - 1)
342 }
343 if (modelData.action === yearsViewCheck) {
344 selectedDate.setFullYear(value)
345 }
346 }
347 }
348 onObjectAdded: (index, object) => {
349 object.modelData.Accessible.ignored = true
350 }
351 }
352
353 Item {
354 Layout.fillWidth: true
355 }
356
357 Components.SegmentedButton {
358 actions: [
359 Kirigami.Action {
360 id: goPreviousAction
361 icon.name: 'go-previous-view'
362 text: i18ndc("kirigami-addons6", "@action:button", "Go Previous")
363 displayHint: Kirigami.DisplayHint.IconOnly
364 onTriggered: {
365 if (pickerView.currentIndex === 1) { // monthGrid index
366 prevYear();
367 } else if (pickerView.currentIndex === 2) { // yearGrid index
368 prevDecade();
369 } else { // dayGrid index
370 prevMonth();
371 }
372 }
373 },
374 Kirigami.Action {
375 text: i18ndc("kirigami-addons6", "@action:button", "Jump to today")
376 displayHint: Kirigami.DisplayHint.IconOnly
377 icon.name: 'go-jump-today'
378 onTriggered: goToday()
379 },
381 id: goNextAction
382 text: i18ndc("kirigami-addons6", "@action:button", "Go Next")
383 icon.name: 'go-next-view'
384 displayHint: Kirigami.DisplayHint.IconOnly
385 onTriggered: {
386 if (pickerView.currentIndex === 1) { // monthGrid index
387 nextYear();
388 } else if (pickerView.currentIndex === 2) { // yearGrid index
389 nextDecade();
390 } else { // dayGrid index
391 nextMonth();
392 }
393 }
394 }
395 ]
396 }
397 }
398
399 QQC2.SwipeView {
400 id: pickerView
401
402 clip: true
403 interactive: false
404 padding: 0
405
406 Layout.fillWidth: true
407 Layout.fillHeight: true
408
409 DatePathView {
410 id: monthPathView
411
412 mainView: pickerView
413 enabled: QQC2.SwipeView.isCurrentItem
414
415 model: InfiniteCalendarViewModel {
416 scale: InfiniteCalendarViewModel.MonthScale
417 currentDate: root.selectedDate
418 minimumDate: root.minimumDate
419 maximumDate: root.maximumDate
420 datesToAdd: 10
421 }
422
423
424 delegate: Loader {
425 id: monthViewLoader
426 property date firstDayOfMonth: model.firstDay
427 property bool isNextOrCurrentItem: index >= monthPathView.currentIndex -1 && index <= monthPathView.currentIndex + 1
428
429 active: isNextOrCurrentItem && root.showDays
430
431 sourceComponent: GridLayout {
432 id: dayGrid
433 columns: 7
434 rows: 7
435 width: monthPathView.width
436 height: monthPathView.height
437 Layout.topMargin: Kirigami.Units.smallSpacing
438
439 property var modelLoader: Loader {
440 asynchronous: true
441 sourceComponent: MonthModel {
442 year: firstDay.getFullYear()
443 month: firstDay.getMonth() + 1 // From pathview model
444 }
445 }
446
447 QQC2.ButtonGroup {
448 buttons: dayGrid.children
449 }
450
451 Repeater {
452 model: dayGrid.modelLoader.item?.weekDays
453 delegate: QQC2.Label {
454 Layout.fillWidth: true
455 Layout.fillHeight: true
456 horizontalAlignment: Text.AlignHCenter
457 rightPadding: Kirigami.Units.mediumSpacing
458 leftPadding: Kirigami.Units.mediumSpacing
459 opacity: 0.7
460 text: modelData
461 Accessible.ignored: true
462 }
463 }
464
465 Repeater {
466 id: dayRepeater
467
468 model: dayGrid.modelLoader.item
469
470 delegate: DatePickerDelegate {
471 id: dayDelegate
472
473 required property bool isToday
474 required property bool sameMonth
475 required property int dayNumber
476
477 repeater: dayRepeater
478 minimumDate: root.minimumDate
479 maximumDate: root.maximumDate
480 previousAction: goPreviousAction
481 nextAction: goNextAction
482
483 horizontalPadding: 0
484
485 Accessible.name: date.toLocaleDateString(locale, Locale.ShortFormat)
486 Accessible.ignored: !monthPathView.QQC2.SwipeView.isCurrentItem || !monthViewLoader.PathView.isCurrentItem
487
488 background {
489 visible: sameMonth
490 }
491
492 highlighted: isToday
493 checkable: true
494 checked: date.getDate() === selectedDate.getDate() &&
495 date.getMonth() === selectedDate.getMonth() &&
496 date.getFullYear() === selectedDate.getFullYear()
497 opacity: sameMonth && inScope ? 1 : 0.6
498 text: dayNumber
499 onClicked: {
500 selectedDate = date;
501 datePicked(date);
502 }
503 }
504 }
505 }
506 }
507
508 onCurrentIndexChanged: {
509 if (pickerView.currentIndex === 0) {
510 root.selectedDate = new Date(currentItem.firstDayOfMonth.getFullYear(), currentItem.firstDayOfMonth.getMonth(), root.selectedDate.getDate());
511 }
512
513 if (currentIndex >= count - 2) {
514 model.addDates(true);
515 } else if (currentIndex <= 1) {
516 model.addDates(false);
517 startIndex += model.datesToAdd;
518 }
519 }
520 }
521
522 DatePathView {
523 id: yearPathView
524
525 mainView: pickerView
526
527 model: InfiniteCalendarViewModel {
528 scale: InfiniteCalendarViewModel.YearScale
529 currentDate: root.selectedDate
530 }
531
532 delegate: Loader {
533 id: yearViewLoader
534
535 required property int index
536 required property date startDate
537
538 property bool isNextOrCurrentItem: index >= yearPathView.currentIndex -1 && index <= yearPathView.currentIndex + 1
539
540 width: parent.width
541 height: parent.height
542
543 active: isNextOrCurrentItem
544
545 sourceComponent: GridLayout {
546 id: yearGrid
547 columns: 3
548 rows: 4
549
550 QQC2.ButtonGroup {
551 buttons: yearGrid.children
552 }
553
554 Repeater {
555 id: monthRepeater
556
557 model: yearGrid.columns * yearGrid.rows
558
559 delegate: DatePickerDelegate {
560 id: monthDelegate
561
562 date: new Date(yearViewLoader.startDate.getFullYear(), index)
563
564 minimumDate: root.minimumDate.valueOf() ? new Date(root.minimumDate).setDate(0) : new Date("invalid")
565 maximumDate: root.maximumDate.valueOf() ? new Date(root.maximumDate.getFullYear(), root.maximumDate.getMonth() + 1, 0) : new Date("invalid")
566 repeater: monthRepeater
567 previousAction: goPreviousAction
568 nextAction: goNextAction
569
570 Accessible.ignored: !yearPathView.QQC2.SwipeView.isCurrentItem || !yearViewLoader.PathView.isCurrentItem
571 Accessible.name: date.toLocaleDateString(Qt.locale(), "MMMM yyyy")
572
573 horizontalPadding: padding * 2
574 rightPadding: undefined
575 leftPadding: undefined
576 highlighted: date.getMonth() === new Date().getMonth() &&
577 date.getFullYear() === new Date().getFullYear()
578 checkable: true
579 checked: date.getMonth() === selectedDate.getMonth() &&
580 date.getFullYear() === selectedDate.getFullYear()
581 text: Qt.locale().standaloneMonthName(date.getMonth())
582 onClicked: {
583 selectedDate = new Date(date);
584 root.datePicked(date);
585 if(root.showDays) pickerView.currentIndex = 0;
586 }
587 }
588 }
589 }
590 }
591
592 onCurrentIndexChanged: {
593 if (pickerView.currentIndex === 1) {
594 root.selectedDate = new Date(currentItem.startDate.getFullYear(), root.selectedDate.getMonth(), root.selectedDate.getDate());
595 }
596
597 if (currentIndex >= count - 2) {
598 model.addDates(true);
599 } else if (currentIndex <= 1) {
600 model.addDates(false);
601 startIndex += model.datesToAdd;
602 }
603 }
604
605 }
606
607 DatePathView {
608 id: decadePathView
609
610 mainView: pickerView
611
612 model: InfiniteCalendarViewModel {
613 scale: InfiniteCalendarViewModel.DecadeScale
614 currentDate: root.selectedDate
615 }
616
617 delegate: Loader {
618 id: decadeViewLoader
619
620 required property int index
621 required property date startDate
622
623 property bool isNextOrCurrentItem: index >= decadePathView.currentIndex -1 && index <= decadePathView.currentIndex + 1
624
625 width: parent.width
626 height: parent.height
627
628 active: isNextOrCurrentItem
629
630 sourceComponent: GridLayout {
631 id: decadeGrid
632
633 columns: 3
634 rows: 4
635
636 QQC2.ButtonGroup {
637 buttons: decadeGrid.children
638 }
639
640 Repeater {
641 id: decadeRepeater
642
643 model: decadeGrid.columns * decadeGrid.rows
644
645 delegate: DatePickerDelegate {
646 id: yearDelegate
647
648 readonly property bool sameDecade: Math.floor(date.getFullYear() / 10) == Math.floor(year / 10)
649
650 Accessible.ignored: !decadePathView.QQC2.SwipeView.isCurrentItem || !decadeViewLoader.PathView.isCurrentItem
651
652 date: new Date(startDate.getFullYear() + index, 0)
653 minimumDate: root.minimumDate.valueOf() ? new Date(root.minimumDate.getFullYear(), 0, 0) : new Date("invalid")
654 maximumDate: root.maximumDate.valueOf() ? new Date(root.maximumDate.getFullYear(), 12, 0) : new Date("invalid")
655 repeater: decadeRepeater
656 previousAction: goPreviousAction
657 nextAction: goNextAction
658
659 highlighted: date.getFullYear() === new Date().getFullYear()
660
661 horizontalPadding: padding * 2
662 rightPadding: undefined
663 leftPadding: undefined
664 checkable: true
665 checked: date.getFullYear() === selectedDate.getFullYear()
666 opacity: sameDecade ? 1 : 0.7
667 text: date.getFullYear()
668 onClicked: {
669 selectedDate = new Date(date);
670 root.datePicked(date);
671 pickerView.currentIndex = 1;
672 }
673 }
674 }
675 }
676 }
677
678 onCurrentIndexChanged: {
679 if (pickerView.currentIndex === 2) {
680 // getFullYear + 1 because the startDate is e.g. 2019, but we want the 2020 decade to be selected
681 root.selectedDate = new Date(currentItem.startDate.getFullYear() + 1, root.selectedDate.getMonth(), root.selectedDate.getDate());
682 }
683
684 if (currentIndex >= count - 2) {
685 model.addDates(true);
686 } else if (currentIndex <= 1) {
687 model.addDates(false);
688 startIndex += model.datesToAdd;
689 }
690 }
691
692 }
693 }
694 }
695}
QString i18ndc(const char *domain, const char *context, const char *text, const TYPE &arg...)
QString i18nd(const char *domain, const char *text, const TYPE &arg...)
int yearDiff(QDate start, QDate end)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:10:34 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.