Commit cd9200ff authored by Tanbir Jishan's avatar Tanbir Jishan Committed by Fushan Wen
Browse files

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 !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)
parent 1119f6fb
Pipeline #230430 passed with stage
in 13 minutes and 28 seconds
......@@ -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)
}
}
}
......@@ -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;
......
......@@ -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();
......
......@@ -163,8 +163,6 @@ Item {
}
if (index < (daysCalendar.rows - 1) * daysCalendar.columns) {
repeater.itemAt(index + daysCalendar.columns).forceActiveFocus(Qt.TabFocusReason);
} else {
daysCalendar.scrollDown();
}
}
......
/*
SPDX-FileCopyrightText: 2022 Tanbir Jishan <tantalising007@gmail.com>
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);
}
}
......@@ -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()
}
}
......
......@@ -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
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment