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