MauiKit Calendar

HourlyView.qml
1// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4import QtQuick 2.15
5import QtQuick.Layouts 1.1
6import QtQuick.Controls 2.15 as QQC2
7import org.kde.kirigami 2.14 as Kirigami
8import QtGraphicalEffects 1.12
9
10import org.mauikit.calendar 1.0 as Kalendar
11import "dateutils.js" as DateUtils
12import "labelutils.js" as LabelUtils
13
14Kirigami.Page {
15 id: root
16
17 property var openOccurrence: ({})
18 property var model
19
20 property date selectedDate: new Date()
21 property date startDate: DateUtils.getFirstDayOfMonth(selectedDate)
22 property date currentDate: new Date() // Needs to get updated for marker to move, done from main.qml
23 readonly property int currentDay: currentDate ? currentDate.getDate() : null
24 readonly property int currentMonth: currentDate ? currentDate.getMonth() : null
25 readonly property int currentYear: currentDate ? currentDate.getFullYear() : null
26 property int day: selectedDate.getDate()
27 property int month: selectedDate.getMonth()
28 property int year: selectedDate.getFullYear()
29 property bool initialWeek: true
30 property int daysToShow: 7
31 readonly property int minutesFromStartOfDay: (root.currentDate.getHours() * 60) + root.currentDate.getMinutes()
32 readonly property bool isDark: Kalendar.KalendarUiUtils.darkMode
33 property bool dragDropEnabled: true
34
35 property real scrollbarWidth: 0
36 readonly property real dayWidth: ((root.width - hourLabelWidth - leftPadding - scrollbarWidth) / daysToShow) - gridLineWidth
37 readonly property real incidenceSpacing: Kirigami.Units.smallSpacing / 2
38 readonly property real gridLineWidth: 1.0
39 readonly property real hourLabelWidth: hourLabelMetrics.boundingRect(new Date(0,0,0,0,0,0,0).toLocaleTimeString(Qt.locale(), Locale.NarrowFormat)).width +
40 Kirigami.Units.largeSpacing * 2.5
41 readonly property real periodHeight: Kirigami.Units.gridUnit
42 readonly property var mode: Kalendar.KalendarApplication.Event
43
44 Kirigami.Theme.inherit: false
45 Kirigami.Theme.colorSet: Kirigami.Theme.View
46
47 FontMetrics {
48 id: hourLabelMetrics
49 font.bold: true
50 }
51
52 background: Rectangle {
53 color: Kirigami.Theme.backgroundColor
54 }
55
56 function setToDate(date, isInitialWeek = false, animate = false) {
57 if(!pathView.currentItem) {
58 return;
59 }
60
61 root.initialWeek = isInitialWeek;
62
63 if(root.daysToShow % 7 === 0) {
64 date = DateUtils.getFirstDayOfWeek(date);
65 }
66 const weekDiff = Math.round((date.getTime() - pathView.currentItem.startDate.getTime()) / (root.daysToShow * 24 * 60 * 60 * 1000));
67
68 let position = pathView.currentItem.item.hourScrollView.getCurrentPosition();
69 let newIndex = pathView.currentIndex + weekDiff;
70 let firstItemDate = pathView.model.data(pathView.model.index(1,0), Kalendar.InfiniteCalendarViewModel.StartDateRole);
71 let lastItemDate = pathView.model.data(pathView.model.index(pathView.model.rowCount() - 1,0), Kalendar.InfiniteCalendarViewModel.StartDateRole);
72
73
74 while(firstItemDate >= date) {
75 pathView.model.addDates(false)
76 firstItemDate = pathView.model.data(pathView.model.index(1,0), Kalendar.InfiniteCalendarViewModel.StartDateRole);
77 newIndex = 0;
78 }
79 if(firstItemDate < date && newIndex === 0) {
80 newIndex = Math.round((date - firstItemDate) / (root.daysToShow * 24 * 60 * 60 * 1000)) + 1
81 }
82
83 while(lastItemDate <= date) {
84 pathView.model.addDates(true)
85 lastItemDate = pathView.model.data(pathView.model.index(pathView.model.rowCount() - 1,0), Kalendar.InfiniteCalendarViewModel.StartDateRole);
86 }
87 pathView.currentIndex = newIndex;
88 selectedDate = date;
89
90 if(isInitialWeek) {
91 pathView.currentItem.item.hourScrollView.setToCurrentTime(animate);
92 } else {
93 pathView.currentItem.item.hourScrollView.setPosition(position);
94 }
95 }
96
97 readonly property Kirigami.Action previousAction: Kirigami.Action {
98 icon.name: "go-previous"
99 text: i18n("Previous Week")
100 shortcut: "Left"
101 onTriggered: setToDate(DateUtils.addDaysToDate(pathView.currentItem.startDate, -root.daysToShow))
102 displayHint: Kirigami.DisplayHint.IconOnly
103 }
104 readonly property Kirigami.Action nextAction: Kirigami.Action {
105 icon.name: "go-next"
106 text: i18n("Next Week")
107 shortcut: "Right"
108 onTriggered: setToDate(DateUtils.addDaysToDate(pathView.currentItem.startDate, root.daysToShow))
109 displayHint: Kirigami.DisplayHint.IconOnly
110 }
111 readonly property Kirigami.Action todayAction: Kirigami.Action {
112 icon.name: "go-jump-today"
113 text: i18n("Now")
114 onTriggered: setToDate(new Date(), true, true);
115 }
116
117 actions {
118 left: Qt.application.layoutDirection === Qt.RightToLeft ? nextAction : previousAction
119 right: Qt.application.layoutDirection === Qt.RightToLeft ? previousAction : nextAction
120 main: todayAction
121 }
122
123 padding: 0
124
125 FontMetrics {
126 id: fontMetrics
127 }
128
129 PathView {
130 id: pathView
131
132 anchors.fill: parent
133 flickDeceleration: Kirigami.Units.longDuration
134 preferredHighlightBegin: 0.5
135 preferredHighlightEnd: 0.5
136 highlightRangeMode: PathView.StrictlyEnforceRange
137 snapMode: PathView.SnapToItem
138 focus: true
139 interactive: Kirigami.Settings.tabletMode
140
141 path: Path {
142 startX: - pathView.width * pathView.count / 2 + pathView.width / 2
143 startY: pathView.height / 2
144 PathLine {
145 x: pathView.width * pathView.count / 2 + pathView.width / 2
146 y: pathView.height / 2
147 }
148 }
149
150 model: root.model
151
152 property real scrollPosition
153 onMovementStarted: {
154 scrollPosition = pathView.currentItem.item.hourScrollView.getCurrentPosition();
155 }
156
157 onMovementEnded: {
158 pathView.currentItem.item.hourScrollView.setPosition(scrollPosition);
159 }
160
161 property date dateToUse
162 property int startIndex: root.model.rowCount() / 2;
163 Component.onCompleted: {
164 currentIndex = startIndex;
165 }
166 onCurrentIndexChanged: if(currentItem) {
167 root.startDate = currentItem.startDate;
168 root.month = currentItem.month;
169 root.year = currentItem.year;
170
171 if(currentIndex >= count - 2) {
172 model.addDates(true);
173 } else if (currentIndex <= 1) {
174 model.addDates(false);
175 startIndex += model.weeksToAdd;
176 }
177 }
178
179 delegate: Loader {
180 id: viewLoader
181
182 readonly property date startDate: model.startDate
183 readonly property date endDate: DateUtils.addDaysToDate(model.startDate, root.daysToShow)
184 readonly property int month: model.selectedMonth - 1 // Convert QDateTime month to JS month
185 readonly property int year: model.selectedYear
186
187 readonly property int index: model.index
188 readonly property bool isCurrentItem: PathView.isCurrentItem
189 readonly property bool isNextOrCurrentItem: index >= pathView.currentIndex -1 && index <= pathView.currentIndex + 1
190 property int multiDayLinesShown: 0
191
192 readonly property int daysFromWeekStart: DateUtils.fullDaysBetweenDates(startDate, root.currentDate) - 1
193 // As long as the date is even slightly larger, it will return 1; since we start from the startDate at 00:00, adjust
194
195 active: isNextOrCurrentItem
196 asynchronous: !isCurrentItem
197 visible: status === Loader.Ready
198 sourceComponent: Column {
199 id: viewColumn
200 width: pathView.width
201 height: pathView.height
202 spacing: 0
203
204 readonly property alias hourScrollView: hourlyView
205
206 Row {
207 id: headingRow
208 width: pathView.width
209 spacing: root.gridLineWidth
210
211 Kirigami.Heading {
212 id: weekNumberHeading
213
214 width: root.hourLabelWidth - root.gridLineWidth
215 horizontalAlignment: Text.AlignRight
216 padding: Kirigami.Units.smallSpacing
217 level: 2
218 text: DateUtils.getWeek(viewLoader.startDate, Qt.locale().firstDayOfWeek)
219 color: Kirigami.Theme.disabledTextColor
220 background: Rectangle {
221 color: Kirigami.Theme.backgroundColor
222 }
223 }
224
225 Repeater {
226 id: dayHeadings
227
228 model: switch(root.daysToShow) {
229 case 1:
230 return dayViewModel.rowCount();
231 case 3:
232 return threeDayViewModel.rowCount();
233 case 7:
234 default:
235 return weekViewModel.rowCount();
236 }
237 delegate: Rectangle {
238 width: root.dayWidth
239 implicitHeight: dayHeading.implicitHeight
240 color: Kirigami.Theme.backgroundColor
241
242 Kirigami.Heading { // Heading is out of the button so the color isn't disabled when the button is
243 id: dayHeading
244
245 property date headingDate: DateUtils.addDaysToDate(viewLoader.startDate, index)
246 property bool isToday: headingDate.getDate() === root.currentDay &&
247 headingDate.getMonth() === root.currentMonth &&
248 headingDate.getFullYear() === root.currentYear
249 width: parent.width
250 horizontalAlignment: Text.AlignRight
251 padding: Kirigami.Units.smallSpacing
252 level: 2
253 color: isToday ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
254 text: {
255 const longText = headingDate.toLocaleDateString(Qt.locale(), "dddd <b>d</b>");
256 const mediumText = headingDate.toLocaleDateString(Qt.locale(), "ddd <b>d</b>");
257 const shortText = mediumText.slice(0,1) + " " + headingDate.toLocaleDateString(Qt.locale(), "<b>d</b>");
258
259
260 if(fontMetrics.boundingRect(longText).width < width) {
261 return longText;
262 } else if(fontMetrics.boundingRect(mediumText).width < width) {
263 return mediumText;
264 } else {
265 return shortText;
266 }
267 }
268 }
269
270 QQC2.Button {
271 implicitHeight: dayHeading.implicitHeight
272 width: parent.width
273
274 flat: true
275 enabled: root.daysToShow > 1
276 onClicked: KalendarUiUtils.openDayLayer(dayHeading.headingDate)
277 }
278 }
279 }
280 Rectangle { // Cover up the shadow of headerTopSeparator above the scrollbar
281 color: Kirigami.Theme.backgroundColor
282 height: parent.height
283 width: root.scrollbarWidth
284 }
285 }
286
287 Kirigami.Separator {
288 id: headerTopSeparator
289 width: pathView.width
290 height: root.gridLineWidth
291 z: -1
292
293 RectangularGlow {
294 anchors.fill: parent
295 z: -1
296 glowRadius: 5
297 spread: 0.3
298 color: Qt.rgba(0.0, 0.0, 0.0, 0.15)
299 visible: !allDayViewLoader.active
300 }
301 }
302
303 Item {
304 id: allDayHeader
305 width: pathView.width
306 height: actualHeight
307 visible: allDayViewLoader.active
308
309 readonly property int minHeight: Kirigami.Units.gridUnit *2
310 readonly property int maxHeight: pathView.height / 3
311 readonly property int lineHeight: viewLoader.multiDayLinesShown * (Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing + root.incidenceSpacing) + Kirigami.Units.smallSpacing
312 readonly property int defaultHeight: Math.min(lineHeight, maxHeight)
313 property int actualHeight: {
314 if (Kalendar.Config.weekViewAllDayHeaderHeight === -1) {
315 return defaultHeight;
316 } else {
317 return Kalendar.Config.weekViewAllDayHeaderHeight;
318 }
319 }
320
321 NumberAnimation {
322 id: resetAnimation
323 target: allDayHeader
324 property: "height"
325 to: allDayHeader.defaultHeight
326 duration: Kirigami.Units.longDuration
327 easing.type: Easing.InOutQuad
328 onFinished: {
329 Kalendar.Config.weekViewAllDayHeaderHeight = -1;
330 Kalendar.Config.save();
331 allDayHeader.actualHeight = allDayHeader.defaultHeight;
332 }
333 }
334
335 Rectangle {
336 id: headerBackground
337 anchors.fill: parent
338 color: Kirigami.Theme.backgroundColor
339 }
340
341 Kirigami.ShadowedRectangle {
342 anchors.left: parent.left
343 anchors.top: parent.bottom
344 width: root.hourLabelWidth
345 height: Kalendar.Config.weekViewAllDayHeaderHeight !== -1 ?
346 resetHeaderHeightButton.height :
347 0
348 z: -1
349 corners.bottomRightRadius: Kirigami.Units.smallSpacing
350 shadow.size: Kirigami.Units.largeSpacing
351 shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.2)
352 shadow.yOffset: 2
353 shadow.xOffset: 2
354 color: Kirigami.Theme.backgroundColor
355 border.width: root.gridLineWidth
356 border.color: headerBottomSeparator.color
357
358 Behavior on height { NumberAnimation {
359 duration: Kirigami.Units.shortDuration
360 easing.type: Easing.InOutQuad
361 } }
362
363 Item {
364 width: root.hourLabelWidth
365 height: parent.height
366 clip: true
367
368 QQC2.ToolButton {
369 id: resetHeaderHeightButton
370 width: root.hourLabelWidth
371 text: i18nc("@action:button", "Reset")
372 onClicked: resetAnimation.start()
373 }
374 }
375 }
376
377 QQC2.Label {
378 width: root.hourLabelWidth
379 height: parent.height
380 padding: Kirigami.Units.smallSpacing
381 leftPadding: Kirigami.Units.largeSpacing
382 verticalAlignment: Text.AlignTop
383 horizontalAlignment: Text.AlignRight
384 text: i18n("All day or Multi day")
385 wrapMode: Text.Wrap
386 elide: Text.ElideRight
387 font: Kirigami.Theme.smallFont
388 color: Kirigami.Theme.disabledTextColor
389 }
390
391 Loader {
392 id: allDayViewLoader
393 anchors.fill: parent
394 anchors.leftMargin: root.hourLabelWidth
395 asynchronous: !viewLoader.isCurrentItem
396 active: switch(root.daysToShow) {
397 case 1:
398 return dayViewDayGridViewModel.incidenceCount > 0;
399 case 3:
400 return threeDayViewDayGridViewModel.incidenceCount > 0;
401 case 7:
402 default:
403 return weekViewDayGridViewModel.incidenceCount > 0;
404 }
405 sourceComponent: Item {
406 id: allDayViewItem
407 implicitHeight: allDayHeader.actualHeight
408 clip: true
409
410 Repeater {
411 // TODO: Clean this up
412 model: switch(root.daysToShow) {
413 case 1:
414 return dayViewDayGridViewModel;
415 case 3:
416 return threeDayViewDayGridViewModel;
417 case 7:
418 default:
419 return weekViewDayGridViewModel;
420 } // from root.model
421 Layout.topMargin: Kirigami.Units.largeSpacing
422 //One row => one week
423 Item {
424 id: weekItem
425 width: parent.width
426 implicitHeight: allDayHeader.actualHeight
427 clip: true
428 RowLayout {
429 width: parent.width
430 height: parent.height
431 spacing: root.gridLineWidth
432 Item {
433 id: dayDelegate
434 Layout.fillWidth: true
435 Layout.fillHeight: true
436 readonly property date startDate: periodStartDate
437
438 QQC2.ScrollView {
439 id: linesListViewScrollView
440 anchors {
441 fill: parent
442 }
443
444 QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
445
446 ListView {
447 id: linesRepeater
448 Layout.fillWidth: true
449 Layout.rightMargin: spacing
450
451 clip: true
452 spacing: root.incidenceSpacing
453
454 ListView {
455 id: allDayIncidencesBackgroundView
456 anchors.fill: parent
457 spacing: root.gridLineWidth
458 orientation: Qt.Horizontal
459 z: -1
460
461 Kirigami.Separator {
462 anchors.fill: parent
463 anchors.rightMargin: root.scrollbarWidth
464 z: -1
465 }
466
467 model: root.daysToShow
468 delegate: Rectangle {
469 id: multiDayViewBackground
470
471 readonly property date date: DateUtils.addDaysToDate(viewLoader.startDate, index)
472 readonly property bool isToday: date.getDate() === root.currentDay &&
473 date.getMonth() === root.currentMonth &&
474 date.getFullYear() === root.currentYear
475
476 width: root.dayWidth
477 height: linesListViewScrollView.height
478 color: multiDayViewIncidenceDropArea.containsDrag ? Kirigami.Theme.positiveBackgroundColor :
479 isToday ? Kirigami.Theme.activeBackgroundColor : Kirigami.Theme.backgroundColor
480
481// DayMouseArea {
482// id: listViewMenu
483// anchors.fill: parent
484
485// addDate: parent.date
486// onAddNewIncidence: root.addIncidence(type, addDate, false)
487// onDeselect: KalendarUiUtils.appMain.incidenceInfoDrawer.close()
488
489// DropArea {
490// id: multiDayViewIncidenceDropArea
491// anchors.fill: parent
492// z: 9999
493// onDropped: if(viewLoader.isCurrentItem) {
494// const pos = mapToItem(root, x, y);
495// drop.source.caughtX = pos.x + root.incidenceSpacing;
496// drop.source.caughtY = pos.y;
497// drop.source.caught = true;
498
499// const incidenceWrapper = Qt.createQmlObject('import org.kde.kalendar 1.0; IncidenceWrapper {id: incidence}', multiDayViewIncidenceDropArea, "incidence");
500// incidenceWrapper.incidenceItem = Kalendar.CalendarManager.incidenceItem(drop.source.incidencePtr);
501
502// let sameTimeOnDate = new Date(listViewMenu.addDate);
503// sameTimeOnDate = new Date(sameTimeOnDate.setHours(drop.source.occurrenceDate.getHours(), drop.source.occurrenceDate.getMinutes()));
504// const offset = sameTimeOnDate.getTime() - drop.source.occurrenceDate.getTime();
505// /* There are 2 possibilities here: we move multiday incidence between days or we move hourly incidence
506// * to convert it into multiday incidence
507// */
508// if (drop.source.objectName === 'hourlyIncidenceDelegateBackgroundBackground') {
509// // This is conversion from non-multiday to multiday
510// KalendarUiUtils.setUpIncidenceDateChange(incidenceWrapper, offset, offset, drop.source.occurrenceDate, drop.source, true)
511// } else {
512// KalendarUiUtils.setUpIncidenceDateChange(incidenceWrapper, offset, offset, drop.source.occurrenceDate, drop.source)
513// }
514// }
515// }
516// }
517 }
518 }
519
520 model: incidences
521 onCountChanged: {
522 viewLoader.multiDayLinesShown = count
523 }
524
525 delegate: Item {
526 id: line
527 height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
528
529 //Incidences
530 Repeater {
531 id: allDayIncidencesRepeater
532 model: modelData
533 DayGridViewIncidenceDelegate {
534 id: dayGridViewIncidenceDelegate
535 objectName: "dayGridViewIncidenceDelegate"
536 dayWidth: root.dayWidth
537 height: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing
538 parentViewSpacing: root.gridLineWidth
539 horizontalSpacing: linesRepeater.spacing
540 openOccurrenceId: root.openOccurrence ? root.openOccurrence.incidenceId : ""
541 isDark: root.isDark
542 reactToCurrentMonth: false
543 dragDropEnabled: root.dragDropEnabled
544 }
545 }
546 }
547 }
548 }
549 }
550 }
551 }
552 }
553 }
554 }
555 }
556
557// ResizerSeparator {
558// id: headerBottomSeparator
559// width: pathView.width
560// height: root.gridLineWidth
561// oversizeMouseAreaVertical: 5
562// z: Infinity
563// visible: allDayViewLoader.active
564
565// function setPos() {
566// Kalendar.Config.weekViewAllDayHeaderHeight = allDayHeader.actualHeight;
567// Kalendar.Config.save();
568// }
569
570// onDragBegin: setPos()
571// onDragReleased: setPos()
572// onDragPositionChanged: allDayHeader.actualHeight = Math.min(allDayHeader.maxHeight, Math.max(allDayHeader.minHeight, Kalendar.Config.weekViewAllDayHeaderHeight + changeY))
573// }
574
575// RectangularGlow {
576// id: headerBottomShadow
577// anchors.top: headerBottomSeparator.bottom
578// anchors.left: parent.left
579// anchors.right: parent.right
580// z: -1
581// glowRadius: 5
582// spread: 0.3
583// color: Qt.rgba(0.0, 0.0, 0.0, 0.15)
584// }
585
586 QQC2.ScrollView {
587 id: hourlyView
588 width: viewColumn.width
589 height: actualHeight
590 contentWidth: availableWidth
591 z: -2
592 QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
593
594 readonly property int periodLength: switch(root.daysToShow) {
595 case 1:
596 return dayViewModel.periodLength;
597 case 3:
598 return threeDayViewModel.periodLength;
599 case 7:
600 default:
601 return weekViewModel.periodLength;
602 }
603 readonly property real periodsPerHour: 60 / periodLength
604 readonly property real daySections: (60 * 24) / periodLength
605 readonly property real dayHeight: (daySections * root.periodHeight) + (root.gridLineWidth * 23)
606 readonly property real hourHeight: periodsPerHour * root.periodHeight
607 readonly property real minuteHeight: hourHeight / 60
608 readonly property Item vScrollBar: QQC2.ScrollBar.vertical
609
610 property int actualHeight: {
611 let h = viewColumn.height - headerBottomSeparator.height - headerTopSeparator.height - headingRow.height;
612 if (allDayHeader.visible) {
613 h -= allDayHeader.height;
614 }
615 return h;
616 }
617
618 NumberAnimation on QQC2.ScrollBar.vertical.position {
619 id: scrollAnimation
620 duration: Kirigami.Units.longDuration
621 easing.type: Easing.InOutQuad
622 }
623
624 function setToCurrentTime(animate = false) {
625 if(currentTimeMarkerLoader.active) {
626 const viewHeight = (applicationWindow().height - applicationWindow().pageStack.globalToolBar.height - headerBottomSeparator.height - allDayHeader.height - headerTopSeparator.height - headingRow.height - Kirigami.Units.gridUnit);
627 // Since we position with anchors, height is 0 -- must calc manually
628
629 const timeMarkerY = (root.currentDate.getHours() * root.gridLineWidth) + (hourlyView.minuteHeight * root.minutesFromStartOfDay) - (height / 2) - (root.gridLineWidth / 2)
630 const yPos = Math.max(0.0, (timeMarkerY / dayHeight))
631 setPosition(yPos, animate);
632 }
633 }
634
635 function getCurrentPosition() {
636 return vScrollBar.position;
637 }
638
639 function setPosition(position, animate = false) {
640 let offset = vScrollBar.visualSize + position - 1;
641 // Initially let's assume that we are still somewhere before bottom of the hourlyView
642 // so lets simply set vScrollBar position to what was given
643 let yPos = position;
644 if (offset > 0) {
645 // Ups, it seems that we are going lower than bottom of the hourlyView
646 // Lets set position to the bottom of the vScrollBar then
647 yPos = 1 - vScrollBar.visualSize;
648 }
649 if (animate) {
650 scrollAnimation.to = yPos;
651 if (scrollAnimation.running) {
652 scrollAnimation.stop();
653 }
654 scrollAnimation.start();
655 } else {
656 vScrollBar.position = yPos;
657 }
658 }
659
660 Connections {
661 target: hourlyView.QQC2.ScrollBar.vertical
662 function onWidthChanged() {
663 if(!Kirigami.Settings.isMobile) root.scrollbarWidth = hourlyView.QQC2.ScrollBar.vertical.width;
664 }
665 }
666 Component.onCompleted: {
667 if(!Kirigami.Settings.isMobile) root.scrollbarWidth = hourlyView.QQC2.ScrollBar.vertical.width;
668 if(currentTimeMarkerLoader.active && root.initialWeek) {
669 setToCurrentTime();
670 }
671 }
672
673 Item {
674 id: hourlyViewContents
675 width: parent.width
676 implicitHeight: hourlyView.dayHeight
677
678 clip: true
679
680 Item {
681 id: hourLabelsColumn
682
683 property real currentTimeLabelTop: currentTimeLabelLoader.active ?
684 currentTimeLabelLoader.item.y
685 : 0
686 property real currentTimeLabelBottom: currentTimeLabelLoader.active ?
687 currentTimeLabelLoader.item.y + fontMetrics.height
688 : 0
689
690 anchors.left: parent.left
691 anchors.top: parent.top
692 anchors.bottom: parent.bottom
693 width: root.hourLabelWidth
694
695 Loader {
696 id: currentTimeLabelLoader
697
698 active: currentTimeMarkerLoader.active
699 sourceComponent: QQC2.Label {
700 id: currentTimeLabel
701
702 width: root.hourLabelWidth
703 color: Kirigami.Theme.highlightColor
704 font.weight: Font.DemiBold
705 horizontalAlignment: Text.AlignRight
706 rightPadding: Kirigami.Units.smallSpacing
707 y: Math.max(0, (root.currentDate.getHours() * root.gridLineWidth) + (hourlyView.minuteHeight * root.minutesFromStartOfDay) - (implicitHeight / 2)) - (root.gridLineWidth / 2)
708 z: 100
709
710 text: root.currentDate.toLocaleTimeString(Qt.locale(), Locale.NarrowFormat)
711
712 }
713 }
714
715 Repeater {
716 model: pathView.model.hourlyViewLocalisedHourLabels // Not a model role but instead one of the model object's properties
717
718 delegate: QQC2.Label {
719 property real textYTop: y
720 property real textYBottom: y + fontMetrics.height
721 property bool overlapWithCurrentTimeLabel: currentTimeLabelLoader.active &&
722 ((hourLabelsColumn.currentTimeLabelTop <= textYTop && hourLabelsColumn.currentTimeLabelBottom >= textYTop) ||
723 (hourLabelsColumn.currentTimeLabelTop < textYBottom && hourLabelsColumn.currentTimeLabelBottom > textYBottom) ||
724 (hourLabelsColumn.currentTimeLabelTop >= textYTop && hourLabelsColumn.currentTimeLabelBottom <= textYBottom))
725
726 y: ((root.periodHeight * hourlyView.periodsPerHour) * (index + 1)) + (root.gridLineWidth * (index + 1)) -
727 (fontMetrics.height / 2) - (root.gridLineWidth / 2)
728 width: root.hourLabelWidth
729 rightPadding: Kirigami.Units.smallSpacing
730 verticalAlignment: Text.AlignBottom
731 horizontalAlignment: Text.AlignRight
732 text: modelData
733 color: Kirigami.Theme.disabledTextColor
734 visible: !overlapWithCurrentTimeLabel
735 }
736 }
737 }
738
739 Item {
740 id: innerWeekView
741 anchors {
742 left: hourLabelsColumn.right
743 top: parent.top
744 bottom: parent.bottom
745 right: parent.right
746 }
747 clip: true
748
750 anchors.fill: parent
751 }
752
753 Row {
754 id: dayColumnRow
755 anchors.fill: parent
756 spacing: root.gridLineWidth
757
758 Repeater {
759 id: dayColumnRepeater
760 model: switch(root.daysToShow) {
761 case 1:
762 return dayViewModel;
763 case 3:
764 return threeDayViewModel;
765 case 7:
766 default:
767 return weekViewModel;
768 } // From root.model
769
770 delegate: Item {
771 id: dayColumn
772
773 readonly property int index: model.index
774 readonly property date columnDate: DateUtils.addDaysToDate(viewLoader.startDate, index)
775 readonly property bool isToday: columnDate.getDate() === root.currentDay &&
776 columnDate.getMonth() === root.currentMonth &&
777 columnDate.getFullYear() === root.currentYear
778
779 width: root.dayWidth
780 height: hourlyView.dayHeight
781 clip: true
782
783 Loader {
784 anchors.fill: parent
785 asynchronous: !viewLoader.isCurrentView
786 ListView {
787 anchors.fill: parent
788 spacing: root.gridLineWidth
789 boundsBehavior: Flickable.StopAtBounds
790 interactive: false
791
792 model: 24
793 delegate: Rectangle {
794 id: backgroundRectangle
795 width: parent.width
796 height: hourlyView.hourHeight
797 color: dayColumn.isToday ? Kirigami.Theme.activeBackgroundColor : Kirigami.Theme.backgroundColor
798
799 property int index: model.index
800
801 ColumnLayout {
802 anchors.fill: parent
803 spacing: 0
804 z: 9999
805 Repeater {
806 id: dropAreaRepeater
807 model: 4
808
809 readonly property int minutes: 60 / model
810
811 DropArea {
812 id: hourlyViewIncidenceDropArea
813 Layout.fillWidth: true
814 Layout.fillHeight: true
815 z: 9999
816 onDropped: if(viewLoader.isCurrentItem) {
817 let incidenceWrapper = Qt.createQmlObject('import org.kde.kalendar 1.0; IncidenceWrapper {id: incidence}', hourlyViewIncidenceDropArea, "incidence");
818 /* So when we drop the entire incidence card somewhere, we are dropping the delegate with object name "hourlyIncidenceDelegateBackgroundBackground" or "multiDayIncidenceDelegateBackgroundBackground" in case when all day event is converted to the hour incidence.
819 * However, when we are simply resizing, we are actually dropping the specific mouseArea within the delegate that handles
820 * the dragging for the incidence's bottom edge which has name "endDtResizeMouseArea". Hence why we check the object names
821 */
822 if(drop.source.objectName === "hourlyIncidenceDelegateBackgroundBackground") {
823 incidenceWrapper.incidenceItem = Kalendar.CalendarManager.incidenceItem(drop.source.incidencePtr);
824
825 const pos = mapToItem(root, dropAreaHighlightRectangle.x, dropAreaHighlightRectangle.y);
826 drop.source.caughtX = pos.x + incidenceSpacing;
827 drop.source.caughtY = pos.y + incidenceSpacing;
828 drop.source.caught = true;
829
830 // We want the date as if it were "from the top" of the droparea
831 const posDate = new Date(backgroundDayMouseArea.addDate.getFullYear(), backgroundDayMouseArea.addDate.getMonth(), backgroundDayMouseArea.addDate.getDate(), backgroundRectangle.index, dropAreaRepeater.minutes * index);
832
833 const startOffset = posDate.getTime() - drop.source.occurrenceDate.getTime();
834
835 KalendarUiUtils.setUpIncidenceDateChange(incidenceWrapper, startOffset, startOffset, drop.source.occurrenceDate, drop.source);
836
837 } else if(drop.source.objectName === "multiDayIncidenceDelegateBackgroundBackground") {
838 incidenceWrapper.incidenceItem = Kalendar.CalendarManager.incidenceItem(drop.source.incidencePtr);
839
840 const pos = mapToItem(root, dropAreaHighlightRectangle.x, dropAreaHighlightRectangle.y);
841 drop.source.caughtX = pos.x + incidenceSpacing;
842 drop.source.caughtY = pos.y + incidenceSpacing;
843 drop.source.caught = true;
844
845 // We want the date as if it were "from the top" of the droparea
846 const startPosDate = new Date(backgroundDayMouseArea.addDate.getFullYear(), backgroundDayMouseArea.addDate.getMonth(), backgroundDayMouseArea.addDate.getDate(), backgroundRectangle.index, dropAreaRepeater.minutes * index);
847 // In case when incidence is converted to not be all day anymore, lets set it as 1h long
848 const endPosDate = new Date(backgroundDayMouseArea.addDate.getFullYear(), backgroundDayMouseArea.addDate.getMonth(), backgroundDayMouseArea.addDate.getDate(), backgroundRectangle.index + 1, dropAreaRepeater.minutes * index);
849
850 const startOffset = startPosDate.getTime() - drop.source.occurrenceDate.getTime();
851 const endOffset = endPosDate.getTime() - drop.source.occurrenceEndDate.getTime();
852
853 KalendarUiUtils.setUpIncidenceDateChange(incidenceWrapper, startOffset, endOffset, drop.source.occurrenceDate, drop.source);
854
855 } else { // The resize affects the end time
856 incidenceWrapper.incidenceItem = Kalendar.CalendarManager.incidenceItem(drop.source.resizerSeparator.parent.incidencePtr);
857
858 const pos = mapToItem(drop.source.resizerSeparator.parent, dropAreaHighlightRectangle.x, dropAreaHighlightRectangle.y);
859 drop.source.resizerSeparator.parent.caughtHeight = (pos.y + dropAreaHighlightRectangle.height - incidenceSpacing)
860 drop.source.resizerSeparator.parent.caught = true;
861
862 // We want the date as if it were "from the bottom" of the droparea
863 const minute = (dropAreaRepeater.minutes * (index + 1)) % 60;
864 const isNextHour = minute === 0 && index !== 0;
865 const hour = isNextHour ? backgroundRectangle.index + 1 : backgroundRectangle.index;
866
867 const posDate = new Date(backgroundDayMouseArea.addDate.getFullYear(), backgroundDayMouseArea.addDate.getMonth(), backgroundDayMouseArea.addDate.getDate(), hour, minute);
868
869 const endOffset = posDate.getTime() - drop.source.resizerSeparator.parent.occurrenceEndDate.getTime();
870
871 KalendarUiUtils.setUpIncidenceDateChange(incidenceWrapper, 0, endOffset, drop.source.resizerSeparator.parent.occurrenceDate, drop.source.resizerSeparator.parent);
872 }
873 }
874
875 Rectangle {
876 id: dropAreaHighlightRectangle
877 anchors.fill: parent
878 color: Kirigami.Theme.positiveBackgroundColor
879 visible: hourlyViewIncidenceDropArea.containsDrag
880 }
881 }
882 }
883 }
884
885// DayMouseArea {
886// id: backgroundDayMouseArea
887// anchors.fill: parent
888// addDate: new Date(DateUtils.addDaysToDate(viewLoader.startDate, dayColumn.index).setHours(index))
889// onAddNewIncidence: KalendarUiUtils.setUpAdd(type, addDate, null, true)
890// onDeselect: KalendarUiUtils.appMain.incidenceInfoDrawer.close()
891// }
892 }
893 }
894 }
895
896 Loader {
897 anchors.fill: parent
898 asynchronous: !viewLoader.isCurrentView
899 Repeater {
900 id: hourlyIncidencesRepeater
901 model: incidences
902
903 delegate: Rectangle {
904 id: hourlyIncidenceDelegateBackgroundBackground
905 objectName: "hourlyIncidenceDelegateBackgroundBackground"
906
907 readonly property int initialIncidenceHeight: (modelData.duration * root.periodHeight) - (root.incidenceSpacing * 2) + gridLineHeightCompensation - root.gridLineWidth
908 readonly property real gridLineYCompensation: (modelData.starts / hourlyView.periodsPerHour) * root.gridLineWidth
909 readonly property real gridLineHeightCompensation: (modelData.duration / hourlyView.periodsPerHour) * root.gridLineWidth
910 property bool isOpenOccurrence: root.openOccurrence ?
911 root.openOccurrence.incidenceId === modelData.incidenceId : false
912
913 x: root.incidenceSpacing + (modelData.priorTakenWidthShare * root.dayWidth)
914 y: (modelData.starts * root.periodHeight) + root.incidenceSpacing + gridLineYCompensation
915 width: (root.dayWidth * modelData.widthShare) - (root.incidenceSpacing * 2)
916 height: initialIncidenceHeight
917 radius: Kirigami.Units.smallSpacing
918 color: Qt.rgba(0,0,0,0)
919 visible: !modelData.allDay
920
921 property alias mouseArea: mouseArea
922 property var incidencePtr: modelData.incidencePtr
923 property date occurrenceDate: modelData.startTime
924 property date occurrenceEndDate: modelData.endTime
925 property bool repositionAnimationEnabled: false
926 property bool caught: false
927 property real caughtX: x
928 property real caughtY: y
929 property real caughtHeight: height
930 property real resizeHeight: height
931
932 Drag.active: mouseArea.drag.active
933 Drag.hotSpot.x: mouseArea.mouseX
934
935 // Drag reposition animations -- when the incidence goes to the correct cell of the hourly grid
936 Behavior on x {
937 enabled: repositionAnimationEnabled
938 NumberAnimation {
939 duration: Kirigami.Units.shortDuration
940 easing.type: Easing.OutCubic
941 }
942 }
943
944 Behavior on y {
945 enabled: repositionAnimationEnabled
946 NumberAnimation {
947 duration: Kirigami.Units.shortDuration
948 easing.type: Easing.OutCubic
949 }
950 }
951
952 Behavior on height {
953 enabled: repositionAnimationEnabled
954 NumberAnimation {
955 duration: Kirigami.Units.shortDuration
956 easing.type: Easing.OutCubic
957 }
958 }
959
960 states: [
961 State {
962 when: hourlyIncidenceDelegateBackgroundBackground.mouseArea.drag.active
963 ParentChange { target: hourlyIncidenceDelegateBackgroundBackground; parent: root }
964 PropertyChanges { target: hourlyIncidenceDelegateBackgroundBackground; isOpenOccurrence: true }
965 },
966 State {
967 when: hourlyIncidenceResizer.mouseArea.drag.active
968 PropertyChanges { target: hourlyIncidenceDelegateBackgroundBackground; height: resizeHeight }
969 },
970 State {
971 when: hourlyIncidenceDelegateBackgroundBackground.caught
972 ParentChange { target: hourlyIncidenceDelegateBackgroundBackground; parent: root }
973 PropertyChanges {
974 target: hourlyIncidenceDelegateBackgroundBackground
975 repositionAnimationEnabled: true
976 x: caughtX
977 y: caughtY
978 height: caughtHeight
979 }
980 }
981 ]
982
983// IncidenceDelegateBackground {
984// id: incidenceDelegateBackground
985// isOpenOccurrence: parent.isOpenOccurrence
986// isDark: root.isDark
987// }
988
989 ColumnLayout {
990 id: incidenceContents
991
992 readonly property color textColor: LabelUtils.getIncidenceLabelColor(modelData.color, root.isDark)
993 readonly property bool isTinyHeight: parent.height <= Kirigami.Units.gridUnit
994
995 clip: true
996
997 anchors {
998 fill: parent
999 leftMargin: Kirigami.Units.smallSpacing
1000 rightMargin: Kirigami.Units.smallSpacing
1001 topMargin: !isTinyHeight ? Kirigami.Units.smallSpacing : 0
1002 bottomMargin: !isTinyHeight ? Kirigami.Units.smallSpacing : 0
1003 }
1004
1005 QQC2.Label {
1006 Layout.fillWidth: true
1007 Layout.fillHeight: true
1008 text: modelData.text
1009 horizontalAlignment: Text.AlignLeft
1010 verticalAlignment: Text.AlignTop
1011 wrapMode: Text.Wrap
1012 elide: Text.ElideRight
1013 font.pointSize: parent.isTinyHeight ? Kirigami.Theme.smallFont.pointSize :
1014 Kirigami.Theme.defaultFont.pointSize
1015 font.weight: Font.Medium
1016 font.strikeout: modelData.todoCompleted
1017 renderType: Text.QtRendering
1018 color: isOpenOccurrence ? (LabelUtils.isDarkColor(modelData.color) ? "white" : "black") :
1019 incidenceContents.textColor
1020 Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
1021 }
1022
1023 RowLayout {
1024 width: parent.width
1025 visible: parent.height > Kirigami.Units.gridUnit * 3
1026 Kirigami.Icon {
1027 id: incidenceIcon
1028 implicitWidth: Kirigami.Units.iconSizes.smallMedium
1029 implicitHeight: Kirigami.Units.iconSizes.smallMedium
1030 source: modelData.incidenceTypeIcon
1031 isMask: true
1032 color: isOpenOccurrence ? (LabelUtils.isDarkColor(modelData.color) ? "white" : "black") :
1033 incidenceContents.textColor
1034 Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
1035 visible: parent.width > Kirigami.Units.gridUnit * 4
1036 }
1037 QQC2.Label {
1038 id: timeLabel
1039 Layout.fillWidth: true
1040 horizontalAlignment: Text.AlignRight
1041 text: modelData.startTime.toLocaleTimeString(Qt.locale(), Locale.NarrowFormat) + "–" + modelData.endTime.toLocaleTimeString(Qt.locale(), Locale.NarrowFormat)
1042 wrapMode: Text.Wrap
1043 renderType: Text.QtRendering
1044 color: isOpenOccurrence ? (LabelUtils.isDarkColor(modelData.color) ? "white" : "black") :
1045 incidenceContents.textColor
1046 Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
1047 visible: parent.width > Kirigami.Units.gridUnit * 3
1048 }
1049 }
1050 }
1051
1052// IncidenceMouseArea {
1053// id: mouseArea
1054// preventStealing: !Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile
1055// incidenceData: modelData
1056// collectionId: modelData.collectionId
1057
1058// drag.target: !Kirigami.Settings.isMobile && !modelData.isReadOnly && root.dragDropEnabled ? parent : undefined
1059// onReleased: parent.Drag.drop()
1060
1061// onViewClicked: KalendarUiUtils.setUpView(modelData)
1062// onEditClicked: KalendarUiUtils.setUpEdit(incidencePtr)
1063// onDeleteClicked: KalendarUiUtils.setUpDelete(incidencePtr, deleteDate)
1064// onTodoCompletedClicked: KalendarUiUtils.completeTodo(incidencePtr)
1065// onAddSubTodoClicked: KalendarUiUtils.setUpAddSubTodo(parentWrapper)
1066// }
1067
1068// ResizerSeparator {
1069// id: hourlyIncidenceResizer
1070// objectName: "endDtResizeMouseArea"
1071// anchors.left: parent.left
1072// anchors.leftMargin: hourlyIncidenceDelegateBackgroundBackground.radius
1073// anchors.bottom: parent.bottom
1074// anchors.right: parent.right
1075// anchors.rightMargin: hourlyIncidenceDelegateBackgroundBackground.radius
1076// height: 1
1077// oversizeMouseAreaVertical: 2
1078// z: Infinity
1079// enabled: !Kirigami.Settings.isMobile && !modelData.isReadOnly
1080// unhoveredColor: "transparent"
1081
1082// onDragPositionChanged: parent.resizeHeight = Math.max(root.periodHeight, hourlyIncidenceDelegateBackgroundBackground.initialIncidenceHeight + changeY)
1083// }
1084 }
1085 }
1086 }
1087 }
1088 }
1089 }
1090
1091 Loader {
1092 id: currentTimeMarkerLoader
1093
1094 active: root.currentDate >= viewLoader.startDate && root.currentDate < viewLoader.endDate
1095
1096 sourceComponent: Rectangle {
1097 id: currentTimeMarker
1098
1099 width: root.dayWidth
1100 height: root.gridLineWidth * 2
1101 color: Kirigami.Theme.highlightColor
1102 x: (viewLoader.daysFromWeekStart * root.dayWidth) + (viewLoader.daysFromWeekStart * root.gridLineWidth)
1103 y: (root.currentDate.getHours() * root.gridLineWidth) + (hourlyView.minuteHeight * root.minutesFromStartOfDay) -
1104 (height / 2) - (root.gridLineWidth / 2)
1105 z: 100
1106
1107 Rectangle {
1108 anchors.left: parent.left
1109 anchors.top: parent.top
1110 anchors.topMargin: -(height / 2) + (parent.height / 2)
1111 width: height
1112 height: parent.height * 5
1113 radius: 100
1114 color: Kirigami.Theme.highlightColor
1115 }
1116 }
1117 }
1118 }
1119 }
1120 }
1121 }
1122 }
1123 }
1124}
Q_SCRIPTABLE Q_NOREPLY void start()
Q_SCRIPTABLE CaptureState status()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
KIOWIDGETS_EXPORT DropJob * drop(const QDropEvent *dropEvent, const QUrl &destUrl, DropJobFlags dropjobFlags, JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
QStringView level(QStringView ifopt)
QObject * parent() const const
qsizetype count() const const
AlignBottom
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:50:32 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.