From cd9200ffe6e531298187cb844823103bd0a2a1a3 Mon Sep 17 00:00:00 2001 From: Tanbir Jishan Date: Sun, 11 Sep 2022 13:28:49 +0000 Subject: [PATCH] components/calendar: Animate the view when date changes The date can be changed using scrolling or the buttons provided. It does not animate the view though. With this MR I have tried to implement this. Though the goal here is not just animating the view. The user can now flick the view as well to change date. There is already https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/1843 this MR that does all of this and more. But there was many controversial changes and the code was a mess. The way animation was being handled could be improved. This is a much more smaller version of that MR. I have tried to make as less changes as possible to the MonthView since it is already very messy. All I have done is creating a new component and wrapped the views in that component. The code changes not as much in the MonthView as it seems in the diff. Here's what I am talking about: ![anim-cal](/uploads/1c1bffebbe11a9e4ebb3c110cfd1bf8c/anim-cal.webm) --- .../package/contents/ui/CalendarView.qml | 2 + components/calendar/calendar.cpp | 6 + components/calendar/calendar.h | 3 + components/calendar/qml/DaysCalendar.qml | 2 - components/calendar/qml/InfiniteList.qml | 261 ++++++++++++++++++ components/calendar/qml/MonthView.qml | 152 ++++++---- components/calendar/qml/qmldir | 1 + 7 files changed, 369 insertions(+), 58 deletions(-) create mode 100644 components/calendar/qml/InfiniteList.qml diff --git a/applets/digital-clock/package/contents/ui/CalendarView.qml b/applets/digital-clock/package/contents/ui/CalendarView.qml index b03a0d93d..fd413caa3 100644 --- a/applets/digital-clock/package/contents/ui/CalendarView.qml +++ b/applets/digital-clock/package/contents/ui/CalendarView.qml @@ -710,6 +710,7 @@ PlasmaExtras.Representation { anchors.bottom: parent.bottom onActiveFocusChanged: if (activeFocus) { monthViewWrapper.nextItemInFocusChain().forceActiveFocus(); + monthView.Keys.onDownPressed(null) } PlasmaCalendar.MonthView { id: monthView @@ -726,6 +727,7 @@ PlasmaExtras.Representation { KeyNavigation.left: KeyNavigation.tab KeyNavigation.tab: addEventButton.visible ? addEventButton : addEventButton.KeyNavigation.down Keys.onUpPressed: tabbar.currentItem.forceActiveFocus(Qt.BacktabFocusReason); + onUpPressed: Keys.onUpPressed(event) } } } diff --git a/components/calendar/calendar.cpp b/components/calendar/calendar.cpp index 372f51e07..9630d1f53 100644 --- a/components/calendar/calendar.cpp +++ b/components/calendar/calendar.cpp @@ -41,6 +41,7 @@ Calendar::Calendar(QObject *parent) { // m_dayHelper = new CalendarDayHelper(this); // connect(m_dayHelper, SIGNAL(calendarChanged()), this, SLOT(updateData())); + connect(this, &Calendar::monthNameChanged, this, &Calendar::monthChanged); } Calendar::~Calendar() @@ -221,6 +222,11 @@ int Calendar::year() const return d->m_displayedDate.year(); } +int Calendar::month() const +{ + return d->m_displayedDate.month(); +} + QAbstractItemModel *Calendar::daysModel() const { return d->m_daysModel; diff --git a/components/calendar/calendar.h b/components/calendar/calendar.h index 6cf602bde..e8a2140bd 100644 --- a/components/calendar/calendar.h +++ b/components/calendar/calendar.h @@ -103,6 +103,7 @@ class Calendar : public QObject * where you would want the short month name. */ Q_PROPERTY(QString monthName READ monthName NOTIFY monthNameChanged) + Q_PROPERTY(int month READ month NOTIFY monthChanged) /** * This model contains the actual grid data of days. For example, if you had set: @@ -164,6 +165,7 @@ public: // Month name QString monthName() const; + int month() const; int year() const; // Models @@ -192,6 +194,7 @@ Q_SIGNALS: void firstDayOfWeekChanged(); void errorMessageChanged(); void monthNameChanged(); + void monthChanged(); void yearChanged(); void weeksModelChanged(); diff --git a/components/calendar/qml/DaysCalendar.qml b/components/calendar/qml/DaysCalendar.qml index f9031f76c..1bd5b236f 100644 --- a/components/calendar/qml/DaysCalendar.qml +++ b/components/calendar/qml/DaysCalendar.qml @@ -163,8 +163,6 @@ Item { } if (index < (daysCalendar.rows - 1) * daysCalendar.columns) { repeater.itemAt(index + daysCalendar.columns).forceActiveFocus(Qt.TabFocusReason); - } else { - daysCalendar.scrollDown(); } } diff --git a/components/calendar/qml/InfiniteList.qml b/components/calendar/qml/InfiniteList.qml new file mode 100644 index 000000000..42d667725 --- /dev/null +++ b/components/calendar/qml/InfiniteList.qml @@ -0,0 +1,261 @@ +/* + SPDX-FileCopyrightText: 2022 Tanbir Jishan + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.workspace.calendar 2.0 + +Item { + id: root + + required property var backend + required property int viewType + property QtObject eventPluginsManager + property alias delegate: infiniteRepeater.delegate + readonly property alias currentItem: infiniteList.currentItem + + enum ViewType { + DayView, + YearView, + DecadeView + } + + enum AnimationDirection { + Upward, + Downward + } + + SwipeView { + id: infiniteList + + anchors.fill: parent + orientation: Qt.Vertical + currentIndex: 1 //middle of the view, currentIndex always returns back to middle so that user can flick both upward and downward + + property bool handlingIndexChange: false // This var is used to prevent date change ⇆ index change loop + property var highlightMoveDuration: PlasmaCore.Units.longDuration + property int lastMonth: -1 + property int lastYear: -1 + + Connections { + id: dateToViewSynchroniser + target: root.backend + + // Animation is done by moving to the edge with zero animation + // duration and then coming with a non-zero animation duration + onMonthChanged: infiniteList.animateDateChange() + onYearChanged: infiniteList.animateDateChange() + } + + Repeater { + id: infiniteRepeater + model: 3 + } + + onCurrentIndexChanged: adjustDate() + + Component.onCompleted: { + //init vars. last* vars are for tracking whether date changed toward future or past + contentItem.highlightMoveDuration = highlightMoveDuration; + lastMonth = root.backend.month - 1; + lastYear = root.backend.year; + + // set up alternative model for delegates at edges + // so that they can be set up to always shows the last state of the main model + // date here means what the respective views should show(e.g. MonthView date -> month) + // this prevents them from showing the current date when they are being animated out of view + // since different years don't have different names for months, we don't need to set up alternative models for YearView + var alternativeModel = undefined; + if (root.viewType === InfiniteList.ViewType.DayView) { + alternativeModel = backend.daysModel; + } else if (root.viewType === InfiniteList.ViewType.DecadeView) { + alternativeModel = yearModel; + } + if (alternativeModel !== undefined) { + infiniteRepeater.itemAt(0).gridModel = alternativeModel; + infiniteRepeater.itemAt(2).gridModel = alternativeModel; + } + } + +/*----------------------------------------------------- helper functions ---------------------------------------------------------------*/ + + function resetIndexTo(index: int, duration = 0) { + contentItem.highlightMoveDuration = duration; + if (currentIndex !== index) { + currentIndex = index; + } + } + + function changeDateOfView() { + const swipedUp = currentIndex == 2; + + switch(root.viewType) { + case InfiniteList.ViewType.DayView: + swipedUp ? root.backend.nextMonth() : root.backend.previousMonth(); + break; + + case InfiniteList.ViewType.YearView: + swipedUp ? root.backend.nextYear() : root.backend.previousYear(); + break; + + case InfiniteList.ViewType.DecadeView: + swipedUp ? root.backend.nextDecade() : root.backend.previousDecade(); + break; + } + } + + function adjustDate() { + const inMiddle = currentIndex == 1; + if (handlingIndexChange || inMiddle) return; + + handlingIndexChange = true; + changeDateOfView(); + resetIndexTo(1); //back to middle + handlingIndexChange = false; + } + + function animate(direction) { + if (handlingIndexChange) return; + + const targetIndex = (direction === InfiniteList.AnimationDirection.Upward) ? 0 : 2; + + handlingIndexChange = true; + resetIndexTo(targetIndex); //move to edge from middle + resetIndexTo(1, highlightMoveDuration); // come back to middle with non-zero animation duration + handlingIndexChange = false; + } + + function animateDateChange(toFuture = undefined) { + const month = root.backend.month - 1; + const year = root.backend.year; + let goToFuture = false; + + if(toFuture === undefined) { + switch(root.viewType) { + case InfiniteList.ViewType.DayView: + if (month === lastMonth) return; + goToFuture = (month > lastMonth || year > lastYear) && !(year < lastYear); + break; + + default: + if (year === lastYear) return; + goToFuture = year > lastYear; + break; + } + } else { + goToFuture = toFuture; + } + + + if (goToFuture) { + animate(InfiniteList.AnimationDirection.Upward); + } else { + animate(InfiniteList.AnimationDirection.Downward); + } + + lastMonth = month; + lastYear = year; + } + + // used to update the alternative decadeview models when year changes + function updateDecadeOverview() { + const date = backend.displayedDate; + const day = date.getDate(); + const month = date.getMonth() + 1; + const year = date.getFullYear(); + const decade = year - year % 10; + + 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); + } + } +/*----------------------------------------------------- alternative models ---------------------------------------------------------------*/ + + Calendar { + id: backend + + days: root.backend.days + weeks: root.backend.weeks + firstDayOfWeek: root.backend.firstDayOfWeek + today: root.backend.today + + Component.onCompleted: { + daysModel.setPluginsManager(root.eventPluginsManager); + } + } + + ListModel { + id: yearModel + + Component.onCompleted: { + for (let 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 + }) + } + infiniteList.updateDecadeOverview(); + } + } + } + +/*----------------------------------------------------- public functions ---------------------------------------------------------------*/ + + function nextView() { + switch(root.viewType) { + case InfiniteList.ViewType.DayView: + backend.goToMonth(root.backend.month); + backend.goToYear(root.backend.year); + root.backend.nextMonth(); + break; + + case InfiniteList.ViewType.YearView: + root.backend.nextYear(); + break; + + case InfiniteList.ViewType.DecadeView: + backend.goToYear(root.backend.year); + infiniteList.updateDecadeOverview(); + root.backend.nextDecade(); + break; + } + } + + function previousView() { + switch(root.viewType) { + case InfiniteList.ViewType.DayView: + backend.goToMonth(root.backend.month); + backend.goToYear(root.backend.year); + root.backend.previousMonth(); + break; + + case InfiniteList.ViewType.YearView: + root.backend.previousYear(); + break; + + case InfiniteList.ViewType.DecadeView: + backend.goToYear(root.backend.year); + infiniteList.updateDecadeOverview(); + root.backend.previousDecade(); + break; + } + } + + function resetToToday() { + backend.goToMonth(root.backend.month); + backend.goToYear(root.backend.year); + root.backend.resetToToday(); + } + + function focusFirstCellOfView() { + infiniteList.currentItem.repeater.itemAt(0).forceActiveFocus(Qt.TabFocusReason); + infiniteList.resetIndexTo(1) + infiniteList.currentItem.repeater.itemAt(0).forceActiveFocus(Qt.TabFocusReason); + } +} diff --git a/components/calendar/qml/MonthView.qml b/components/calendar/qml/MonthView.qml index 273cfc477..a04c8192b 100644 --- a/components/calendar/qml/MonthView.qml +++ b/components/calendar/qml/MonthView.qml @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later */ -import QtQuick 2.0 +import QtQuick 2.15 import QtQuick.Layouts 1.1 import org.kde.plasma.workspace.calendar 2.0 @@ -71,7 +71,8 @@ Item { KeyNavigation.up: nextButton // The view can have no highlighted item, so always highlight the first item - Keys.onDownPressed: swipeView.currentItem.repeater.itemAt(0).forceActiveFocus(Qt.TabFocusReason); + Keys.onDownPressed: swipeView.currentItem.focusFirstCellOfView() + signal upPressed(var event) function isToday(date) { return date.toDateString() === new Date().toDateString(); @@ -86,7 +87,7 @@ Item { * Move calendar to month view showing today's date. */ function resetToToday() { - calendarBackend.resetToToday(); + mainDaysCalendar.resetToToday(); root.currentDate = root.today; root.currentDateAuxilliaryText = root.todayAuxilliaryText; swipeView.currentIndex = 0; @@ -131,12 +132,13 @@ Item { */ function nextView() { if (swipeView.currentIndex === 0) { - calendarBackend.nextMonth(); + mainDaysCalendar.nextView(); } else if (swipeView.currentIndex === 1) { - calendarBackend.nextYear(); + yearView.nextView(); } else if (swipeView.currentIndex === 2) { - calendarBackend.nextDecade(); + decadeView.nextView(); } + } /** @@ -145,14 +147,15 @@ Item { */ function previousView() { if (swipeView.currentIndex === 0) { - calendarBackend.previousMonth(); + mainDaysCalendar.previousView(); } else if (swipeView.currentIndex === 1) { - calendarBackend.previousYear(); + yearView.previousView(); } else if (swipeView.currentIndex === 2) { - calendarBackend.previousDecade(); + decadeView.previousView(); } } + /** * \return CalendarView */ @@ -181,7 +184,7 @@ Item { } /** - * Show month view. + * Show decade view. */ function showDecadeView() { swipeView.currentIndex = 2; @@ -367,79 +370,116 @@ Item { updateDecadeOverview(); } + onFocusChanged: if(focus) { + currentItem.focusFirstCellOfView(); + } + // MonthView - DaysCalendar { - id: mainDaysCalendar + InfiniteList { + id: mainDaysCalendar + + readonly property double cellHeight: currentItem.cellHeight + + backend: calendarBackend + viewType: InfiniteList.ViewType.DayView + eventPluginsManager: root.eventPluginsManager + + function handleUpPress(event) { + if(root.showCustomHeader) { + root.upPressed(event); + return; + } + swipeView.Keys.onUpPressed(event); + } - columns: calendarBackend.days - rows: calendarBackend.weeks + delegate: DaysCalendar { + columns: calendarBackend.days + rows: calendarBackend.weeks - showWeekNumbers: root.showWeekNumbers + showWeekNumbers: root.showWeekNumbers - headerModel: calendarBackend.days - gridModel: calendarBackend.daysModel + headerModel: calendarBackend.days + gridModel: calendarBackend.daysModel - dateMatchingPrecision: Calendar.MatchYearMonthAndDay + dateMatchingPrecision: Calendar.MatchYearMonthAndDay - KeyNavigation.left: swipeView.KeyNavigation.left - KeyNavigation.tab: swipeView.KeyNavigation.tab + KeyNavigation.left: swipeView.KeyNavigation.left + KeyNavigation.tab: swipeView.KeyNavigation.tab + Keys.onUpPressed: mainDaysCalendar.handleUpPress(event) - onActivated: { - const rowNumber = Math.floor(index / columns); - week = 1 + calendarBackend.weeksModel[rowNumber]; - root.currentDate = new Date(date.yearNumber, date.monthNumber - 1, date.dayNumber) + onActivated: { + const rowNumber = Math.floor(index / columns); + week = 1 + calendarBackend.weeksModel[rowNumber]; + root.currentDate = new Date(date.yearNumber, date.monthNumber - 1, date.dayNumber) - if (date.subLabel) { - root.currentDateAuxilliaryText = date.subLabel; + if (date.subLabel) { + root.currentDateAuxilliaryText = date.subLabel; + } } + onScrollUp: root.nextView() + onScrollDown: root.previousView() } - - onScrollUp: root.nextView() - onScrollDown: root.previousView() } // YearView - DaysCalendar { - columns: 3 - rows: 4 + InfiniteList { + id: yearView - dateMatchingPrecision: Calendar.MatchYearAndMonth + backend: calendarBackend + viewType: InfiniteList.ViewType.YearView + delegate: DaysCalendar { + columns: 3 + rows: 4 - gridModel: monthModel + dateMatchingPrecision: Calendar.MatchYearAndMonth - KeyNavigation.left: swipeView.KeyNavigation.left - KeyNavigation.tab: swipeView.KeyNavigation.tab + gridModel: monthModel - onActivated: { - calendarBackend.goToMonth(date.monthNumber); - swipeView.currentIndex = 0; + KeyNavigation.left: swipeView.KeyNavigation.left + KeyNavigation.tab: swipeView.KeyNavigation.tab + Keys.onUpPressed: mainDaysCalendar.handleUpPress(event) + + onActivated: { + calendarBackend.goToMonth(date.monthNumber); + swipeView.currentIndex = 0; + } + onScrollUp: root.nextView() + onScrollDown: root.previousView() } } // DecadeView - DaysCalendar { - readonly property int decade: { - const year = calendarBackend.displayedDate.getFullYear() - return year - year % 10 - } + InfiniteList { + id: decadeView + + backend: calendarBackend + viewType: InfiniteList.ViewType.DecadeView + delegate: DaysCalendar { + readonly property int decade: { + const year = calendarBackend.displayedDate.getFullYear() + return year - year % 10 + } - columns: 3 - rows: 4 + columns: 3 + rows: 4 + width: decadeView.width + height: decadeView.height + dateMatchingPrecision: Calendar.MatchYear - dateMatchingPrecision: Calendar.MatchYear + gridModel: yearModel - gridModel: yearModel + KeyNavigation.left: swipeView.KeyNavigation.left + KeyNavigation.tab: swipeView.KeyNavigation.tab + Keys.onUpPressed: mainDaysCalendar.handleUpPress(event) - KeyNavigation.left: swipeView.KeyNavigation.left - KeyNavigation.tab: swipeView.KeyNavigation.tab + onActivated: { + calendarBackend.goToYear(date.yearNumber); + swipeView.currentIndex = 1; + } - onActivated: { - calendarBackend.goToYear(date.yearNumber); - swipeView.currentIndex = 1; + onScrollUp: root.nextView() + onScrollDown: root.previousView() } - - onScrollUp: calendarBackend.nextYear() - onScrollDown: calendarBackend.previousYear() } } diff --git a/components/calendar/qml/qmldir b/components/calendar/qml/qmldir index 7b095b03d..63e2a0d84 100644 --- a/components/calendar/qml/qmldir +++ b/components/calendar/qml/qmldir @@ -6,3 +6,4 @@ MonthMenu 2.0 MonthMenu.qml internal CalendarToolbar CalendarToolbar.qml internal DayDelegate DayDelegate.qml internal DaysCalendar DaysCalendar.qml +internal InfiniteList InfiniteList.qml