Warning, /pim/merkuro/src/calendar/qml/Controls/DateControls/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.merkuro.calendar 1.0 as Calendar
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 date today: new Date()
0018     property int year: selectedDate.getFullYear()
0019     property int month: selectedDate.getMonth()
0020     property int day: selectedDate.getDate()
0021     property bool showDays: true
0022     property bool showControlHeader: true
0023 
0024     topPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0025     rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0026     bottomPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0027     leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0028 
0029     onSelectedDateChanged: setToDate(selectedDate)
0030     onShowDaysChanged: if (!showDays) pickerView.currentIndex = 1;
0031 
0032     function setToDate(date) {
0033         const yearDiff = date.getFullYear() - yearPathView.currentItem.startDate.getFullYear();
0034         // 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
0035         // instead of staying within the 2010 decade, which contains a 2020 cell at the very end
0036         const decadeDiff = Math.floor((date.getFullYear() + 1 - decadePathView.currentItem.startDate.getFullYear()) / 12); // 12 years in one decade grid
0037 
0038         let newYearIndex = yearPathView.currentIndex + yearDiff;
0039         let newDecadeIndex = decadePathView.currentIndex + decadeDiff;
0040 
0041         let firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0042         let lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 2,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0043         let firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0044         let lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0045 
0046         if(showDays) { // Set to correct index, including creating new dates in model if needed, for the month view
0047             const monthDiff = date.getMonth() - monthPathView.currentItem.firstDayOfMonth.getMonth() + (12 * (date.getFullYear() - monthPathView.currentItem.firstDayOfMonth.getFullYear()));
0048             let newMonthIndex = monthPathView.currentIndex + monthDiff;
0049             let firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), Calendar.InfiniteCalendarViewModel.FirstDayOfMonthRole);
0050             let lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), Calendar.InfiniteCalendarViewModel.FirstDayOfMonthRole);
0051 
0052             while(firstMonthItemDate >= date) {
0053                 monthPathView.model.addDates(false)
0054                 firstMonthItemDate = monthPathView.model.data(monthPathView.model.index(1,0), Calendar.InfiniteCalendarViewModel.FirstDayOfMonthRole);
0055                 newMonthIndex = 0;
0056             }
0057             if(firstMonthItemDate < date && newMonthIndex === 0) {
0058                 newMonthIndex = date.getMonth() - firstMonthItemDate.getMonth() + (12 * (date.getFullYear() - firstMonthItemDate.getFullYear())) + 1;
0059             }
0060 
0061             while(lastMonthItemDate <= date) {
0062                 monthPathView.model.addDates(true)
0063                 lastMonthItemDate = monthPathView.model.data(monthPathView.model.index(monthPathView.model.rowCount() - 1,0), Calendar.InfiniteCalendarViewModel.FirstDayOfMonthRole);
0064             }
0065 
0066             monthPathView.currentIndex = newMonthIndex;
0067         }
0068 
0069         // Set to index and create dates if needed for year view
0070         while(firstYearItemDate >= date) {
0071             yearPathView.model.addDates(false)
0072             firstYearItemDate = yearPathView.model.data(yearPathView.model.index(1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0073             newYearIndex = 0;
0074         }
0075         if(firstYearItemDate < date && newYearIndex === 0) {
0076             newYearIndex = date.getFullYear() - firstYearItemDate.getFullYear() + 1;
0077         }
0078 
0079         while(lastYearItemDate <= date) {
0080             yearPathView.model.addDates(true)
0081             lastYearItemDate = yearPathView.model.data(yearPathView.model.index(yearPathView.model.rowCount() - 1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0082         }
0083 
0084         // Set to index and create dates if needed for decade view
0085         while(firstDecadeItemDate >= date) {
0086             decadePathView.model.addDates(false)
0087             firstDecadeItemDate = decadePathView.model.data(decadePathView.model.index(1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0088             newDecadeIndex = 0;
0089         }
0090         if(firstDecadeItemDate < date && newDecadeIndex === 0) {
0091             newDecadeIndex = date.getFullYear() - firstDecadeItemDate.getFullYear() + 1;
0092         }
0093 
0094         while(lastDecadeItemDate.getFullYear() <= date.getFullYear()) {
0095             decadePathView.model.addDates(true)
0096             lastDecadeItemDate = decadePathView.model.data(decadePathView.model.index(decadePathView.model.rowCount() - 1,0), Calendar.InfiniteCalendarViewModel.StartDateRole);
0097         }
0098 
0099         yearPathView.currentIndex = newYearIndex;
0100         decadePathView.currentIndex = newDecadeIndex;
0101     }
0102 
0103     function prevMonth() {
0104         selectedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() - 1, selectedDate.getDate())
0105     }
0106 
0107     function nextMonth() {
0108         selectedDate = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, selectedDate.getDate())
0109     }
0110 
0111     function prevYear() {
0112         selectedDate = new Date(selectedDate.getFullYear() - 1, selectedDate.getMonth(), selectedDate.getDate())
0113     }
0114 
0115     function nextYear() {
0116         selectedDate = new Date(selectedDate.getFullYear() + 1, selectedDate.getMonth(), selectedDate.getDate())
0117     }
0118 
0119     function prevDecade() {
0120         selectedDate = new Date(selectedDate.getFullYear() - 10, selectedDate.getMonth(), selectedDate.getDate())
0121     }
0122 
0123     function nextDecade() {
0124         selectedDate = new Date(selectedDate.getFullYear() + 10, selectedDate.getMonth(), selectedDate.getDate())
0125     }
0126 
0127     contentItem: ColumnLayout {
0128         id: pickerLayout
0129 
0130         RowLayout {
0131             id: headingRow
0132             Layout.fillWidth: true
0133             visible: datepicker.showControlHeader
0134 
0135             Kirigami.Heading {
0136                 id: monthLabel
0137                 Layout.fillWidth: true
0138                 text: i18nc("%1 is month name, %2 is year", "%1 %2", Qt.locale().standaloneMonthName(selectedDate.getMonth()), String(selectedDate.getFullYear()))
0139                 level: 1
0140             }
0141             QQC2.ToolButton {
0142                 icon.name: 'go-previous-view'
0143                 onClicked: {
0144                     if (pickerView.currentIndex == 1) { // monthGrid index
0145                         prevYear()
0146                     } else if (pickerView.currentIndex == 2) { // yearGrid index
0147                         prevDecade()
0148                     } else { // dayGrid index
0149                         prevMonth()
0150                     }
0151                 }
0152             }
0153             QQC2.ToolButton {
0154                 icon.name: 'go-jump-today'
0155                 onClicked: {
0156                     selectedDate = today;
0157                     clickedDate = today;
0158                 }
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: i18n("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: i18n("Months")
0191                 onClicked: pickerView.currentIndex = 1
0192             }
0193             QQC2.TabButton {
0194                 id: yearsViewCheck
0195                 Layout.fillWidth: true
0196                 text: i18n("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: Calendar.InfiniteCalendarViewModel {
0236                     scale: Calendar.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 
0261                     required property date firstDayOfMonth
0262                     required property int index
0263 
0264                     property bool isNextOrCurrentItem: index >= monthPathView.currentIndex -1 && index <= monthPathView.currentIndex + 1
0265 
0266                     active: isNextOrCurrentItem && datepicker.showDays
0267 
0268                     sourceComponent: GridLayout {
0269                         id: dayGrid
0270 
0271                         columns: 7
0272                         rows: 7
0273 
0274                         width: monthPathView.width
0275                         height: monthPathView.height
0276 
0277                         Layout.topMargin: Kirigami.Units.smallSpacing
0278 
0279                         property var modelLoader: Loader {
0280                             asynchronous: true
0281                             sourceComponent: Calendar.MonthModel {
0282                                 year: monthViewLoader.firstDayOfMonth.getFullYear()
0283                                 month: monthViewLoader.firstDayOfMonth.getMonth() + 1 // From pathview model
0284                             }
0285                         }
0286 
0287                         QQC2.ButtonGroup {
0288                             buttons: dayGrid.children
0289                         }
0290 
0291                         Repeater {
0292                             model: dayGrid.modelLoader.item.weekDays
0293                             delegate: QQC2.Label {
0294                                 required property var modelData
0295 
0296                                 Layout.fillWidth: true
0297                                 Layout.fillHeight: true
0298 
0299                                 horizontalAlignment: Text.AlignHCenter
0300                                 opacity: 0.7
0301                                 text: modelData
0302                             }
0303                         }
0304 
0305                         Repeater {
0306                             model: dayGrid.modelLoader.item
0307 
0308                             delegate: QQC2.Button {
0309                                 Layout.fillWidth: true
0310                                 Layout.fillHeight: true
0311                                 flat: true
0312                                 highlighted: model.isToday
0313                                 checkable: true
0314                                 checked: date.getDate() === clickedDate.getDate() &&
0315                                     date.getMonth() === clickedDate.getMonth() &&
0316                                     date.getFullYear() === clickedDate.getFullYear()
0317                                 opacity: sameMonth ? 1 : 0.7
0318                                 text: model.dayNumber
0319                                 onClicked: datePicked(model.date), clickedDate = model.date
0320                             }
0321                         }
0322                     }
0323                 }
0324             }
0325 
0326             PathView {
0327                 id: yearPathView
0328 
0329                 Layout.fillWidth: true
0330                 Layout.fillHeight: true
0331                 implicitHeight: Kirigami.Units.gridUnit * 9
0332                 flickDeceleration: Kirigami.Units.longDuration
0333                 preferredHighlightBegin: 0.5
0334                 preferredHighlightEnd: 0.5
0335                 snapMode: PathView.SnapToItem
0336                 focus: true
0337                 interactive: Kirigami.Settings.tabletMode
0338                 clip: true
0339 
0340                 path: Path {
0341                     startX: - yearPathView.width * yearPathView.count / 2 + yearPathView.width / 2
0342                     startY: yearPathView.height / 2
0343                     PathLine {
0344                         x: yearPathView.width * yearPathView.count / 2 + yearPathView.width / 2
0345                         y: yearPathView.height / 2
0346                     }
0347                 }
0348 
0349                 model: Calendar.InfiniteCalendarViewModel {
0350                     scale: Calendar.InfiniteCalendarViewModel.YearScale
0351                 }
0352 
0353                 property int startIndex
0354                 Component.onCompleted: {
0355                     startIndex = count / 2;
0356                     currentIndex = startIndex;
0357                 }
0358                 onCurrentIndexChanged: {
0359                     if(pickerView.currentIndex == 1) {
0360                         datepicker.selectedDate = new Date(currentItem.startDate.getFullYear(), datepicker.selectedDate.getMonth(), datepicker.selectedDate.getDate())
0361                     }
0362 
0363                     if(currentIndex >= count - 2) {
0364                         model.addDates(true);
0365                     } else if (currentIndex <= 1) {
0366                         model.addDates(false);
0367                         startIndex += model.datesToAdd;
0368                     }
0369                 }
0370 
0371                 delegate: Loader {
0372                     id: yearViewLoader
0373                     property date startDate: model.startDate
0374                     property bool isNextOrCurrentItem: index >= yearPathView.currentIndex -1 && index <= yearPathView.currentIndex + 1
0375 
0376                     active: isNextOrCurrentItem
0377 
0378                     sourceComponent: GridLayout {
0379                         id: yearGrid
0380                         columns: 3
0381                         rows: 4
0382                         Layout.fillWidth: true
0383                         Layout.fillHeight: true
0384                         Layout.topMargin: Kirigami.Units.smallSpacing
0385 
0386                         QQC2.ButtonGroup {
0387                             buttons: yearGrid.children
0388                         }
0389 
0390                         Repeater {
0391                             model: yearGrid.columns * yearGrid.rows
0392                             delegate: QQC2.Button {
0393                                 property date date: new Date(startDate.getFullYear(), index)
0394                                 Layout.fillWidth: true
0395                                 Layout.fillHeight: true
0396                                 flat: true
0397                                 highlighted: date.getMonth() === new Date().getMonth() &&
0398                                     date.getFullYear() === new Date().getFullYear()
0399                                 checkable: true
0400                                 checked: date.getMonth() === clickedDate.getMonth() &&
0401                                     date.getFullYear() === clickedDate.getFullYear()
0402                                 text: Qt.locale().standaloneMonthName(date.getMonth())
0403                                 onClicked: {
0404                                     selectedDate = new Date(date);
0405                                     clickedDate = new Date(date);
0406                                     datepicker.datePicked(date);
0407                                     if(datepicker.showDays) pickerView.currentIndex = 0;
0408                                 }
0409                             }
0410                         }
0411                     }
0412                 }
0413             }
0414 
0415             PathView {
0416                 id: decadePathView
0417 
0418                 Layout.fillWidth: true
0419                 Layout.fillHeight: true
0420                 implicitHeight: Kirigami.Units.gridUnit * 9
0421                 flickDeceleration: Kirigami.Units.longDuration
0422                 preferredHighlightBegin: 0.5
0423                 preferredHighlightEnd: 0.5
0424                 snapMode: PathView.SnapToItem
0425                 focus: true
0426                 interactive: Kirigami.Settings.tabletMode
0427                 clip: true
0428 
0429                 path: Path {
0430                     startX: - decadePathView.width * decadePathView.count / 2 + decadePathView.width / 2
0431                     startY: decadePathView.height / 2
0432                     PathLine {
0433                         x: decadePathView.width * decadePathView.count / 2 + decadePathView.width / 2
0434                         y: decadePathView.height / 2
0435                     }
0436                 }
0437 
0438                 model: Calendar.InfiniteCalendarViewModel {
0439                     scale: Calendar.InfiniteCalendarViewModel.DecadeScale
0440                 }
0441 
0442                 property int startIndex
0443                 Component.onCompleted: {
0444                     startIndex = count / 2;
0445                     currentIndex = startIndex;
0446                 }
0447                 onCurrentIndexChanged: {
0448                     if(pickerView.currentIndex == 2) {
0449                         // getFullYear + 1 because the startDate is e.g. 2019, but we want the 2020 decade to be selected
0450                         datepicker.selectedDate = new Date(currentItem.startDate.getFullYear() + 1, datepicker.selectedDate.getMonth(), datepicker.selectedDate.getDate())
0451                     }
0452 
0453                     if(currentIndex >= count - 2) {
0454                         model.addDates(true);
0455                     } else if (currentIndex <= 1) {
0456                         model.addDates(false);
0457                         startIndex += model.datesToAdd;
0458                     }
0459                 }
0460 
0461                 delegate: Loader {
0462                     id: decadeViewLoader
0463                     property date startDate: model.startDate
0464                     property bool isNextOrCurrentItem: index >= decadePathView.currentIndex -1 && index <= decadePathView.currentIndex + 1
0465 
0466                     active: isNextOrCurrentItem
0467 
0468                     sourceComponent: GridLayout {
0469                         id: decadeGrid
0470                         columns: 3
0471                         rows: 4
0472                         Layout.fillWidth: true
0473                         Layout.fillHeight: true
0474                         Layout.topMargin: Kirigami.Units.smallSpacing
0475 
0476                         QQC2.ButtonGroup {
0477                             buttons: decadeGrid.children
0478                         }
0479 
0480                         Repeater {
0481                             model: decadeGrid.columns * decadeGrid.rows
0482                             delegate: QQC2.Button {
0483                                 property date date: new Date(startDate.getFullYear() + index, 0)
0484                                 property bool sameDecade: Math.floor(date.getFullYear() / 10) == Math.floor(year / 10)
0485                                 Layout.fillWidth: true
0486                                 Layout.fillHeight: true
0487                                 flat: true
0488                                 highlighted: date.getFullYear() === new Date().getFullYear()
0489                                 checkable: true
0490                                 checked: date.getFullYear() === clickedDate.getFullYear()
0491                                 opacity: sameDecade ? 1 : 0.7
0492                                 text: date.getFullYear()
0493                                 onClicked: {
0494                                     selectedDate = new Date(date);
0495                                     clickedDate = new Date(date);
0496                                     datepicker.datePicked(date);
0497                                     pickerView.currentIndex = 1;
0498                                 }
0499                             }
0500                         }
0501                     }
0502                 }
0503             }
0504         }
0505     }
0506 }