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

KDE's Doxygen guidelines are available online.