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 text: root.selectedDate.getDate()
240 onTriggered: pickerView.currentIndex = 0 // dayGrid is first item in pickerView
241 checked: pickerView.currentIndex === 0
242 },
243 Kirigami.Action {
244 text: root.selectedDate.toLocaleDateString(Qt.locale(), "MMMM")
245 onTriggered: pickerView.currentIndex = 1
246 checked: pickerView.currentIndex === 1
247 },
248 Kirigami.Action {
249 id: yearsViewCheck
250 text: root.selectedDate.getFullYear()
251 onTriggered: pickerView.currentIndex = 2
252 checked: pickerView.currentIndex === 2
253 }
254 ]
255 }
256
257 Item {
258 Layout.fillWidth: true
259 }
260
261 Components.SegmentedButton {
262 actions: [
263 Kirigami.Action {
264 id: goPreviousAction
265 icon.name: 'go-previous-view'
266 text: i18ndc("kirigami-addons6", "@action:button", "Go Previous")
267 displayHint: Kirigami.DisplayHint.IconOnly
268 onTriggered: {
269 if (pickerView.currentIndex === 1) { // monthGrid index
270 prevYear();
271 } else if (pickerView.currentIndex === 2) { // yearGrid index
272 prevDecade();
273 } else { // dayGrid index
274 prevMonth();
275 }
276 }
277 },
278 Kirigami.Action {
279 text: i18ndc("kirigami-addons6", "@action:button", "Jump to today")
280 displayHint: Kirigami.DisplayHint.IconOnly
281 icon.name: 'go-jump-today'
282 onTriggered: goToday()
283 },
285 id: goNextAction
286 text: i18ndc("kirigami-addons6", "@action:button", "Go Next")
287 icon.name: 'go-next-view'
288 displayHint: Kirigami.DisplayHint.IconOnly
289 onTriggered: {
290 if (pickerView.currentIndex === 1) { // monthGrid index
291 nextYear();
292 } else if (pickerView.currentIndex === 2) { // yearGrid index
293 nextDecade();
294 } else { // dayGrid index
295 nextMonth();
296 }
297 }
298 }
299 ]
300 }
301 }
302
303 QQC2.SwipeView {
304 id: pickerView
305
306 clip: true
307 interactive: false
308 padding: 0
309
310 Layout.fillWidth: true
311 Layout.fillHeight: true
312
313 DatePathView {
314 id: monthPathView
315
316 mainView: pickerView
317
318 model: InfiniteCalendarViewModel {
319 scale: InfiniteCalendarViewModel.MonthScale
320 currentDate: root.selectedDate
321 minimumDate: root.minimumDate
322 maximumDate: root.maximumDate
323 datesToAdd: 10
324 }
325
326 delegate: Loader {
327 id: monthViewLoader
328 property date firstDayOfMonth: model.firstDay
329 property bool isNextOrCurrentItem: index >= monthPathView.currentIndex -1 && index <= monthPathView.currentIndex + 1
330
331 active: isNextOrCurrentItem && root.showDays
332
333 sourceComponent: GridLayout {
334 id: dayGrid
335 columns: 7
336 rows: 7
337 width: monthPathView.width
338 height: monthPathView.height
339 Layout.topMargin: Kirigami.Units.smallSpacing
340
341 property var modelLoader: Loader {
342 asynchronous: true
343 sourceComponent: MonthModel {
344 year: firstDay.getFullYear()
345 month: firstDay.getMonth() + 1 // From pathview model
346 }
347 }
348
349 QQC2.ButtonGroup {
350 buttons: dayGrid.children
351 }
352
353 Repeater {
354 model: dayGrid.modelLoader.item.weekDays
355 delegate: QQC2.Label {
356 Layout.fillWidth: true
357 Layout.fillHeight: true
358 horizontalAlignment: Text.AlignHCenter
359 rightPadding: Kirigami.Units.mediumSpacing
360 leftPadding: Kirigami.Units.mediumSpacing
361 opacity: 0.7
362 text: modelData
363 }
364 }
365
366 Repeater {
367 id: dayRepeater
368
369 model: dayGrid.modelLoader.item
370
371 delegate: DatePickerDelegate {
372 id: dayDelegate
373
374 required property bool isToday
375 required property bool sameMonth
376 required property int dayNumber
377
378 repeater: dayRepeater
379 minimumDate: root.minimumDate
380 maximumDate: root.maximumDate
381 previousAction: goPreviousAction
382 nextAction: goNextAction
383
384 horizontalPadding: 0
385
386 Accessible.name: if (dayNumber === 1 || index === 0) {
387 date.toLocaleDateString(locale, Locale.ShortFormat)
388 } else {
389 dayNumber
390 }
391
392 background {
393 visible: sameMonth
394 }
395
396 highlighted: isToday
397 checkable: true
398 checked: date.getDate() === selectedDate.getDate() &&
399 date.getMonth() === selectedDate.getMonth() &&
400 date.getFullYear() === selectedDate.getFullYear()
401 opacity: sameMonth && inScope ? 1 : 0.6
402 text: dayNumber
403 onClicked: {
404 selectedDate = date;
405 selectedDate = date;
406 datePicked(date);
407 }
408 }
409 }
410 }
411 }
412
413 onCurrentIndexChanged: {
414 if (pickerView.currentIndex === 0) {
415 root.selectedDate = new Date(currentItem.firstDayOfMonth.getFullYear(), currentItem.firstDayOfMonth.getMonth(), root.selectedDate.getDate());
416 }
417
418 if (currentIndex >= count - 2) {
419 model.addDates(true);
420 } else if (currentIndex <= 1) {
421 model.addDates(false);
422 startIndex += model.datesToAdd;
423 }
424 }
425 }
426
427 DatePathView {
428 id: yearPathView
429
430 mainView: pickerView
431
432 model: InfiniteCalendarViewModel {
433 scale: InfiniteCalendarViewModel.YearScale
434 currentDate: root.selectedDate
435 }
436
437 delegate: Loader {
438 id: yearViewLoader
439
440 required property int index
441 required property date startDate
442
443 property bool isNextOrCurrentItem: index >= yearPathView.currentIndex -1 && index <= yearPathView.currentIndex + 1
444
445 width: parent.width
446 height: parent.height
447
448 active: isNextOrCurrentItem
449
450 sourceComponent: GridLayout {
451 id: yearGrid
452 columns: 3
453 rows: 4
454
455 QQC2.ButtonGroup {
456 buttons: yearGrid.children
457 }
458
459 Repeater {
460 id: monthRepeater
461
462 model: yearGrid.columns * yearGrid.rows
463
464 delegate: DatePickerDelegate {
465 id: monthDelegate
466
467 date: new Date(yearViewLoader.startDate.getFullYear(), index)
468
469 minimumDate: root.minimumDate.valueOf() ? new Date(root.minimumDate).setDate(0) : new Date("invalid")
470 maximumDate: root.maximumDate.valueOf() ? new Date(root.maximumDate.getFullYear(), root.maximumDate.getMonth() + 1, 0) : new Date("invalid")
471 repeater: monthRepeater
472 previousAction: goPreviousAction
473 nextAction: goNextAction
474
475 horizontalPadding: padding * 2
476 rightPadding: undefined
477 leftPadding: undefined
478 highlighted: date.getMonth() === new Date().getMonth() &&
479 date.getFullYear() === new Date().getFullYear()
480 checkable: true
481 checked: date.getMonth() === selectedDate.getMonth() &&
482 date.getFullYear() === selectedDate.getFullYear()
483 text: Qt.locale().standaloneMonthName(date.getMonth())
484 onClicked: {
485 selectedDate = new Date(date);
486 root.datePicked(date);
487 if(root.showDays) pickerView.currentIndex = 0;
488 }
489 }
490 }
491 }
492 }
493
494 onCurrentIndexChanged: {
495 if (pickerView.currentIndex === 1) {
496 root.selectedDate = new Date(currentItem.startDate.getFullYear(), root.selectedDate.getMonth(), root.selectedDate.getDate());
497 }
498
499 if (currentIndex >= count - 2) {
500 model.addDates(true);
501 } else if (currentIndex <= 1) {
502 model.addDates(false);
503 startIndex += model.datesToAdd;
504 }
505 }
506
507 }
508
509 DatePathView {
510 id: decadePathView
511
512 mainView: pickerView
513
514 model: InfiniteCalendarViewModel {
515 scale: InfiniteCalendarViewModel.DecadeScale
516 currentDate: root.selectedDate
517 }
518
519 delegate: Loader {
520 id: decadeViewLoader
521
522 required property int index
523 required property date startDate
524
525 property bool isNextOrCurrentItem: index >= decadePathView.currentIndex -1 && index <= decadePathView.currentIndex + 1
526
527 width: parent.width
528 height: parent.height
529
530 active: isNextOrCurrentItem
531
532 sourceComponent: GridLayout {
533 id: decadeGrid
534
535 columns: 3
536 rows: 4
537
538 QQC2.ButtonGroup {
539 buttons: decadeGrid.children
540 }
541
542 Repeater {
543 id: decadeRepeater
544
545 model: decadeGrid.columns * decadeGrid.rows
546
547 delegate: DatePickerDelegate {
548 id: yearDelegate
549
550 readonly property bool sameDecade: Math.floor(date.getFullYear() / 10) == Math.floor(year / 10)
551
552 date: new Date(startDate.getFullYear() + index, 0)
553 minimumDate: root.minimumDate.valueOf() ? new Date(root.minimumDate.getFullYear(), 0, 0) : new Date("invalid")
554 maximumDate: root.maximumDate.valueOf() ? new Date(root.maximumDate.getFullYear(), 12, 0) : new Date("invalid")
555 repeater: decadeRepeater
556 previousAction: goPreviousAction
557 nextAction: goNextAction
558
559 highlighted: date.getFullYear() === new Date().getFullYear()
560
561 horizontalPadding: padding * 2
562 rightPadding: undefined
563 leftPadding: undefined
564 checkable: true
565 checked: date.getFullYear() === selectedDate.getFullYear()
566 opacity: sameDecade ? 1 : 0.7
567 text: date.getFullYear()
568 onClicked: {
569 selectedDate = new Date(date);
570 root.datePicked(date);
571 pickerView.currentIndex = 1;
572 }
573 }
574 }
575 }
576 }
577
578 onCurrentIndexChanged: {
579 if (pickerView.currentIndex === 2) {
580 // getFullYear + 1 because the startDate is e.g. 2019, but we want the 2020 decade to be selected
581 root.selectedDate = new Date(currentItem.startDate.getFullYear() + 1, root.selectedDate.getMonth(), root.selectedDate.getDate());
582 }
583
584 if (currentIndex >= count - 2) {
585 model.addDates(true);
586 } else if (currentIndex <= 1) {
587 model.addDates(false);
588 startIndex += model.datesToAdd;
589 }
590 }
591
592 }
593 }
594 }
595}
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...)
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 Tue Mar 26 2024 11:16:11 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.