From b6f9e51b65cd4fad354f7604e75960d3ab88647a Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 25 Mar 2021 23:26:54 +0000 Subject: [PATCH] Redesign calendar component 1. This removes the grid around the items 2. The header component is now optional and the API caller can disable it to add its own. 3. It is now using a SwipeView to provide better touch navigation 4. Use buttons to navigate to year/month/decade view instead of a non-discoverable click on the heading 5. Add some convenient functions to manipulate the view and get its state 6. Add a bit of documentation Implements https://phabricator.kde.org/file/data/wiv5bs4ucvqnfiibxtjy/PHID-FILE-nnurs2z323lvwrjsavun/system_tray_w%E2%81%84headers_2x.png --- .../calendar/qml/DaysCalendar.qml | 188 +-------- .../calendar/qml/MonthView.qml | 359 +++++++++++------- 2 files changed, 234 insertions(+), 313 deletions(-) diff --git a/src/declarativeimports/calendar/qml/DaysCalendar.qml b/src/declarativeimports/calendar/qml/DaysCalendar.qml index eb4d308bc..00b52b9a0 100644 --- a/src/declarativeimports/calendar/qml/DaysCalendar.qml +++ b/src/declarativeimports/calendar/qml/DaysCalendar.qml @@ -20,37 +20,27 @@ Item { signal headerClicked - signal previous - signal next - signal activated(int index, var date, var item) // so it forwards it to the delegate which then emits activated with all the necessary data signal activateHighlightedItem readonly property int gridColumns: showWeekNumbers ? calendarGrid.columns + 1 : calendarGrid.columns - property alias previousLabel: previousButton.tooltip - property alias nextLabel: nextButton.tooltip - property int rows property int columns property bool showWeekNumbers - onShowWeekNumbersChanged: canvas.requestPaint() - // how precise date matching should be, 3 = day+month+year, 2 = month+year, 1 = just year property int dateMatchingPrecision property alias headerModel: days.model property alias gridModel: repeater.model - property alias title: heading.text - // Take the calendar width, subtract the inner and outer spacings and divide by number of columns (==days in week) - readonly property int cellWidth: Math.floor((stack.width - (daysCalendar.columns + 1) * root.borderWidth) / (daysCalendar.columns + (showWeekNumbers ? 1 : 0))) + readonly property int cellWidth: Math.floor((swipeView.width - (daysCalendar.columns + 1) * root.borderWidth) / (daysCalendar.columns + (showWeekNumbers ? 1 : 0))) // Take the calendar height, subtract the inner spacings and divide by number of rows (root.weeks + one row for day names) - readonly property int cellHeight: Math.floor((stack.height - heading.height - calendarGrid.rows * root.borderWidth) / calendarGrid.rows) + readonly property int cellHeight: Math.floor((swipeView.height - heading.height - calendarGrid.rows * root.borderWidth) / calendarGrid.rows) property real transformScale: 1 property point transformOrigin: Qt.point(width / 2, height / 2) @@ -76,166 +66,6 @@ Item { } } - RowLayout { - anchors { - top: parent.top - left: canvas.left - right: canvas.right - } - spacing: PlasmaCore.Units.smallSpacing - - PlasmaExtras.Heading { - id: heading - - Layout.fillWidth: true - - level: 2 - elide: Text.ElideRight - font.capitalization: Font.Capitalize - //SEE QTBUG-58307 - //try to make all heights an even number, otherwise the layout engine gets confused - Layout.preferredHeight: implicitHeight + implicitHeight%2 - - MouseArea { - id: monthMouse - property int previousPixelDelta - - anchors.fill: parent - onClicked: { - if (!stack.busy) { - daysCalendar.headerClicked() - } - } - onExited: previousPixelDelta = 0 - onWheel: { - var delta = wheel.angleDelta.y || wheel.angleDelta.x - var pixelDelta = wheel.pixelDelta.y || wheel.pixelDelta.x - - // For high-precision touchpad scrolling, we get a wheel event for basically every slightest - // finger movement. To prevent the view from suddenly ending up in the next century, we - // cumulate all the pixel deltas until they're larger than the label and then only change - // the month. Standard mouse wheel scrolling is unaffected since it's fine. - if (pixelDelta) { - if (Math.abs(previousPixelDelta) < monthMouse.height) { - previousPixelDelta += pixelDelta - return - } - } - - if (delta >= 15) { - daysCalendar.previous() - } else if (delta <= -15) { - daysCalendar.next() - } - previousPixelDelta = 0 - } - } - } - - Components.ToolButton { - id: previousButton - property string tooltip - - icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-next" : "go-previous" - onClicked: daysCalendar.previous() - Accessible.name: tooltip - Components.ToolTip { text: parent.tooltip } - //SEE QTBUG-58307 - Layout.preferredHeight: implicitHeight + implicitHeight%2 - } - - Components.ToolButton { - icon.name: "go-jump-today" - property string tooltip - - onClicked: root.resetToToday() - tooltip: i18ndc("libplasma5", "Reset calendar to today", "Today") - Accessible.name: tooltip - Accessible.description: i18nd("libplasma5", "Reset calendar to today") - Components.ToolTip { text: parent.tooltip } - //SEE QTBUG-58307 - Layout.preferredHeight: implicitHeight + implicitHeight%2 - } - - Components.ToolButton { - id: nextButton - property string tooltip - - icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-previous" : "go-next" - onClicked: daysCalendar.next() - Accessible.name: tooltip - Components.ToolTip { text: parent.tooltip } - //SEE QTBUG-58307 - Layout.preferredHeight: implicitHeight + implicitHeight%2 - } - } - - // Paints the inner grid and the outer frame - Canvas { - id: canvas - - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - } - width: (daysCalendar.cellWidth + root.borderWidth) * gridColumns + root.borderWidth - height: (daysCalendar.cellHeight + root.borderWidth) * calendarGrid.rows + root.borderWidth - - opacity: root.borderOpacity - antialiasing: false - clip: false - onPaint: { - var ctx = getContext("2d"); - // this is needed as otherwise the canvas seems to have some sort of - // inner clip region which does not update on size changes - ctx.reset(); - ctx.save(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.strokeStyle = PlasmaCore.Theme.textColor; - ctx.lineWidth = root.borderWidth - ctx.globalAlpha = 1.0; - - ctx.beginPath(); - - // When line is more wide than 1px, it is painted with 1px line at the actual coords - // and then 1px lines are added first to the left of the middle then right (then left again) - // So all the lines need to be offset a bit to have their middle point in the center - // of the grid spacing rather than on the left most pixel, otherwise they will be painted - // over the days grid which will be visible on eg. mouse hover - var lineBasePoint = Math.floor(root.borderWidth / 2) - - // horizontal lines - for (var i = 0; i < calendarGrid.rows + 1; i++) { - var lineY = lineBasePoint + (daysCalendar.cellHeight + root.borderWidth) * (i); - - if (i == 0 || i == calendarGrid.rows) { - ctx.moveTo(0, lineY); - } else { - ctx.moveTo(showWeekNumbers ? daysCalendar.cellWidth + root.borderWidth : root.borderWidth, lineY); - } - ctx.lineTo(width, lineY); - } - - // vertical lines - for (var i = 0; i < gridColumns + 1; i++) { - var lineX = lineBasePoint + (daysCalendar.cellWidth + root.borderWidth) * (i); - - // Draw the outer vertical lines in full height so that it closes - // the outer rectangle - if (i == 0 || i == gridColumns || !daysCalendar.headerModel) { - ctx.moveTo(lineX, 0); - } else { - ctx.moveTo(lineX, root.borderWidth + daysCalendar.cellHeight); - } - ctx.lineTo(lineX, height); - } - - ctx.closePath(); - ctx.stroke(); - ctx.restore(); - } - } - PlasmaCore.Svg { id: calendarSvg imagePath: "widgets/calendar" @@ -251,20 +81,13 @@ Item { } } - Connections { - target: theme - function onTextColorChanged() { - canvas.requestPaint(); - } - } - Column { id: weeksColumn visible: showWeekNumbers anchors { - top: canvas.top + top: parent.top left: parent.left - bottom: canvas.bottom + bottom: parent.bottom // The borderWidth needs to be counted twice here because it goes // in fact through two lines - the topmost one (the outer edge) // and then the one below weekday strings @@ -291,7 +114,8 @@ Item { id: calendarGrid anchors { - right: canvas.right + top: parent.top + right: parent.right rightMargin: root.borderWidth bottom: parent.bottom bottomMargin: root.borderWidth diff --git a/src/declarativeimports/calendar/qml/MonthView.qml b/src/declarativeimports/calendar/qml/MonthView.qml index fb6c2ce32..624da671b 100644 --- a/src/declarativeimports/calendar/qml/MonthView.qml +++ b/src/declarativeimports/calendar/qml/MonthView.qml @@ -6,15 +6,15 @@ SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.0 -import QtQuick.Controls 1.1 +import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.1 import org.kde.plasma.calendar 2.0 import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.components 3.0 as Components import org.kde.plasma.extras 2.0 as PlasmaExtras -PinchArea { +Item { id: root anchors.fill: parent @@ -39,76 +39,130 @@ PinchArea { property int firstDay: new Date(showDate.getFullYear(), showDate.getMonth(), 1).getDay() property alias today: calendarBackend.today property bool showWeekNumbers: false + property bool showCustomHeader: false + + /** + * Current index of the internal swipeView. Usefull for binding + * a TabBar to it. + */ + property alias currentIndex: swipeView.currentIndex property alias cellHeight: mainDaysCalendar.cellHeight property QtObject daysModel: calendarBackend.daysModel - onPinchStarted: stack.currentItem.transformOrigin = pinch.center - onPinchUpdated: { - var item = stack.currentItem - if (stack.depth < 3 && pinch.scale < 1) { - item.transformScale = pinch.scale - item.opacity = pinch.scale - } else if (stack.depth > 1 && pinch.scale > 1) { - item.transformScale = pinch.scale - item.opacity = (2 - pinch.scale / 2) - } - } - onPinchFinished: { - var item = stack.currentItem - if (item.transformScale < 0.7) { - item.headerClicked() - } else if (item.transformScale > 1.4) { - item.activateHighlightedItem() - } else { - item.transformScale = 1 - item.opacity = 1 - } - } - function isToday(date) { - if (date.toDateString() == new Date().toDateString()) { - return true; - } - - return false; + return date.toDateString() === new Date().toDateString(); } function eventDate(yearNumber,monthNumber,dayNumber) { - var d = new Date(yearNumber, monthNumber-1, dayNumber); + const d = new Date(yearNumber, monthNumber-1, dayNumber); return Qt.formatDate(d, "dddd dd MMM yyyy"); } + /** + * Move calendar to month view showing today's date. + */ function resetToToday() { calendarBackend.resetToToday(); root.currentDate = root.today; - stack.pop(null); + swipeView.currentIndex = 0; } function updateYearOverview() { - var date = calendarBackend.displayedDate; - var day = date.getDate(); - var year = date.getFullYear(); + const date = calendarBackend.displayedDate; + const day = date.getDate(); + const year = date.getFullYear(); - for (var i = 0, j = monthModel.count; i < j; ++i) { + for (let i = 0, j = monthModel.count; i < j; ++i) { monthModel.setProperty(i, "yearNumber", year); } } function updateDecadeOverview() { - var date = calendarBackend.displayedDate; - var day = date.getDate(); - var month = date.getMonth() + 1; - var year = date.getFullYear(); - var decade = year - year % 10; + const date = calendarBackend.displayedDate; + const day = date.getDate(); + const month = date.getMonth() + 1; + const year = date.getFullYear(); + const decade = year - year % 10; - for (var i = 0, j = yearModel.count; i < j; ++i) { - var label = decade - 1 + i; + for (let i = 0, j = yearModel.count; i < j; ++i) { + const label = decade - 1 + i; yearModel.setProperty(i, "yearNumber", label); yearModel.setProperty(i, "label", label); } } + /** + * Possible calendar views + */ + enum CalendarView { + DayView, + MonthView, + YearView + } + + /** + * Go to the next month/year/decade depending on the current + * calendar view displayed. + */ + function nextFrame() { + if (swipeView.currentIndex === 0) { + calendarBackend.nextMonth(); + } else if (swipeView.currentIndex === 1) { + calendarBackend.nextYear(); + } else if (swipeView.currentIndex === 2) { + calendarBackend.nextDecade(); + } + } + + /** + * Go to the previous month/year/decade depending on the current + * calendar view displayed. + */ + function previousFrame() { + if (swipeView.currentIndex === 0) { + calendarBackend.previousMonth(); + } else if (swipeView.currentIndex === 1) { + calendarBackend.previousYear(); + } else if (swipeView.currentIndex === 2) { + calendarBackend.previousDecade(); + } + } + + /** + * \return CalendarView + */ + readonly property var calendarViewDisplayed: { + if (swipeView.currentIndex === 0) { + return MonthView.CalendarView.DayView; + } else if (swipeView.currentIndex === 1) { + return MonthView.CalendarView.MonthView; + } else if (swipeView.currentIndex === 2) { + return MonthView.CalendarView.YearView; + } + } + + /** + * Show month view. + */ + function showMonthView() { + swipeView.currentIndex = 0; + } + + /** + * Show year view. + */ + function showYearView() { + swipeView.currentIndex = 1; + } + + /** + * Show month view. + */ + function showDecadeView() { + swipeView.currentIndex = 2; + } + Calendar { id: calendarBackend @@ -148,6 +202,8 @@ PinchArea { Component.onCompleted: { for (var i = 0; i < 12; ++i) { append({ + label: 2050, // this value will be overwritten, but it set the type of the property to int + yearNumber: 2050, isCurrent: (i > 0 && i < 11) // first and last year are outside the decade }) } @@ -165,63 +221,136 @@ PinchArea { onPressed: mouse.accepted = false } - StackView { - id: stack + ColumnLayout { + id: viewHeader + visible: !showCustomHeader + height: !visible ? 0 : implicitHeight + width: parent.width + anchors { + top: parent.top + } - anchors.fill: parent + RowLayout { + PlasmaExtras.Heading { + id: heading + text: i18ndc("libplasma5", "Format: **month** year", "%1 %2", root.selectedMonth, root.selectedYear.toString()) - delegate: StackViewDelegate { - pushTransition: StackViewTransition { - NumberAnimation { - target: exitItem - duration: PlasmaCore.Units.longDuration - property: "opacity" - from: 1 - to: 0 - } - NumberAnimation { - target: enterItem - duration: PlasmaCore.Units.longDuration - property: "opacity" - from: 0 - to: 1 - } - NumberAnimation { - target: enterItem - duration: PlasmaCore.Units.longDuration - property: "transformScale" - from: 1.5 - to: 1 - } + level: 2 + elide: Text.ElideRight + font.capitalization: Font.Capitalize + //SEE QTBUG-58307 + //try to make all heights an even number, otherwise the layout engine gets confused + Layout.preferredHeight: implicitHeight + implicitHeight%2 + Layout.fillWidth: true } - popTransition: StackViewTransition { - NumberAnimation { - target: exitItem - duration: PlasmaCore.Units.longDuration - property: "opacity" - from: 1 - to: 0 + Row { + spacing: 0 + Components.ToolButton { + id: previousButton + property string tooltip: { + switch(root.calendarViewDisplayed) { + case MonthView.CalendarView.DayView: + return i18nd("libplasma5", "Previous Month") + case MonthView.CalendarView.MonthView: + return i18nd("libplasma5", "Previous Year") + case MonthView.CalendarView.YearView: + return i18nd("libplasma5", "Previous Decade") + default: + return ""; + } + } + + //SEE QTBUG-58307 + Layout.preferredHeight: implicitHeight + implicitHeight % 2 + icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-next" : "go-previous" + onClicked: root.previousFrame() + Accessible.name: tooltip + Components.ToolTip { text: parent.tooltip } } - NumberAnimation { - target: exitItem - duration: PlasmaCore.Units.longDuration - property: "transformScale" - // so no matter how much you scaled, it would still fly towards you - to: exitItem.transformScale * 1.5 + + Components.ToolButton { + icon.name: "go-jump-today" + property string tooltip + + //SEE QTBUG-58307 + Layout.preferredHeight: implicitHeight + implicitHeight % 2 + onClicked: root.resetToToday() + Components.ToolTip { + text: i18ndc("libplasma5", "Reset calendar to today", "Today") + } + Accessible.name: tooltip + Accessible.description: i18nd("libplasma5", "Reset calendar to today") } - NumberAnimation { - target: enterItem - duration: PlasmaCore.Units.longDuration - property: "opacity" - from: 0 - to: 1 + + Components.ToolButton { + id: nextButton + property string tooltip: { + switch(root.calendarViewDisplayed) { + case MonthView.CalendarView.DayView: + return i18nd("libplasma5", "Next Month") + case MonthView.CalendarView.MonthView: + return i18nd("libplasma5", "Next Year") + case MonthView.CalendarView.YearView: + return i18nd("libplasma5", "Next Decade") + default: + return ""; + } + } + + //SEE QTBUG-58307 + Layout.preferredHeight: implicitHeight + implicitHeight % 2 + icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-previous" : "go-next" + Components.ToolTip { text: parent.tooltip } + onClicked: root.nextFrame(); + Accessible.name: tooltip } } } - initialItem: DaysCalendar { + Components.TabBar { + id: tabBar + currentIndex: swipeView.currentIndex + Layout.preferredWidth: contentWidth + Layout.alignment: Qt.AlignRight + Components.TabButton { + text: i18nc("libplasma5", "Days"); + onClicked: root.showMonthView(); + opacity: root.calendarViewDisplayed === MonthView.CalendarView.MonthView ? 1 : 0.8 + width: implicitWidth + } + Components.TabButton { + text: i18nd("libplasma5", "Months"); + onClicked: root.showYearView(); + opacity: root.calendarViewDisplayed === MonthView.CalendarView.YearView ? 1 : 0.7 + width: implicitWidth + } + Components.TabButton { + text: i18nd("libplasma5", "Years"); + onClicked: root.showDecadeView(); + opacity: root.calendarViewDisplayed === MonthView.CalendarView.DecadeView ? 1 : 0.7 + width: implicitWidth + } + } + } + + QQC2.SwipeView { + id: swipeView + orientation: Qt.Vertical + anchors { + top: viewHeader.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + clip: true + + onCurrentIndexChanged: if (currentIndex > 1) { + updateDecadeOverview(); + } + + // MonthView + DaysCalendar { id: mainDaysCalendar - title: calendarBackend.displayedDate.getFullYear() == new Date().getFullYear() ? root.selectedMonth : root.selectedMonth + ", " + root.selectedYear columns: calendarBackend.days rows: calendarBackend.weeks @@ -233,75 +362,43 @@ PinchArea { dateMatchingPrecision: Calendar.MatchYearMonthAndDay - previousLabel: i18nd("libplasma5", "Previous Month") - nextLabel: i18nd("libplasma5", "Next Month") - - onPrevious: calendarBackend.previousMonth() - onNext: calendarBackend.nextMonth() - onHeaderClicked: { - stack.push(yearOverview) - } onActivated: { - var rowNumber = Math.floor(index / columns); + const rowNumber = Math.floor(index / columns); week = 1 + calendarBackend.weeksModel[rowNumber]; root.currentDate = new Date(date.yearNumber, date.monthNumber - 1, date.dayNumber) } } - } - - Component { - id: yearOverview + // YearView DaysCalendar { - title: calendarBackend.displayedDate.getFullYear() columns: 3 rows: 4 dateMatchingPrecision: Calendar.MatchYearAndMonth gridModel: monthModel - - previousLabel: i18nd("libplasma5", "Previous Year") - nextLabel: i18nd("libplasma5", "Next Year") - - onPrevious: calendarBackend.previousYear() - onNext: calendarBackend.nextYear() - onHeaderClicked: { - updateDecadeOverview(); - stack.push(decadeOverview) - } onActivated: { - calendarBackend.goToMonth(date.monthNumber) - stack.pop() + calendarBackend.goToMonth(date.monthNumber); + swipeView.currentIndex = 0; } } - } - - Component { - id: decadeOverview + // DecadeView DaysCalendar { readonly property int decade: { - var year = calendarBackend.displayedDate.getFullYear() + const year = calendarBackend.displayedDate.getFullYear() return year - year % 10 } - title: decade + " – " + (decade + 9) columns: 3 rows: 4 dateMatchingPrecision: Calendar.MatchYear gridModel: yearModel - - previousLabel: i18nd("libplasma5", "Previous Decade") - nextLabel: i18nd("libplasma5", "Next Decade") - - onPrevious: calendarBackend.previousDecade() - onNext: calendarBackend.nextDecade() onActivated: { - calendarBackend.goToYear(date.yearNumber) - stack.pop() + calendarBackend.goToYear(date.yearNumber); + swipeView.currentIndex = 1; } } }