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 headerClicked
signal previous
signal next
signal activated(int index, var date, var item) signal activated(int index, var date, var item)
// so it forwards it to the delegate which then emits activated with all the necessary data // so it forwards it to the delegate which then emits activated with all the necessary data
signal activateHighlightedItem signal activateHighlightedItem
readonly property int gridColumns: showWeekNumbers ? calendarGrid.columns + 1 : calendarGrid.columns 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 rows
property int columns property int columns
property bool showWeekNumbers property bool showWeekNumbers
onShowWeekNumbersChanged: canvas.requestPaint()
// how precise date matching should be, 3 = day+month+year, 2 = month+year, 1 = just year // how precise date matching should be, 3 = day+month+year, 2 = month+year, 1 = just year
property int dateMatchingPrecision property int dateMatchingPrecision
property alias headerModel: days.model property alias headerModel: days.model
property alias gridModel: repeater.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) // 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) // 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 real transformScale: 1
property point transformOrigin: Qt.point(width / 2, height / 2) 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 { PlasmaCore.Svg {
id: calendarSvg id: calendarSvg
imagePath: "widgets/calendar" imagePath: "widgets/calendar"
@ -251,20 +81,13 @@ Item {
} }
} }
Connections {
target: theme
function onTextColorChanged() {
canvas.requestPaint();
}
}
Column { Column {
id: weeksColumn id: weeksColumn
visible: showWeekNumbers visible: showWeekNumbers
anchors { anchors {
top: canvas.top top: parent.top
left: parent.left left: parent.left
bottom: canvas.bottom bottom: parent.bottom
// The borderWidth needs to be counted twice here because it goes // The borderWidth needs to be counted twice here because it goes
// in fact through two lines - the topmost one (the outer edge) // in fact through two lines - the topmost one (the outer edge)
// and then the one below weekday strings // and then the one below weekday strings
@ -291,7 +114,8 @@ Item {
id: calendarGrid id: calendarGrid
anchors { anchors {
right: canvas.right top: parent.top
right: parent.right
rightMargin: root.borderWidth rightMargin: root.borderWidth
bottom: parent.bottom bottom: parent.bottom
bottomMargin: root.borderWidth bottomMargin: root.borderWidth

View File

@ -6,15 +6,15 @@
SPDX-License-Identifier: GPL-2.0-or-later SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 1.1 import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import org.kde.plasma.calendar 2.0 import org.kde.plasma.calendar 2.0
import org.kde.plasma.core 2.0 as PlasmaCore 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 import org.kde.plasma.extras 2.0 as PlasmaExtras
PinchArea { Item {
id: root id: root
anchors.fill: parent anchors.fill: parent
@ -39,76 +39,130 @@ PinchArea {
property int firstDay: new Date(showDate.getFullYear(), showDate.getMonth(), 1).getDay() property int firstDay: new Date(showDate.getFullYear(), showDate.getMonth(), 1).getDay()
property alias today: calendarBackend.today property alias today: calendarBackend.today
property bool showWeekNumbers: false 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 alias cellHeight: mainDaysCalendar.cellHeight
property QtObject daysModel: calendarBackend.daysModel 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) { function isToday(date) {
if (date.toDateString() == new Date().toDateString()) { return date.toDateString() === new Date().toDateString();
return true;
}
return false;
} }
function eventDate(yearNumber,monthNumber,dayNumber) { 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"); return Qt.formatDate(d, "dddd dd MMM yyyy");
} }
/**
* Move calendar to month view showing today's date.
*/
function resetToToday() { function resetToToday() {
calendarBackend.resetToToday(); calendarBackend.resetToToday();
root.currentDate = root.today; root.currentDate = root.today;
stack.pop(null); swipeView.currentIndex = 0;
} }
function updateYearOverview() { function updateYearOverview() {
var date = calendarBackend.displayedDate; const date = calendarBackend.displayedDate;
var day = date.getDate(); const day = date.getDate();
var year = date.getFullYear(); 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); monthModel.setProperty(i, "yearNumber", year);
} }
} }
function updateDecadeOverview() { function updateDecadeOverview() {
var date = calendarBackend.displayedDate; const date = calendarBackend.displayedDate;
var day = date.getDate(); const day = date.getDate();
var month = date.getMonth() + 1; const month = date.getMonth() + 1;
var year = date.getFullYear(); const year = date.getFullYear();
var decade = year - year % 10; const decade = year - year % 10;
for (var i = 0, j = yearModel.count; i < j; ++i) { for (let i = 0, j = yearModel.count; i < j; ++i) {
var label = decade - 1 + i; const label = decade - 1 + i;
yearModel.setProperty(i, "yearNumber", label); yearModel.setProperty(i, "yearNumber", label);
yearModel.setProperty(i, "label", 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 { Calendar {
id: calendarBackend id: calendarBackend
@ -148,6 +202,8 @@ PinchArea {
Component.onCompleted: { Component.onCompleted: {
for (var i = 0; i < 12; ++i) { for (var i = 0; i < 12; ++i) {
append({ 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 isCurrent: (i > 0 && i < 11) // first and last year are outside the decade
}) })
} }
@ -165,63 +221,136 @@ PinchArea {
onPressed: mouse.accepted = false onPressed: mouse.accepted = false
} }
StackView { ColumnLayout {
id: stack 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 { level: 2
pushTransition: StackViewTransition { elide: Text.ElideRight
NumberAnimation { font.capitalization: Font.Capitalize
target: exitItem //SEE QTBUG-58307
duration: PlasmaCore.Units.longDuration //try to make all heights an even number, otherwise the layout engine gets confused
property: "opacity" Layout.preferredHeight: implicitHeight + implicitHeight%2
from: 1 Layout.fillWidth: true
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
}
} }
popTransition: StackViewTransition { Row {
NumberAnimation { spacing: 0
target: exitItem Components.ToolButton {
duration: PlasmaCore.Units.longDuration id: previousButton
property: "opacity" property string tooltip: {
from: 1 switch(root.calendarViewDisplayed) {
to: 0 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 Components.ToolButton {
duration: PlasmaCore.Units.longDuration icon.name: "go-jump-today"
property: "transformScale" property string tooltip
// so no matter how much you scaled, it would still fly towards you
to: exitItem.transformScale * 1.5 //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 Components.ToolButton {
duration: PlasmaCore.Units.longDuration id: nextButton
property: "opacity" property string tooltip: {
from: 0 switch(root.calendarViewDisplayed) {
to: 1 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 id: mainDaysCalendar
title: calendarBackend.displayedDate.getFullYear() == new Date().getFullYear() ? root.selectedMonth : root.selectedMonth + ", " + root.selectedYear
columns: calendarBackend.days columns: calendarBackend.days
rows: calendarBackend.weeks rows: calendarBackend.weeks
@ -233,75 +362,43 @@ PinchArea {
dateMatchingPrecision: Calendar.MatchYearMonthAndDay dateMatchingPrecision: Calendar.MatchYearMonthAndDay
previousLabel: i18nd("libplasma5", "Previous Month")
nextLabel: i18nd("libplasma5", "Next Month")
onPrevious: calendarBackend.previousMonth()
onNext: calendarBackend.nextMonth()
onHeaderClicked: {
stack.push(yearOverview)
}
onActivated: { onActivated: {
var rowNumber = Math.floor(index / columns); const rowNumber = Math.floor(index / columns);
week = 1 + calendarBackend.weeksModel[rowNumber]; week = 1 + calendarBackend.weeksModel[rowNumber];
root.currentDate = new Date(date.yearNumber, date.monthNumber - 1, date.dayNumber) root.currentDate = new Date(date.yearNumber, date.monthNumber - 1, date.dayNumber)
} }
} }
}
Component {
id: yearOverview
// YearView
DaysCalendar { DaysCalendar {
title: calendarBackend.displayedDate.getFullYear()
columns: 3 columns: 3
rows: 4 rows: 4
dateMatchingPrecision: Calendar.MatchYearAndMonth dateMatchingPrecision: Calendar.MatchYearAndMonth
gridModel: monthModel gridModel: monthModel
previousLabel: i18nd("libplasma5", "Previous Year")
nextLabel: i18nd("libplasma5", "Next Year")
onPrevious: calendarBackend.previousYear()
onNext: calendarBackend.nextYear()
onHeaderClicked: {
updateDecadeOverview();
stack.push(decadeOverview)
}
onActivated: { onActivated: {
calendarBackend.goToMonth(date.monthNumber) calendarBackend.goToMonth(date.monthNumber);
stack.pop() swipeView.currentIndex = 0;
} }
} }
}
Component {
id: decadeOverview
// DecadeView
DaysCalendar { DaysCalendar {
readonly property int decade: { readonly property int decade: {
var year = calendarBackend.displayedDate.getFullYear() const year = calendarBackend.displayedDate.getFullYear()
return year - year % 10 return year - year % 10
} }
title: decade + " " + (decade + 9)
columns: 3 columns: 3
rows: 4 rows: 4
dateMatchingPrecision: Calendar.MatchYear dateMatchingPrecision: Calendar.MatchYear
gridModel: yearModel gridModel: yearModel
previousLabel: i18nd("libplasma5", "Previous Decade")
nextLabel: i18nd("libplasma5", "Next Decade")
onPrevious: calendarBackend.previousDecade()
onNext: calendarBackend.nextDecade()
onActivated: { onActivated: {
calendarBackend.goToYear(date.yearNumber) calendarBackend.goToYear(date.yearNumber);
stack.pop() swipeView.currentIndex = 1;
} }
} }
} }