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 }