Warning, /libraries/kirigami-addons/src/dateandtime/private/DatePicker.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
0002 // SPDX-FileCopyrightText: 2023 Carl Schwan <carl@carlschwan.eu>
0003 // SPDX-License-Identifier: LGPL-2.1-or-later
0004 
0005 import QtQuick 2.15
0006 import QtQuick.Controls 2.15 as QQC2
0007 import QtQuick.Layouts 1.15
0008 import org.kde.kirigami 2.15 as Kirigami
0009 import org.kde.kirigamiaddons.dateandtime 1.0
0010 import org.kde.kirigamiaddons.components 1.0 as Components
0011 import org.kde.kirigamiaddons.delegates 1.0 as Delegates
0012 
0013 QQC2.Control {
0014     id: root
0015 
0016     signal datePicked(date pickedDate)
0017 
0018     property date selectedDate: new Date() // Decides calendar span
0019     readonly property int year: selectedDate.getFullYear()
0020     readonly property int month: selectedDate.getMonth()
0021     readonly property int day: selectedDate.getDate()
0022     property bool showDays: true
0023     property bool showControlHeader: true
0024 
0025     /**
0026      * This property holds the minimum date (inclusive) that the user can select.
0027      *
0028      * By default, no limit is applied to the date selection.
0029      */
0030     property date minimumDate
0031 
0032     /**
0033      * This property holds the maximum date (inclusive) that the user can select.
0034      *
0035      * By default, no limit is applied to the date selection.
0036      */
0037     property date maximumDate
0038 
0039     topPadding: Kirigami.Units.largeSpacing
0040     rightPadding: Kirigami.Units.largeSpacing
0041     bottomPadding: Kirigami.Units.largeSpacing
0042     leftPadding: Kirigami.Units.largeSpacing
0043 
0044     onActiveFocusChanged: if (activeFocus) {
0045         dateSegmentedButton.forceActiveFocus();
0046     }
0047 
0048     property bool _completed: false
0049     property bool _runSetDate: false
0050 
0051     onSelectedDateChanged: if (selectedDate !== null && _completed) {
0052         setToDate(selectedDate)
0053     }
0054 
0055     Component.onCompleted: {
0056         _completed = true;
0057         if (selectedDate) {
0058             setToDate(selectedDate);
0059         }
0060     }
0061     onShowDaysChanged: if (!showDays) pickerView.currentIndex = 1;
0062 
0063     function setToDate(date) {
0064         if (_runSetDate) {
0065             return;
0066         }
0067         _runSetDate = true;
0068 
0069         if (root.minimumDate.valueOf() && date.valueOf() < minimumDate.valueOf()) {
0070             date = minimumDate;
0071         }
0072 
0073         if (root.maximumDate.valueOf() && date.valueOf() > maximumDate.valueOf()) {
0074             date = maximumDate;
0075         }
0076 
0077         const yearDiff = date.getFullYear() - yearPathView.currentItem.startDate.getFullYear();
0078         // 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
0079         // instead of staying within the 2010 decade, which contains a 2020 cell at the very end
0080         const decadeDiff = Math.floor((date.getFullYear() + 1 - decadePathView.currentItem.startDate.getFullYear()) / 12); // 12 years in one decade grid
0081 
0082         let newYearIndex = yearPathView.currentIndex + yearDiff;
0083         let newDecadeIndex = decadePathView.currentIndex + decadeDiff;
0084 
0085         let firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0086         let lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 2,0), InfiniteCalendarViewModel.StartDateRole);
0087         let firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0088         let lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
0089 
0090         if(showDays) { // Set to correct index, including creating new dates in model if needed, for the month view
0091             const monthDiff = date.getMonth() - monthPathView.currentItem.firstDayOfMonth.getMonth() + (12 * (date.getFullYear() - monthPathView.currentItem.firstDayOfMonth.getFullYear()));
0092             let newMonthIndex = monthPathView.currentIndex + monthDiff;
0093             let firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0094             let lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0095 
0096             while(firstMonthItemDate >= date) {
0097                 monthPathView.model.addDates(false)
0098                 firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0099                 newMonthIndex = 0;
0100             }
0101             if(firstMonthItemDate < date && newMonthIndex === 0) {
0102                 newMonthIndex = date.getMonth() - firstMonthItemDate.getMonth() + (12 * (date.getFullYear() - firstMonthItemDate.getFullYear())) + 1;
0103             }
0104 
0105             while(lastMonthItemDate <= date) {
0106                 monthPathView.model.addDates(true)
0107                 lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0108             }
0109 
0110             monthPathView.currentIndex = newMonthIndex;
0111         }
0112 
0113         // Set to index and create dates if needed for year view
0114         while(firstYearItemDate >= date) {
0115             yearPathView.model.addDates(false)
0116             firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0117             newYearIndex = 0;
0118         }
0119         if(firstYearItemDate < date && newYearIndex === 0) {
0120             newYearIndex = date.getFullYear() - firstYearItemDate.getFullYear() + 1;
0121         }
0122 
0123         while(lastYearItemDate <= date) {
0124             yearPathView.model.addDates(true)
0125             lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
0126         }
0127 
0128         // Set to index and create dates if needed for decade view
0129         while(firstDecadeItemDate >= date) {
0130             decadePathView.model.addDates(false)
0131             firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0132             newDecadeIndex = 0;
0133         }
0134         if(firstDecadeItemDate < date && newDecadeIndex === 0) {
0135             newDecadeIndex = date.getFullYear() - firstDecadeItemDate.getFullYear() + 1;
0136         }
0137 
0138         while(lastDecadeItemDate.getFullYear() <= date.getFullYear()) {
0139             decadePathView.model.addDates(true)
0140             lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
0141         }
0142 
0143         yearPathView.currentIndex = newYearIndex;
0144         decadePathView.currentIndex = newDecadeIndex;
0145 
0146         _runSetDate = false;
0147     }
0148 
0149     function goToday() {
0150         selectedDate = new Date()
0151     }
0152 
0153     function prevMonth() {
0154         const newDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() - 1, selectedDate.getDate());
0155         if (root.minimumDate.valueOf() && newDate.valueOf() < minimumDate.valueOf()) {
0156             if (selectedDate == minimumDate) {
0157                 return;
0158             }
0159             selectedDate = minimumDate;
0160         } else {
0161             selectedDate = newDate;
0162         }
0163     }
0164 
0165     function nextMonth() {
0166         const newDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, selectedDate.getDate());
0167         if (root.maximumDate.valueOf() && newDate.valueOf() > maximumDate.valueOf()) {
0168             if (selectedDate == maximumDate) {
0169                 return;
0170             }
0171             selectedDate = maximumDate;
0172             return;
0173         } else {
0174             selectedDate = newDate;
0175         }
0176     }
0177 
0178     function prevYear() {
0179         const newDate = new Date(selectedDate.getFullYear() - 1, selectedDate.getMonth(), selectedDate.getDate())
0180         if (root.minimumDate.valueOf() && newDate.valueOf() < minimumDate.valueOf()) {
0181             if (selectedDate == minimumDate) {
0182                 return;
0183             }
0184             selectedDate = minimumDate;
0185         } else {
0186             selectedDate = newDate;
0187         }
0188     }
0189 
0190     function nextYear() {
0191         const newDate = new Date(selectedDate.getFullYear() + 1, selectedDate.getMonth(), selectedDate.getDate());
0192         if (root.maximumDate && newDate.valueOf() > maximumDate.valueOf()) {
0193             if (selectedDate == maximumDate) {
0194                 return;
0195             }
0196             selectedDate = maximumDate;
0197         } else {
0198             selectedDate = newDate;
0199         }
0200     }
0201 
0202     function prevDecade() {
0203         const newDate = new Date(selectedDate.getFullYear() - 10, selectedDate.getMonth(), selectedDate.getDate());
0204         if (root.minimumDate.valueOf() && newDate.valueOf() < minimumDate.valueOf()) {
0205             if (selectedDate == minimumDate) {
0206                 return;
0207             }
0208             selectedDate = minimumDate;
0209         } else {
0210             selectedDate = newDate;
0211         }
0212     }
0213 
0214     function nextDecade() {
0215         const newDate = new Date(selectedDate.getFullYear() + 10, selectedDate.getMonth(), selectedDate.getDate())
0216         if (root.maximumDate && newDate.valueOf() > maximumDate.valueOf()) {
0217             if (selectedDate == maximumDate) {
0218                 return;
0219             }
0220             selectedDate = maximumDate;
0221         } else {
0222             selectedDate = newDate;
0223         }
0224     }
0225 
0226     contentItem: ColumnLayout {
0227         id: pickerLayout
0228 
0229         RowLayout {
0230             id: headingRow
0231             Layout.fillWidth: true
0232             Layout.bottomMargin: Kirigami.Units.smallSpacing
0233 
0234             Components.SegmentedButton {
0235                 id: dateSegmentedButton
0236 
0237                 actions: [
0238                     Kirigami.Action {
0239                         text: root.selectedDate.getDate()
0240                         onTriggered: pickerView.currentIndex = 0 // dayGrid is first item in pickerView
0241                         checked: pickerView.currentIndex === 0
0242                     },
0243                     Kirigami.Action {
0244                         text: root.selectedDate.toLocaleDateString(Qt.locale(), "MMMM")
0245                         onTriggered: pickerView.currentIndex = 1
0246                         checked: pickerView.currentIndex === 1
0247                     },
0248                     Kirigami.Action {
0249                         id: yearsViewCheck
0250                         text: root.selectedDate.getFullYear()
0251                         onTriggered: pickerView.currentIndex = 2
0252                         checked: pickerView.currentIndex === 2
0253                     }
0254                 ]
0255             }
0256 
0257             Item {
0258                 Layout.fillWidth: true
0259             }
0260 
0261             Components.SegmentedButton {
0262                 actions: [
0263                     Kirigami.Action {
0264                         id: goPreviousAction
0265                         icon.name: 'go-previous-view'
0266                         text: i18ndc("kirigami-addons6", "@action:button", "Go Previous")
0267                         displayHint: Kirigami.DisplayHint.IconOnly
0268                         onTriggered: {
0269                             if (pickerView.currentIndex === 1) { // monthGrid index
0270                                 prevYear();
0271                             } else if (pickerView.currentIndex === 2) { // yearGrid index
0272                                 prevDecade();
0273                             } else { // dayGrid index
0274                                 prevMonth();
0275                             }
0276                         }
0277                     },
0278                     Kirigami.Action {
0279                         text: i18ndc("kirigami-addons6", "@action:button", "Jump to today")
0280                         displayHint: Kirigami.DisplayHint.IconOnly
0281                         icon.name: 'go-jump-today'
0282                         onTriggered: goToday()
0283                     },
0284                     Kirigami.Action {
0285                         id: goNextAction
0286                         text: i18ndc("kirigami-addons6", "@action:button", "Go Next")
0287                         icon.name: 'go-next-view'
0288                         displayHint: Kirigami.DisplayHint.IconOnly
0289                         onTriggered: {
0290                             if (pickerView.currentIndex === 1) { // monthGrid index
0291                                 nextYear();
0292                             } else if (pickerView.currentIndex === 2) { // yearGrid index
0293                                 nextDecade();
0294                             } else { // dayGrid index
0295                                 nextMonth();
0296                             }
0297                         }
0298                     }
0299                 ]
0300             }
0301         }
0302 
0303         QQC2.SwipeView {
0304             id: pickerView
0305 
0306             clip: true
0307             interactive: false
0308             padding: 0
0309 
0310             Layout.fillWidth: true
0311             Layout.fillHeight: true
0312 
0313             DatePathView {
0314                 id: monthPathView
0315 
0316                 mainView: pickerView
0317 
0318                 model: InfiniteCalendarViewModel {
0319                     scale: InfiniteCalendarViewModel.MonthScale
0320                     currentDate: root.selectedDate
0321                     minimumDate: root.minimumDate
0322                     maximumDate: root.maximumDate
0323                     datesToAdd: 10
0324                 }
0325 
0326                 delegate: Loader {
0327                     id: monthViewLoader
0328                     property date firstDayOfMonth: model.firstDay
0329                     property bool isNextOrCurrentItem: index >= monthPathView.currentIndex -1 && index <= monthPathView.currentIndex + 1
0330 
0331                     active: isNextOrCurrentItem && root.showDays
0332 
0333                     sourceComponent: GridLayout {
0334                         id: dayGrid
0335                         columns: 7
0336                         rows: 7
0337                         width: monthPathView.width
0338                         height: monthPathView.height
0339                         Layout.topMargin: Kirigami.Units.smallSpacing
0340 
0341                         property var modelLoader: Loader {
0342                             asynchronous: true
0343                             sourceComponent: MonthModel {
0344                                 year: firstDay.getFullYear()
0345                                 month: firstDay.getMonth() + 1 // From pathview model
0346                             }
0347                         }
0348 
0349                         QQC2.ButtonGroup {
0350                             buttons: dayGrid.children
0351                         }
0352 
0353                         Repeater {
0354                             model: dayGrid.modelLoader.item.weekDays
0355                             delegate: QQC2.Label {
0356                                 Layout.fillWidth: true
0357                                 Layout.fillHeight: true
0358                                 horizontalAlignment: Text.AlignHCenter
0359                                 rightPadding: Kirigami.Units.mediumSpacing
0360                                 leftPadding: Kirigami.Units.mediumSpacing
0361                                 opacity: 0.7
0362                                 text: modelData
0363                             }
0364                         }
0365 
0366                         Repeater {
0367                             id: dayRepeater
0368 
0369                             model: dayGrid.modelLoader.item
0370 
0371                             delegate: DatePickerDelegate {
0372                                 id: dayDelegate
0373 
0374                                 required property bool isToday
0375                                 required property bool sameMonth
0376                                 required property int dayNumber
0377 
0378                                 repeater: dayRepeater
0379                                 minimumDate: root.minimumDate
0380                                 maximumDate: root.maximumDate
0381                                 previousAction: goPreviousAction
0382                                 nextAction: goNextAction
0383 
0384                                 horizontalPadding: 0
0385 
0386                                 Accessible.name: if (dayNumber === 1 || index === 0) {
0387                                     date.toLocaleDateString(locale, Locale.ShortFormat)
0388                                 } else {
0389                                     dayNumber
0390                                 }
0391 
0392                                 background {
0393                                     visible: sameMonth
0394                                 }
0395 
0396                                 highlighted: isToday
0397                                 checkable: true
0398                                 checked: date.getDate() === selectedDate.getDate() &&
0399                                     date.getMonth() === selectedDate.getMonth() &&
0400                                     date.getFullYear() === selectedDate.getFullYear()
0401                                 opacity: sameMonth && inScope ? 1 : 0.6
0402                                 text: dayNumber
0403                                 onClicked: {
0404                                     selectedDate = date;
0405                                     selectedDate = date;
0406                                     datePicked(date);
0407                                 }
0408                             }
0409                         }
0410                     }
0411                 }
0412 
0413                 onCurrentIndexChanged: {
0414                     if (pickerView.currentIndex === 0) {
0415                         root.selectedDate = new Date(currentItem.firstDayOfMonth.getFullYear(), currentItem.firstDayOfMonth.getMonth(), root.selectedDate.getDate());
0416                     }
0417 
0418                     if (currentIndex >= count - 2) {
0419                         model.addDates(true);
0420                     } else if (currentIndex <= 1) {
0421                         model.addDates(false);
0422                         startIndex += model.datesToAdd;
0423                     }
0424                 }
0425             }
0426 
0427             DatePathView {
0428                 id: yearPathView
0429 
0430                 mainView: pickerView
0431 
0432                 model: InfiniteCalendarViewModel {
0433                     scale: InfiniteCalendarViewModel.YearScale
0434                     currentDate: root.selectedDate
0435                 }
0436 
0437                 delegate: Loader {
0438                     id: yearViewLoader
0439 
0440                     required property int index
0441                     required property date startDate
0442 
0443                     property bool isNextOrCurrentItem: index >= yearPathView.currentIndex -1 && index <= yearPathView.currentIndex + 1
0444 
0445                     width: parent.width
0446                     height: parent.height
0447 
0448                     active: isNextOrCurrentItem
0449 
0450                     sourceComponent: GridLayout {
0451                         id: yearGrid
0452                         columns: 3
0453                         rows: 4
0454 
0455                         QQC2.ButtonGroup {
0456                             buttons: yearGrid.children
0457                         }
0458 
0459                         Repeater {
0460                             id: monthRepeater
0461 
0462                             model: yearGrid.columns * yearGrid.rows
0463 
0464                             delegate: DatePickerDelegate {
0465                                 id: monthDelegate
0466 
0467                                 date: new Date(yearViewLoader.startDate.getFullYear(), index)
0468 
0469                                 minimumDate: root.minimumDate.valueOf() ? new Date(root.minimumDate).setDate(0) : new Date("invalid")
0470                                 maximumDate: root.maximumDate.valueOf() ? new Date(root.maximumDate.getFullYear(), root.maximumDate.getMonth() + 1, 0) : new Date("invalid")
0471                                 repeater: monthRepeater
0472                                 previousAction: goPreviousAction
0473                                 nextAction: goNextAction
0474 
0475                                 horizontalPadding: padding * 2
0476                                 rightPadding: undefined
0477                                 leftPadding: undefined
0478                                 highlighted: date.getMonth() === new Date().getMonth() &&
0479                                     date.getFullYear() === new Date().getFullYear()
0480                                 checkable: true
0481                                 checked: date.getMonth() === selectedDate.getMonth() &&
0482                                     date.getFullYear() === selectedDate.getFullYear()
0483                                 text: Qt.locale().standaloneMonthName(date.getMonth())
0484                                 onClicked: {
0485                                     selectedDate = new Date(date);
0486                                     root.datePicked(date);
0487                                     if(root.showDays) pickerView.currentIndex = 0;
0488                                 }
0489                             }
0490                         }
0491                     }
0492                 }
0493 
0494                 onCurrentIndexChanged: {
0495                     if (pickerView.currentIndex === 1) {
0496                         root.selectedDate = new Date(currentItem.startDate.getFullYear(), root.selectedDate.getMonth(), root.selectedDate.getDate());
0497                     }
0498 
0499                     if (currentIndex >= count - 2) {
0500                         model.addDates(true);
0501                     } else if (currentIndex <= 1) {
0502                         model.addDates(false);
0503                         startIndex += model.datesToAdd;
0504                     }
0505                 }
0506 
0507             }
0508 
0509             DatePathView {
0510                 id: decadePathView
0511 
0512                 mainView: pickerView
0513 
0514                 model: InfiniteCalendarViewModel {
0515                     scale: InfiniteCalendarViewModel.DecadeScale
0516                     currentDate: root.selectedDate
0517                 }
0518 
0519                 delegate: Loader {
0520                     id: decadeViewLoader
0521 
0522                     required property int index
0523                     required property date startDate
0524 
0525                     property bool isNextOrCurrentItem: index >= decadePathView.currentIndex -1 && index <= decadePathView.currentIndex + 1
0526 
0527                     width: parent.width
0528                     height: parent.height
0529 
0530                     active: isNextOrCurrentItem
0531 
0532                     sourceComponent: GridLayout {
0533                         id: decadeGrid
0534 
0535                         columns: 3
0536                         rows: 4
0537 
0538                         QQC2.ButtonGroup {
0539                             buttons: decadeGrid.children
0540                         }
0541 
0542                         Repeater {
0543                             id: decadeRepeater
0544 
0545                             model: decadeGrid.columns * decadeGrid.rows
0546 
0547                             delegate: DatePickerDelegate {
0548                                 id: yearDelegate
0549 
0550                                 readonly property bool sameDecade: Math.floor(date.getFullYear() / 10) == Math.floor(year / 10)
0551 
0552                                 date: new Date(startDate.getFullYear() + index, 0)
0553                                 minimumDate: root.minimumDate.valueOf() ? new Date(root.minimumDate.getFullYear(), 0, 0) : new Date("invalid")
0554                                 maximumDate: root.maximumDate.valueOf() ? new Date(root.maximumDate.getFullYear(), 12, 0) : new Date("invalid")
0555                                 repeater: decadeRepeater
0556                                 previousAction: goPreviousAction
0557                                 nextAction: goNextAction
0558 
0559                                 highlighted: date.getFullYear() === new Date().getFullYear()
0560 
0561                                 horizontalPadding: padding * 2
0562                                 rightPadding: undefined
0563                                 leftPadding: undefined
0564                                 checkable: true
0565                                 checked: date.getFullYear() === selectedDate.getFullYear()
0566                                 opacity: sameDecade ? 1 : 0.7
0567                                 text: date.getFullYear()
0568                                 onClicked: {
0569                                     selectedDate = new Date(date);
0570                                     root.datePicked(date);
0571                                     pickerView.currentIndex = 1;
0572                                 }
0573                             }
0574                         }
0575                     }
0576                 }
0577 
0578                 onCurrentIndexChanged: {
0579                     if (pickerView.currentIndex === 2) {
0580                         // getFullYear + 1 because the startDate is e.g. 2019, but we want the 2020 decade to be selected
0581                         root.selectedDate = new Date(currentItem.startDate.getFullYear() + 1, root.selectedDate.getMonth(), root.selectedDate.getDate());
0582                     }
0583 
0584                     if (currentIndex >= count - 2) {
0585                         model.addDates(true);
0586                     } else if (currentIndex <= 1) {
0587                         model.addDates(false);
0588                         startIndex += model.datesToAdd;
0589                     }
0590                 }
0591 
0592             }
0593         }
0594     }
0595 }