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 2.15
6import QtQuick.Controls 2.15 as QQC2
7import QtQuick.Layouts 1.15
8import org.kde.kirigami 2.15 as Kirigami
9import org.kde.kirigamiaddons.dateandtime 1.0
10import org.kde.kirigamiaddons.components 1.0 as Components
11import org.kde.kirigamiaddons.delegates 1.0 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}
Month model exposing month days and events to a QML view.
Definition monthmodel.h:14
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)
const QObjectList & children() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:54:39 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.