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

0001 // SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
0002 // SPDX-License-Identifier: LGPL-2.1-or-later
0003 
0004 import QtQuick 2.15
0005 import QtQuick.Controls 2.15 as QQC2
0006 import QtQuick.Layouts 1.15
0007 import org.kde.kirigami 2.15 as Kirigami
0008 import org.kde.kirigamiaddons.dateandtime 0.1
0009 
0010 QQC2.Control {
0011     id: datepicker
0012 
0013     signal datePicked(date pickedDate)
0014 
0015     property date selectedDate: new Date() // Decides calendar span
0016     property date clickedDate: new Date() // User's chosen date
0017     property int year: selectedDate.getFullYear()
0018     property int month: selectedDate.getMonth()
0019     property int day: selectedDate.getDate()
0020     property bool showDays: true
0021     property bool showControlHeader: true
0022 
0023     topPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0024     rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0025     bottomPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0026     leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0027 
0028     onSelectedDateChanged: setToDate(selectedDate)
0029     onShowDaysChanged: if (!showDays) pickerView.currentIndex = 1;
0030 
0031     function setToDate(date) {
0032         const yearDiff = date.getFullYear() - yearPathView.currentItem.startDate.getFullYear();
0033         // 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
0034         // instead of staying within the 2010 decade, which contains a 2020 cell at the very end
0035         const decadeDiff = Math.floor((date.getFullYear() + 1 - decadePathView.currentItem.startDate.getFullYear()) / 12); // 12 years in one decade grid
0036 
0037         let newYearIndex = yearPathView.currentIndex + yearDiff;
0038         let newDecadeIndex = decadePathView.currentIndex + decadeDiff;
0039 
0040         let firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0041         let lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 2,0), InfiniteCalendarViewModel.StartDateRole);
0042         let firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0043         let lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
0044 
0045         if(showDays) { // Set to correct index, including creating new dates in model if needed, for the month view
0046             const monthDiff = date.getMonth() - monthPathView.currentItem.firstDayOfMonth.getMonth() + (12 * (date.getFullYear() - monthPathView.currentItem.firstDayOfMonth.getFullYear()));
0047             let newMonthIndex = monthPathView.currentIndex + monthDiff;
0048             let firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0049             let lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0050 
0051             while(firstMonthItemDate >= date) {
0052                 monthPathView.model.addDates(false)
0053                 firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0054                 newMonthIndex = 0;
0055             }
0056             if(firstMonthItemDate < date && newMonthIndex === 0) {
0057                 newMonthIndex = date.getMonth() - firstMonthItemDate.getMonth() + (12 * (date.getFullYear() - firstMonthItemDate.getFullYear())) + 1;
0058             }
0059 
0060             while(lastMonthItemDate <= date) {
0061                 monthPathView.model.addDates(true)
0062                 lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.FirstDayOfMonthRole);
0063             }
0064 
0065             monthPathView.currentIndex = newMonthIndex;
0066         }
0067 
0068         // Set to index and create dates if needed for year view
0069         while(firstYearItemDate >= date) {
0070             yearPathView.model.addDates(false)
0071             firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0072             newYearIndex = 0;
0073         }
0074         if(firstYearItemDate < date && newYearIndex === 0) {
0075             newYearIndex = date.getFullYear() - firstYearItemDate.getFullYear() + 1;
0076         }
0077 
0078         while(lastYearItemDate <= date) {
0079             yearPathView.model.addDates(true)
0080             lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
0081         }
0082 
0083         // Set to index and create dates if needed for decade view
0084         while(firstDecadeItemDate >= date) {
0085             decadePathView.model.addDates(false)
0086             firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), InfiniteCalendarViewModel.StartDateRole);
0087             newDecadeIndex = 0;
0088         }
0089         if(firstDecadeItemDate < date && newDecadeIndex === 0) {
0090             newDecadeIndex = date.getFullYear() - firstDecadeItemDate.getFullYear() + 1;
0091         }
0092 
0093         while(lastDecadeItemDate.getFullYear() <= date.getFullYear()) {
0094             decadePathView.model.addDates(true)
0095             lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), InfiniteCalendarViewModel.StartDateRole);
0096         }
0097 
0098         yearPathView.currentIndex = newYearIndex;
0099         decadePathView.currentIndex = newDecadeIndex;
0100     }
0101 
0102     function goToday() {
0103         selectedDate = new Date()
0104     }
0105 
0106     function prevMonth() {
0107         selectedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() - 1, selectedDate.getDate())
0108     }
0109 
0110     function nextMonth() {
0111         selectedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, selectedDate.getDate())
0112     }
0113 
0114     function prevYear() {
0115         selectedDate = new Date(selectedDate.getFullYear() - 1, selectedDate.getMonth(), selectedDate.getDate())
0116     }
0117 
0118     function nextYear() {
0119         selectedDate = new Date(selectedDate.getFullYear() + 1, selectedDate.getMonth(), selectedDate.getDate())
0120     }
0121 
0122     function prevDecade() {
0123         selectedDate = new Date(selectedDate.getFullYear() - 10, selectedDate.getMonth(), selectedDate.getDate())
0124     }
0125 
0126     function nextDecade() {
0127         selectedDate = new Date(selectedDate.getFullYear() + 10, selectedDate.getMonth(), selectedDate.getDate())
0128     }
0129 
0130     contentItem: ColumnLayout {
0131         id: pickerLayout
0132 
0133         RowLayout {
0134             id: headingRow
0135             Layout.fillWidth: true
0136             visible: datepicker.showControlHeader
0137 
0138             Kirigami.Heading {
0139                 id: monthLabel
0140                 Layout.fillWidth: true
0141                 text: i18ndc("kirigami-addons", "%1 is month name, %2 is year", "%1 %2", Qt.locale().standaloneMonthName(selectedDate.getMonth()), String(selectedDate.getFullYear()))
0142                 level: 1
0143             }
0144             QQC2.ToolButton {
0145                 icon.name: 'go-previous-view'
0146                 onClicked: {
0147                     if (pickerView.currentIndex == 1) { // monthGrid index
0148                         prevYear()
0149                     } else if (pickerView.currentIndex == 2) { // yearGrid index
0150                         prevDecade()
0151                     } else { // dayGrid index
0152                         prevMonth()
0153                     }
0154                 }
0155             }
0156             QQC2.ToolButton {
0157                 icon.name: 'go-jump-today'
0158                 onClicked: goToday()
0159             }
0160             QQC2.ToolButton {
0161                 icon.name: 'go-next-view'
0162                 onClicked: {
0163                     if (pickerView.currentIndex == 1) { // monthGrid index
0164                         nextYear()
0165                     } else if (pickerView.currentIndex == 2) { // yearGrid index
0166                         nextDecade()
0167                     } else { // dayGrid index
0168                         nextMonth()
0169                     }
0170                 }
0171             }
0172         }
0173 
0174         QQC2.TabBar {
0175             id: rangeBar
0176             currentIndex: pickerView.currentIndex
0177             Layout.fillWidth: true
0178 
0179             QQC2.TabButton {
0180                 id: daysViewCheck
0181                 Layout.fillWidth: true
0182                 text: i18ndc("kirigami-addons", "@title:tab", "Days")
0183                 onClicked: pickerView.currentIndex = 0 // dayGrid is first item in pickerView
0184                 visible: datepicker.showDays
0185                 width: visible ? implicitWidth : 0
0186             }
0187             QQC2.TabButton {
0188                 id: monthsViewCheck
0189                 Layout.fillWidth: true
0190                 text: i18ndc("kirigami-addons", "@title:tab", "Months")
0191                 onClicked: pickerView.currentIndex = 1
0192             }
0193             QQC2.TabButton {
0194                 id: yearsViewCheck
0195                 Layout.fillWidth: true
0196                 text: i18ndc("kirigami-addons", "@title:tab", "Years")
0197                 onClicked: pickerView.currentIndex = 2
0198             }
0199         }
0200         Kirigami.Separator {
0201             Layout.topMargin: (-pickerLayout.spacing *2) - 1
0202             Layout.fillWidth: true
0203         }
0204 
0205         QQC2.SwipeView {
0206             id: pickerView
0207             Layout.fillWidth: true
0208             Layout.fillHeight: true
0209             clip: true
0210             interactive: false
0211 
0212             PathView {
0213                 id: monthPathView
0214 
0215                 Layout.fillWidth: true
0216                 Layout.fillHeight: true
0217                 implicitHeight: Kirigami.Units.gridUnit * 16
0218                 flickDeceleration: Kirigami.Units.longDuration
0219                 preferredHighlightBegin: 0.5
0220                 preferredHighlightEnd: 0.5
0221                 snapMode: PathView.SnapToItem
0222                 focus: true
0223                 interactive: Kirigami.Settings.tabletMode
0224                 clip: true
0225 
0226                 path: Path {
0227                     startX: - monthPathView.width * monthPathView.count / 2 + monthPathView.width / 2
0228                     startY: monthPathView.height / 2
0229                     PathLine {
0230                         x: monthPathView.width * monthPathView.count / 2 + monthPathView.width / 2
0231                         y: monthPathView.height / 2
0232                     }
0233                 }
0234 
0235                 model: InfiniteCalendarViewModel {
0236                     scale: InfiniteCalendarViewModel.MonthScale
0237                     datesToAdd: 300
0238                 }
0239 
0240                 property int startIndex
0241                 Component.onCompleted: {
0242                     startIndex = count / 2;
0243                     currentIndex = startIndex;
0244                 }
0245                 onCurrentIndexChanged: {
0246                     if(pickerView.currentIndex == 0) {
0247                         datepicker.selectedDate = new Date(currentItem.firstDayOfMonth.getFullYear(), currentItem.firstDayOfMonth.getMonth(), datepicker.selectedDate.getDate());
0248                     }
0249 
0250                     if(currentIndex >= count - 2) {
0251                         model.addDates(true);
0252                     } else if (currentIndex <= 1) {
0253                         model.addDates(false);
0254                         startIndex += model.datesToAdd;
0255                     }
0256                 }
0257 
0258                 delegate: Loader {
0259                     id: monthViewLoader
0260                     property date firstDayOfMonth: model.firstDay
0261                     property bool isNextOrCurrentItem: index >= monthPathView.currentIndex -1 && index <= monthPathView.currentIndex + 1
0262 
0263                     active: isNextOrCurrentItem && datepicker.showDays
0264 
0265                     sourceComponent: GridLayout {
0266                         id: dayGrid
0267                         columns: 7
0268                         rows: 7
0269                         width: monthPathView.width
0270                         height: monthPathView.height
0271                         Layout.topMargin: Kirigami.Units.smallSpacing
0272 
0273                         property var modelLoader: Loader {
0274                             asynchronous: true
0275                             sourceComponent: MonthModel {
0276                                 year: firstDay.getFullYear()
0277                                 month: firstDay.getMonth() + 1 // From pathview model
0278                             }
0279                         }
0280 
0281                         QQC2.ButtonGroup {
0282                             buttons: dayGrid.children
0283                         }
0284 
0285                         Repeater {
0286                             model: dayGrid.modelLoader.item.weekDays
0287                             delegate: QQC2.Label {
0288                                 Layout.fillWidth: true
0289                                 Layout.fillHeight: true
0290                                 horizontalAlignment: Text.AlignHCenter
0291                                 opacity: 0.7
0292                                 text: modelData
0293                             }
0294                         }
0295 
0296                         Repeater {
0297                             model: dayGrid.modelLoader.item
0298 
0299                             delegate: QQC2.Button {
0300                                 Layout.fillWidth: true
0301                                 Layout.fillHeight: true
0302                                 flat: true
0303                                 highlighted: model.isToday
0304                                 checkable: true
0305                                 checked: date.getDate() === clickedDate.getDate() &&
0306                                     date.getMonth() === clickedDate.getMonth() &&
0307                                     date.getFullYear() === clickedDate.getFullYear()
0308                                 opacity: sameMonth ? 1 : 0.7
0309                                 text: model.dayNumber
0310                                 onClicked: {
0311                                     clickedDate = model.date;
0312                                     selectedDate = model.date;
0313                                     datePicked(model.date);
0314                                 }
0315                             }
0316                         }
0317                     }
0318                 }
0319             }
0320 
0321             PathView {
0322                 id: yearPathView
0323 
0324                 Layout.fillWidth: true
0325                 Layout.fillHeight: true
0326                 implicitHeight: Kirigami.Units.gridUnit * 9
0327                 flickDeceleration: Kirigami.Units.longDuration
0328                 preferredHighlightBegin: 0.5
0329                 preferredHighlightEnd: 0.5
0330                 snapMode: PathView.SnapToItem
0331                 focus: true
0332                 interactive: Kirigami.Settings.tabletMode
0333                 clip: true
0334 
0335                 path: Path {
0336                     startX: - yearPathView.width * yearPathView.count / 2 + yearPathView.width / 2
0337                     startY: yearPathView.height / 2
0338                     PathLine {
0339                         x: yearPathView.width * yearPathView.count / 2 + yearPathView.width / 2
0340                         y: yearPathView.height / 2
0341                     }
0342                 }
0343 
0344                 model: InfiniteCalendarViewModel {
0345                     scale: InfiniteCalendarViewModel.YearScale
0346                 }
0347 
0348                 property int startIndex
0349                 Component.onCompleted: {
0350                     startIndex = count / 2;
0351                     currentIndex = startIndex;
0352                 }
0353                 onCurrentIndexChanged: {
0354                     if(pickerView.currentIndex == 1) {
0355                         datepicker.selectedDate = new Date(currentItem.startDate.getFullYear(), datepicker.selectedDate.getMonth(), datepicker.selectedDate.getDate())
0356                     }
0357 
0358                     if(currentIndex >= count - 2) {
0359                         model.addDates(true);
0360                     } else if (currentIndex <= 1) {
0361                         model.addDates(false);
0362                         startIndex += model.datesToAdd;
0363                     }
0364                 }
0365 
0366                 delegate: Loader {
0367                     id: yearViewLoader
0368                     property date startDate: model.startDate
0369                     property bool isNextOrCurrentItem: index >= yearPathView.currentIndex -1 && index <= yearPathView.currentIndex + 1
0370 
0371                     active: isNextOrCurrentItem
0372 
0373                     sourceComponent: GridLayout {
0374                         id: yearGrid
0375                         columns: 3
0376                         rows: 4
0377                         Layout.fillWidth: true
0378                         Layout.fillHeight: true
0379                         Layout.topMargin: Kirigami.Units.smallSpacing
0380 
0381                         QQC2.ButtonGroup {
0382                             buttons: yearGrid.children
0383                         }
0384 
0385                         Repeater {
0386                             model: yearGrid.columns * yearGrid.rows
0387                             delegate: QQC2.Button {
0388                                 property date date: new Date(startDate.getFullYear(), index)
0389                                 Layout.fillWidth: true
0390                                 Layout.fillHeight: true
0391                                 flat: true
0392                                 highlighted: date.getMonth() === new Date().getMonth() &&
0393                                     date.getFullYear() === new Date().getFullYear()
0394                                 checkable: true
0395                                 checked: date.getMonth() === clickedDate.getMonth() &&
0396                                     date.getFullYear() === clickedDate.getFullYear()
0397                                 text: Qt.locale().standaloneMonthName(date.getMonth())
0398                                 onClicked: {
0399                                     selectedDate = new Date(date);
0400                                     clickedDate = new Date(date);
0401                                     datepicker.datePicked(date);
0402                                     if(datepicker.showDays) pickerView.currentIndex = 0;
0403                                 }
0404                             }
0405                         }
0406                     }
0407                 }
0408             }
0409 
0410             PathView {
0411                 id: decadePathView
0412 
0413                 Layout.fillWidth: true
0414                 Layout.fillHeight: true
0415                 implicitHeight: Kirigami.Units.gridUnit * 9
0416                 flickDeceleration: Kirigami.Units.longDuration
0417                 preferredHighlightBegin: 0.5
0418                 preferredHighlightEnd: 0.5
0419                 snapMode: PathView.SnapToItem
0420                 focus: true
0421                 interactive: Kirigami.Settings.tabletMode
0422                 clip: true
0423 
0424                 path: Path {
0425                     startX: - decadePathView.width * decadePathView.count / 2 + decadePathView.width / 2
0426                     startY: decadePathView.height / 2
0427                     PathLine {
0428                         x: decadePathView.width * decadePathView.count / 2 + decadePathView.width / 2
0429                         y: decadePathView.height / 2
0430                     }
0431                 }
0432 
0433                 model: InfiniteCalendarViewModel {
0434                     scale: InfiniteCalendarViewModel.DecadeScale
0435                 }
0436 
0437                 property int startIndex
0438                 Component.onCompleted: {
0439                     startIndex = count / 2;
0440                     currentIndex = startIndex;
0441                 }
0442                 onCurrentIndexChanged: {
0443                     if(pickerView.currentIndex == 2) {
0444                         // getFullYear + 1 because the startDate is e.g. 2019, but we want the 2020 decade to be selected
0445                         datepicker.selectedDate = new Date(currentItem.startDate.getFullYear() + 1, datepicker.selectedDate.getMonth(), datepicker.selectedDate.getDate())
0446                     }
0447 
0448                     if(currentIndex >= count - 2) {
0449                         model.addDates(true);
0450                     } else if (currentIndex <= 1) {
0451                         model.addDates(false);
0452                         startIndex += model.datesToAdd;
0453                     }
0454                 }
0455 
0456                 delegate: Loader {
0457                     id: decadeViewLoader
0458                     property date startDate: model.startDate
0459                     property bool isNextOrCurrentItem: index >= decadePathView.currentIndex -1 && index <= decadePathView.currentIndex + 1
0460 
0461                     active: isNextOrCurrentItem
0462 
0463                     sourceComponent: GridLayout {
0464                         id: decadeGrid
0465                         columns: 3
0466                         rows: 4
0467                         Layout.fillWidth: true
0468                         Layout.fillHeight: true
0469                         Layout.topMargin: Kirigami.Units.smallSpacing
0470 
0471                         QQC2.ButtonGroup {
0472                             buttons: decadeGrid.children
0473                         }
0474 
0475                         Repeater {
0476                             model: decadeGrid.columns * decadeGrid.rows
0477                             delegate: QQC2.Button {
0478                                 property date date: new Date(startDate.getFullYear() + index, 0)
0479                                 property bool sameDecade: Math.floor(date.getFullYear() / 10) == Math.floor(year / 10)
0480                                 Layout.fillWidth: true
0481                                 Layout.fillHeight: true
0482                                 flat: true
0483                                 highlighted: date.getFullYear() === new Date().getFullYear()
0484                                 checkable: true
0485                                 checked: date.getFullYear() === clickedDate.getFullYear()
0486                                 opacity: sameDecade ? 1 : 0.7
0487                                 text: date.getFullYear()
0488                                 onClicked: {
0489                                     selectedDate = new Date(date);
0490                                     clickedDate = new Date(date);
0491                                     datepicker.datePicked(date);
0492                                     pickerView.currentIndex = 1;
0493                                 }
0494                             }
0495                         }
0496                     }
0497                 }
0498             }
0499         }
0500     }
0501 }