import "root:/widgets" import "root:/services" import "root:/config" import QtQuick Column { id: root spacing: Appearance.spacing.normal width: 280 property date currentDate: new Date() property bool isCurrentMonth: true property string currentTime: Qt.formatDateTime(new Date(), "HH:mm:ss") property int currentYear: new Date().getFullYear() property date selectedStartDate: new Date() property date selectedEndDate: new Date() property bool isSelectingRange: false property string dateCalculation: "" property bool hasSelection: false property bool hasValidSelection: root.selectedStartDate !== undefined function calculateDays() { if (!root.selectedStartDate) return if (root.selectedStartDate && root.selectedEndDate) { // For single date selection, compare with today const today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()) const selected = new Date(root.selectedStartDate.getFullYear(), root.selectedStartDate.getMonth(), root.selectedStartDate.getDate()) const diffDays = Math.round((selected - today) / (1000 * 60 * 60 * 24)) if (diffDays === 0) { root.dateCalculation = "0 days" } else if (diffDays > 0) { root.dateCalculation = diffDays + " days until" } else { root.dateCalculation = Math.abs(diffDays) + " days since" } } } function resetSelection() { root.selectedStartDate = new Date() root.selectedEndDate = new Date() root.isSelectingRange = false root.dateCalculation = "" root.hasSelection = false } // Update time every second Timer { interval: 1000 running: true repeat: true onTriggered: { root.currentTime = Qt.formatDateTime(new Date(), "HH:mm:ss") } } // Date calculation display Row { visible: root.hasSelection anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small StyledText { text: root.dateCalculation font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: Colours.palette.m3onSurfaceVariant } MaterialIcon { text: "close" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: root.resetSelection() } } } // Date display section Row { anchors.left: parent.left anchors.leftMargin: 12 spacing: Appearance.spacing.normal // Show dates in chronological order Row { spacing: Appearance.spacing.small StyledText { text: { if (!root.hasSelection) return Qt.formatDateTime(new Date(), "dd MMM yyyy") const today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()) const selected = new Date(root.selectedStartDate.getFullYear(), root.selectedStartDate.getMonth(), root.selectedStartDate.getDate()) const diffDays = Math.round((selected - today) / (1000 * 60 * 60 * 24)) // For past dates, show selected date first if (diffDays < 0) { return Qt.formatDateTime(root.selectedStartDate, "dd MMM yyyy") } // For future dates, show today first return Qt.formatDateTime(new Date(), "dd MMM yyyy") } font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: { if (!root.hasSelection) return Colours.palette.m3onSurface const today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()) const selected = new Date(root.selectedStartDate.getFullYear(), root.selectedStartDate.getMonth(), root.selectedStartDate.getDate()) const diffDays = Math.round((selected - today) / (1000 * 60 * 60 * 24)) return diffDays < 0 ? Colours.palette.m3primary : Colours.palette.m3onSurface } } // Arrow and second date (when applicable) Row { visible: root.hasSelection spacing: Appearance.spacing.small StyledText { text: "→" font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } StyledText { text: { if (!root.hasSelection) return "" const today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()) const selected = new Date(root.selectedStartDate.getFullYear(), root.selectedStartDate.getMonth(), root.selectedStartDate.getDate()) const diffDays = Math.round((selected - today) / (1000 * 60 * 60 * 24)) // For past dates, show today second if (diffDays < 0) { return Qt.formatDateTime(new Date(), "dd MMM yyyy") } // For future dates, show selected date second return Qt.formatDateTime(root.selectedStartDate, "dd MMM yyyy") } font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: { if (!root.hasSelection) return Colours.palette.m3onSurface const today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()) const selected = new Date(root.selectedStartDate.getFullYear(), root.selectedStartDate.getMonth(), root.selectedStartDate.getDate()) const diffDays = Math.round((selected - today) / (1000 * 60 * 60 * 24)) return diffDays < 0 ? Colours.palette.m3onSurface : Colours.palette.m3primary } } } } } // Time display StyledText { anchors.horizontalCenter: parent.horizontalCenter text: root.currentTime font.pointSize: Appearance.font.size.larger font.family: Appearance.font.family.mono } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small Row { spacing: Appearance.spacing.small MaterialIcon { text: "chevron_left" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { root.currentYear-- root.currentDate = new Date(root.currentYear, monthView.currentIndex, 1) } } } StyledText { text: root.currentYear font.pointSize: Appearance.font.size.large font.weight: 700 } MaterialIcon { text: "chevron_right" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { root.currentYear++ root.currentDate = new Date(root.currentYear, monthView.currentIndex, 1) } } } } Row { spacing: Appearance.spacing.small MaterialIcon { text: "chevron_left" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { monthView.decrementCurrentIndex() } } } StyledText { text: Qt.formatDateTime(root.currentDate, "MMMM") font.pointSize: Appearance.font.size.large font.weight: 400 color: Colours.palette.m3onSurfaceVariant } MaterialIcon { text: "chevron_right" color: Colours.palette.m3onSurfaceVariant MouseArea { anchors.fill: parent onClicked: { monthView.incrementCurrentIndex() } } } } } // Today button between controls and calendar Rectangle { id: todayButton visible: root.currentYear !== new Date().getFullYear() || monthView.currentIndex !== new Date().getMonth() anchors.horizontalCenter: parent.horizontalCenter width: todayText.width + 20 height: 25 color: mouseArea.containsMouse ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3surfaceContainer radius: Appearance.rounding.small Text { id: todayText anchors.centerIn: parent text: "Today" color: Colours.palette.m3onSurface font.pixelSize: 12 } MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true onClicked: { root.currentYear = new Date().getFullYear() monthView.currentIndex = new Date().getMonth() } } } // Calendar grid Rectangle { width: 280 height: 250 anchors.horizontalCenter: parent.horizontalCenter color: "transparent" border.color: Colours.palette.m3outline border.width: 1 radius: 8 ListView { id: monthView anchors.fill: parent anchors.margins: 12 anchors.rightMargin: 0 // Reduced right margin orientation: ListView.Horizontal snapMode: ListView.SnapOneItem highlightRangeMode: ListView.StrictlyEnforceRange highlightMoveDuration: Appearance.anim.durations.normal highlightMoveVelocity: -1 highlightResizeDuration: Appearance.anim.durations.normal highlightResizeVelocity: -1 // Add mouse wheel scrolling MouseArea { anchors.fill: parent property bool canScroll: true property Timer scrollTimer: Timer { interval: 200 // Increased debounce time for smoother scrolling onTriggered: parent.canScroll = true } // Configurable scroll direction property bool invertScrollDirection: false // Set to true to invert the scroll direction onWheel: { if (!canScroll) return // Handle trackpad horizontal scroll if (wheel.angleDelta.x !== 0) { const scrollRight = wheel.angleDelta.x > 0 const shouldGoBack = invertScrollDirection ? !scrollRight : scrollRight if (shouldGoBack) { // Going to previous month if (monthView.currentIndex === 0) { root.currentYear-- monthView.currentIndex = 11 } else { monthView.decrementCurrentIndex() } } else { // Going to next month if (monthView.currentIndex === 11) { root.currentYear++ monthView.currentIndex = 0 } else { monthView.incrementCurrentIndex() } } } // Handle mouse wheel vertical scroll else if (wheel.angleDelta.y !== 0) { const scrollDown = wheel.angleDelta.y < 0 const shouldGoBack = invertScrollDirection ? !scrollDown : scrollDown if (shouldGoBack) { // Going to previous month if (monthView.currentIndex === 0) { root.currentYear-- monthView.currentIndex = 11 } else { monthView.decrementCurrentIndex() } } else { // Going to next month if (monthView.currentIndex === 11) { root.currentYear++ monthView.currentIndex = 0 } else { monthView.incrementCurrentIndex() } } } // Prevent rapid scrolling canScroll = false scrollTimer.restart() } } // Add touch gesture support with percentage-based scrolling MouseArea { anchors.fill: parent property real startX: 0 property bool isDragging: false property real scrollPercentage: 0.0 // 0.0 to 1.0, represents progress to next/prev month property real scrollSpeed: 0.15 // Increased threshold for more deliberate scrolling onPressed: (mouse) => { startX = mouse.x isDragging = true scrollPercentage = 0.0 } onReleased: { isDragging = false scrollPercentage = 0.0 } onPositionChanged: (mouse) => { if (!isDragging) return const dragDistance = mouse.x - startX const maxDragDistance = monthView.width * 0.4 // Reduced to 40% of width for more responsive feel // Calculate scroll percentage (-1.0 to 1.0) scrollPercentage = Math.max(-1.0, Math.min(1.0, dragDistance / maxDragDistance)) // Only trigger month change when we've scrolled enough if (Math.abs(scrollPercentage) >= scrollSpeed) { if (scrollPercentage > 0) { // Scrolling right - previous month if (monthView.currentIndex === 0) { root.currentYear-- monthView.currentIndex = 11 } else { monthView.decrementCurrentIndex() } } else { // Scrolling left - next month if (monthView.currentIndex === 11) { root.currentYear++ monthView.currentIndex = 0 } else { monthView.incrementCurrentIndex() } } // Reset scroll percentage after month change scrollPercentage = 0.0 startX = mouse.x } } } model: 12 // Show all months currentIndex: new Date().getMonth() // Ensure current month is active and fully visible when calendar opens Component.onCompleted: { currentIndex = new Date().getMonth() // Force immediate opacity update for current month for (let i = 0; i < count; i++) { const item = itemAt(i) if (item) { item.opacity = i === currentIndex ? 1 : 0.2 } } } delegate: Grid { width: monthView.width height: monthView.height opacity: monthView.currentIndex === index ? 1 : 0.2 // Dim non-current months Behavior on opacity { NumberAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } } columns: 7 spacing: Appearance.spacing.small // Day headers Repeater { // English notation // model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] // German notation model: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] StyledText { width: 30 horizontalAlignment: Text.AlignHCenter text: modelData font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } } // Calendar days Repeater { model: { const firstDay = new Date(root.currentYear, monthView.currentIndex, 1); const lastDay = new Date(root.currentYear, monthView.currentIndex + 1, 0); const daysInMonth = lastDay.getDate(); const firstDayOfWeek = firstDay.getDay() || 7; // Convert Sunday (0) to 7 const today = new Date(); let days = []; // Add previous month's days const prevMonthLastDay = new Date(root.currentYear, monthView.currentIndex, 0).getDate(); for (let i = firstDayOfWeek - 1; i > 0; i--) { const date = new Date(root.currentYear, monthView.currentIndex - 1, prevMonthLastDay - i + 1); days.push({ day: prevMonthLastDay - i + 1, isCurrentMonth: false, isNextMonth: false, date: date }); } // Add days of the current month for (let i = 1; i <= daysInMonth; i++) { const date = new Date(root.currentYear, monthView.currentIndex, i); const isToday = i === today.getDate() && monthView.currentIndex === today.getMonth() && root.currentYear === today.getFullYear(); days.push({ day: i, isCurrentMonth: true, isToday: isToday, isNextMonth: false, date: date }); } // Add next month's days const remainingCells = Math.ceil((firstDayOfWeek - 1 + daysInMonth) / 7) * 7 - days.length; for (let i = 1; i <= remainingCells; i++) { const date = new Date(root.currentYear, monthView.currentIndex + 1, i); days.push({ day: i, isCurrentMonth: false, isNextMonth: true, date: date }); } return days; } Rectangle { width: 30 height: 30 radius: Appearance.rounding.full color: { if (modelData.date.getTime() === root.selectedStartDate.getTime()) { return Colours.palette.m3primary } else if (modelData.date.getTime() === root.selectedEndDate.getTime()) { return Colours.palette.m3secondary } else if (modelData.isToday) { return Colours.palette.m3tertiary } return "transparent" } border.width: modelData.isToday ? 2 : 0 border.color: Colours.palette.m3outline MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: function(mouse) { if (!modelData.date) return if (mouse.button === Qt.RightButton) { root.selectedStartDate = modelData.date root.hasSelection = true root.calculateDays() } else if (mouse.button === Qt.LeftButton) { if (root.selectedStartDate) { root.selectedEndDate = modelData.date root.hasSelection = true root.calculateDays() } } } } StyledText { anchors.centerIn: parent text: modelData.day font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono color: { if (modelData.date.getTime() === root.selectedStartDate.getTime() || modelData.date.getTime() === root.selectedEndDate.getTime()) { return Colours.palette.m3onPrimary } else if (modelData.isToday) { return Colours.palette.m3onTertiary } else if (modelData.isCurrentMonth) { return Colours.palette.m3onSurface } else if (modelData.isNextMonth) { return "#404040" } else { return "#505050" } } } } } } onCurrentIndexChanged: { root.currentDate = new Date(root.currentYear, currentIndex, 1) root.isCurrentMonth = currentIndex === new Date().getMonth() && root.currentYear === new Date().getFullYear() } } } // Usage instructions Column { width: 280 anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small StyledText { text: " Usage:" font.pointSize: Appearance.font.size.tiny color: "#505050" } StyledText { text: " • Scroll to navigate" font.pointSize: Appearance.font.size.tiny color: "#505050" } StyledText { text: " • Right-click calculates days from/till today" font.pointSize: Appearance.font.size.tiny color: "#505050" } } // Reset to current month when popout closes Connections { target: root.parent function onVisibleChanged() { if (!root.parent.visible) { monthView.currentIndex = new Date().getMonth() root.currentYear = new Date().getFullYear() root.currentDate = new Date() root.isCurrentMonth = true root.resetSelection() } } } }