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
This commit is contained in:
Carl Schwan 2021-03-25 23:26:54 +00:00
parent 53b8883019
commit b6f9e51b65
2 changed files with 234 additions and 313 deletions

View File

@ -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

View File

@ -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", "<strong>%1</strong> %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;
}
}
}