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 }