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 }