1/*
2    SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
3    SPDX-FileCopyrightText: 2015 Martin Klapetek <mklapetek@kde.org>
4    SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
5
6    SPDX-License-Identifier: GPL-2.0-or-later
7*/
8import QtQuick 2.4
9import QtQuick.Layouts 1.1
10import QtQml 2.15
11
12import org.kde.kquickcontrolsaddons 2.0 // For kcmshell
13import org.kde.plasma.core 2.0 as PlasmaCore
14import org.kde.plasma.calendar 2.0 as PlasmaCalendar
15import org.kde.plasma.components 3.0 as PlasmaComponents3
16import org.kde.plasma.extras 2.0 as PlasmaExtras
17import org.kde.plasma.private.digitalclock 1.0
18
19// Top-level layout containing:
20// - Left column with world clock and agenda view
21// - Right column with current date header and calendar
22PlasmaExtras.Representation {
23    id: calendar
24
25    // The "sensible" values
26    property int _minimumWidth: (calendar.showAgenda || calendar.showClocks) ? PlasmaCore.Units.gridUnit * 45 : PlasmaCore.Units.gridUnit * 22
27    property int _minimumHeight: PlasmaCore.Units.gridUnit * 25
28
29    PlasmaCore.ColorScope.inherit: false
30    PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.NormalColorGroup
31
32    Layout.minimumWidth: _minimumWidth
33    Layout.minimumHeight: _minimumHeight
34    Layout.preferredWidth: _minimumWidth
35    Layout.preferredHeight: _minimumHeight
36    Layout.maximumWidth: _minimumWidth
37    Layout.maximumHeight: _minimumHeight
38
39    collapseMarginsHint: true
40
41    readonly property int paddings: PlasmaCore.Units.smallSpacing
42    readonly property bool showAgenda: PlasmaCalendar.EventPluginsManager.enabledPlugins.length > 0
43    readonly property bool showClocks: plasmoid.configuration.selectedTimeZones.length > 1
44
45    property alias borderWidth: monthView.borderWidth
46    property alias monthView: monthView
47
48    property bool debug: false
49
50    property bool isExpanded: plasmoid.expanded
51
52    onIsExpandedChanged: {
53        // clear all the selections when the plasmoid is showing/hiding
54        monthView.resetToToday();
55    }
56
57    // Header containing date and pin button
58    header: PlasmaExtras.PlasmoidHeading {
59        id: headerArea
60        implicitHeight: calendarHeader.implicitHeight
61
62        // Agenda view header
63        // -----------------
64        ColumnLayout {
65            id: eventHeader
66
67            anchors.left: parent.left
68            width: visible ? parent.width / 2 - 1 : 0
69
70            visible: calendar.showAgenda || calendar.showClocks
71            RowLayout {
72                PlasmaExtras.Heading {
73                    Layout.fillWidth: true
74                    Layout.leftMargin: calendar.paddings // Match calendar title
75
76                    text: monthView.currentDate.toLocaleDateString(Qt.locale(), Locale.LongFormat)
77                }
78            }
79            RowLayout {
80                // Heading text
81                PlasmaExtras.Heading {
82                    visible: agenda.visible
83
84                    Layout.fillWidth: true
85                    Layout.leftMargin: calendar.paddings
86
87                    level: 2
88
89                    text: i18n("Events")
90                    maximumLineCount: 1
91                    elide: Text.ElideRight
92                }
93                PlasmaComponents3.ToolButton {
94                    visible: agenda.visible && ApplicationIntegration.korganizerInstalled
95                    text: i18nc("@action:button Add event", "Add…")
96                    Layout.rightMargin: calendar.paddings
97                    icon.name: "list-add"
98                    onClicked: ApplicationIntegration.launchKorganizer()
99                }
100            }
101        }
102
103        // Vertical separator line between columns
104        // =======================================
105        PlasmaCore.SvgItem {
106            id: headerSeparator
107            anchors.left: eventHeader.right
108            anchors.bottomMargin: PlasmaCore.Units.smallSpacing * 2
109            width: visible ? 1 : 0
110            height: calendarHeader.height - PlasmaCore.Units.smallSpacing * 2
111            visible: eventHeader.visible
112
113            elementId: "vertical-line"
114            svg: PlasmaCore.Svg {
115                imagePath: "widgets/line"
116            }
117        }
118
119        GridLayout {
120            id: calendarHeader
121            width: calendar.showAgenda || calendar.showClocks ? parent.width / 2 : parent.width
122            anchors.left: headerSeparator.right
123            columns: 6
124            rows: 2
125
126            PlasmaExtras.Heading {
127                Layout.row: 0
128                Layout.column: 0
129                Layout.columnSpan: 3
130                Layout.fillWidth: true
131                Layout.leftMargin: calendar.paddings + PlasmaCore.Units.smallSpacing
132                text: monthView.selectedYear === (new Date()).getFullYear() ? monthView.selectedMonth : i18nc("Format: month year", "%1 %2", monthView.selectedMonth, monthView.selectedYear.toString())
133            }
134
135            PlasmaComponents3.ToolButton {
136                Layout.row: 0
137                Layout.column: 4
138                Layout.alignment: Qt.AlignRight
139                visible: plasmoid.action("configure").enabled
140                icon.name: "configure"
141                onClicked: plasmoid.action("configure").trigger()
142                PlasmaComponents3.ToolTip {
143                    text: plasmoid.action("configure").text
144                }
145            }
146
147            // Allows the user to keep the calendar open for reference
148            PlasmaComponents3.ToolButton {
149                Layout.row: 0
150                Layout.column: 5
151                checkable: true
152                checked: plasmoid.configuration.pin
153                onToggled: plasmoid.configuration.pin = checked
154                icon.name: "window-pin"
155                PlasmaComponents3.ToolTip {
156                    text: i18n("Keep Open")
157                }
158            }
159
160            PlasmaComponents3.TabBar {
161                id: tabbar
162                currentIndex: monthView.currentIndex
163                Layout.row: 1
164                Layout.column: 0
165                Layout.columnSpan: 3
166                Layout.topMargin: PlasmaCore.Units.smallSpacing
167                Layout.fillWidth: true
168                Layout.leftMargin: PlasmaCore.Units.smallSpacing
169
170                PlasmaComponents3.TabButton {
171                    text: i18n("Days");
172                    onClicked: monthView.showMonthView();
173                    display: PlasmaComponents3.AbstractButton.TextOnly
174                }
175                PlasmaComponents3.TabButton {
176                    text: i18n("Months");
177                    onClicked: monthView.showYearView();
178                    display: PlasmaComponents3.AbstractButton.TextOnly
179                }
180                PlasmaComponents3.TabButton {
181                    text: i18n("Years");
182                    onClicked: monthView.showDecadeView();
183                    display: PlasmaComponents3.AbstractButton.TextOnly
184                }
185            }
186
187            PlasmaComponents3.ToolButton {
188                id: previousButton
189                property string tooltip
190                Layout.row: 1
191                Layout.column: 3
192
193                Layout.leftMargin: PlasmaCore.Units.smallSpacing
194                Layout.bottomMargin: PlasmaCore.Units.smallSpacing
195                icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-next" : "go-previous"
196                onClicked: monthView.previousView()
197                Accessible.name: tooltip
198                PlasmaComponents3.ToolTip {
199                    text: {
200                        switch(monthView.calendarViewDisplayed) {
201                            case PlasmaCalendar.MonthView.CalendarView.DayView:
202                                return i18n("Previous month")
203                            case PlasmaCalendar.MonthView.CalendarView.MonthView:
204                                return i18n("Previous year")
205                            case PlasmaCalendar.MonthView.CalendarView.YearView:
206                                return i18n("Previous decade")
207                            default:
208                                return "";
209                        }
210                    }
211                }
212            }
213
214            PlasmaComponents3.ToolButton {
215                Layout.bottomMargin: PlasmaCore.Units.smallSpacing
216                Layout.row: 1
217                Layout.column: 4
218                onClicked: monthView.resetToToday()
219                text: i18ndc("libplasma5", "Reset calendar to today", "Today")
220                Accessible.description: i18nd("libplasma5", "Reset calendar to today")
221            }
222
223            PlasmaComponents3.ToolButton {
224                id: nextButton
225                property string tooltip
226                Layout.bottomMargin: PlasmaCore.Units.smallSpacing
227                Layout.row: 1
228                Layout.column: 5
229
230                icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-previous" : "go-next"
231                onClicked: monthView.nextView()
232                Accessible.name: tooltip
233                PlasmaComponents3.ToolTip {
234                    text: {
235                        switch(monthView.calendarViewDisplayed) {
236                            case PlasmaCalendar.MonthView.CalendarView.DayView:
237                                return i18n("Next month")
238                            case PlasmaCalendar.MonthView.CalendarView.MonthView:
239                                return i18n("Next year")
240                            case PlasmaCalendar.MonthView.CalendarView.YearView:
241                                return i18n("Next decade")
242                            default:
243                                return "";
244                        }
245                    }
246                }
247            }
248        }
249    }
250
251    // Left column containing agenda view and time zones
252    // ==================================================
253    ColumnLayout {
254        id: leftColumn
255
256        visible: calendar.showAgenda || calendar.showClocks
257        width: parent.width / 2 - 1
258        anchors {
259            left: parent.left
260            top: parent.top
261            bottom: parent.bottom
262        }
263
264
265        // Agenda view itself
266        Item {
267            id: agenda
268            visible: calendar.showAgenda
269
270            Layout.fillWidth: true
271            Layout.fillHeight: true
272            Layout.minimumHeight: PlasmaCore.Units.gridUnit * 4
273
274            function formatDateWithoutYear(date) {
275                // Unfortunatelly Qt overrides ECMA's Date.toLocaleDateString(),
276                // which is able to return locale-specific date-and-month-only date
277                // formats, with its dumb version that only supports Qt::DateFormat
278                // enum subset. So to get a day-and-month-only date format string we
279                // must resort to this magic and hope there are no locales that use
280                // other separators...
281                var format = Qt.locale().dateFormat(Locale.ShortFormat).replace(/[./ ]*Y{2,4}[./ ]*/i, '');
282                return Qt.formatDate(date, format);
283            }
284
285            function dateEquals(date1, date2) {
286                const values1 = [
287                    date1.getFullYear(),
288                    date1.getMonth(),
289                    date1.getDate()
290                ];
291
292                const values2 = [
293                    date2.getFullYear(),
294                    date2.getMonth(),
295                    date2.getDate()
296                ];
297
298                return values1.every((value, index) => {
299                    return (value === values2[index]);
300                }, false)
301            }
302
303            Connections {
304                target: monthView
305
306                function onCurrentDateChanged() {
307                    // Apparently this is needed because this is a simple QList being
308                    // returned and if the list for the current day has 1 event and the
309                    // user clicks some other date which also has 1 event, QML sees the
310                    // sizes match and does not update the labels with the content.
311                    // Resetting the model to null first clears it and then correct data
312                    // are displayed.
313                    holidaysList.model = null;
314                    holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate);
315                }
316            }
317
318            Connections {
319                target: monthView.daysModel
320
321                function onAgendaUpdated(updatedDate) {
322                    if (agenda.dateEquals(updatedDate, monthView.currentDate)) {
323                        holidaysList.model = null;
324                        holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate);
325                    }
326                }
327            }
328
329            Connections {
330                target: plasmoid.configuration
331
332                onEnabledCalendarPluginsChanged: {
333                    PlasmaCalendar.EventPluginsManager.enabledPlugins = plasmoid.configuration.enabledCalendarPlugins;
334                }
335            }
336
337            Binding {
338                target: plasmoid
339                property: "hideOnWindowDeactivate"
340                value: !plasmoid.configuration.pin
341                restoreMode: Binding.RestoreBinding
342            }
343
344            TextMetrics {
345                id: dateLabelMetrics
346
347                // Date/time are arbitrary values with all parts being two-digit
348                readonly property string timeString: Qt.formatTime(new Date(2000, 12, 12, 12, 12, 12, 12))
349                readonly property string dateString: agenda.formatDateWithoutYear(new Date(2000, 12, 12, 12, 12, 12))
350
351                font: PlasmaCore.Theme.defaultFont
352                text: timeString.length > dateString.length ? timeString : dateString
353            }
354
355            PlasmaComponents3.ScrollView {
356                id: holidaysView
357                anchors.fill: parent
358
359                ListView {
360                    id: holidaysList
361                    highlight: Item {}
362
363                    delegate: PlasmaComponents3.ItemDelegate {
364                        id: eventItem
365                        width: holidaysList.width
366                        padding: calendar.paddings
367                        leftPadding: calendar.paddings + PlasmaCore.Units.smallSpacing * 2
368                        text: eventTitle.text
369                        hoverEnabled: true
370                        property bool hasTime: {
371                            // Explicitly all-day event
372                            if (modelData.isAllDay) {
373                                return false;
374                            }
375                            // Multi-day event which does not start or end today (so
376                            // is all-day from today's point of view)
377                            if (modelData.startDateTime - monthView.currentDate < 0 &&
378                                modelData.endDateTime - monthView.currentDate > 86400000) { // 24hrs in ms
379                                return false;
380                            }
381
382                            // Non-explicit all-day event
383                            const startIsMidnight = modelData.startDateTime.getHours() === 0
384                                            && modelData.startDateTime.getMinutes() === 0;
385
386                            const endIsMidnight = modelData.endDateTime.getHours() === 0
387                                            && modelData.endDateTime.getMinutes() === 0;
388
389                            const sameDay = modelData.startDateTime.getDate() === modelData.endDateTime.getDate()
390                                    && modelData.startDateTime.getDay() === modelData.endDateTime.getDay()
391
392                            return !(startIsMidnight && endIsMidnight && sameDay);
393                        }
394
395                        PlasmaComponents3.ToolTip {
396                            text: modelData.description
397                            visible: text !== "" && eventItem.hovered
398                        }
399
400                        contentItem: GridLayout {
401                            id: eventGrid
402                            columns: 3
403                            rows: 2
404                            rowSpacing: 0
405                            columnSpacing: 2 * PlasmaCore.Units.smallSpacing
406
407                            Rectangle {
408                                id: eventColor
409
410                                Layout.row: 0
411                                Layout.column: 0
412                                Layout.rowSpan: 2
413                                Layout.fillHeight: true
414
415                                color: modelData.eventColor
416                                width: 5 * PlasmaCore.Units.devicePixelRatio
417                                visible: modelData.eventColor !== ""
418                            }
419
420                            PlasmaComponents3.Label {
421                                id: startTimeLabel
422
423                                readonly property bool startsToday: modelData.startDateTime - monthView.currentDate >= 0
424                                readonly property bool startedYesterdayLessThan12HoursAgo: modelData.startDateTime - monthView.currentDate >= -43200000 //12hrs in ms
425
426                                Layout.row: 0
427                                Layout.column: 1
428                                Layout.minimumWidth: dateLabelMetrics.width
429
430                                text: startsToday || startedYesterdayLessThan12HoursAgo
431                                        ? Qt.formatTime(modelData.startDateTime)
432                                        : agenda.formatDateWithoutYear(modelData.startDateTime)
433                                horizontalAlignment: Qt.AlignRight
434                                visible: eventItem.hasTime
435                            }
436
437                            PlasmaComponents3.Label {
438                                id: endTimeLabel
439
440                                readonly property bool endsToday: modelData.endDateTime - monthView.currentDate <= 86400000 // 24hrs in ms
441                                readonly property bool endsTomorrowInLessThan12Hours: modelData.endDateTime - monthView.currentDate <= 86400000 + 43200000 // 36hrs in ms
442
443                                Layout.row: 1
444                                Layout.column: 1
445                                Layout.minimumWidth: dateLabelMetrics.width
446
447                                text: endsToday || endsTomorrowInLessThan12Hours
448                                        ? Qt.formatTime(modelData.endDateTime)
449                                        : agenda.formatDateWithoutYear(modelData.endDateTime)
450                                horizontalAlignment: Qt.AlignRight
451                                opacity: 0.7
452
453                                visible: eventItem.hasTime
454                            }
455
456                            PlasmaComponents3.Label {
457                                id: eventTitle
458
459                                Layout.row: 0
460                                Layout.column: 2
461                                Layout.fillWidth: true
462
463                                elide: Text.ElideRight
464                                text: modelData.title
465                                verticalAlignment: Text.AlignVCenter
466                                maximumLineCount: 2
467                            }
468                        }
469                    }
470                }
471            }
472
473            PlasmaExtras.Heading {
474                anchors.fill: holidaysView
475                horizontalAlignment: Text.AlignHCenter
476                verticalAlignment: Text.AlignVCenter
477                anchors.leftMargin: PlasmaCore.Units.largeSpacing
478                anchors.rightMargin: PlasmaCore.Units.largeSpacing
479                text: monthView.isToday(monthView.currentDate) ? i18n("No events for today")
480                                                            : i18n("No events for this day");
481                level: 3
482                enabled: false
483                visible: holidaysList.count == 0
484            }
485        }
486
487        // Horizontal separator line between events and time zones
488        PlasmaCore.SvgItem {
489            visible: worldClocks.visible && agenda.visible
490
491            Layout.fillWidth: true
492            Layout.preferredHeight: naturalSize.height
493
494            elementId: "horizontal-line"
495            svg: PlasmaCore.Svg {
496                imagePath: "widgets/line"
497            }
498        }
499
500        // Clocks stuff
501        // ------------
502        // Header text + button to change time & timezone
503        PlasmaExtras.PlasmoidHeading {
504            visible: worldClocks.visible
505            leftInset: 0
506            rightInset: 0
507            rightPadding: PlasmaCore.Units.smallSpacing
508            contentItem: RowLayout {
509                PlasmaExtras.Heading {
510                    Layout.leftMargin: calendar.paddings + PlasmaCore.Units.smallSpacing * 2
511                    Layout.fillWidth: true
512
513                    level: 2
514
515                    text: i18n("Time Zones")
516                    maximumLineCount: 1
517                    elide: Text.ElideRight
518                }
519
520                PlasmaComponents3.ToolButton {
521                    visible: KCMShell.authorize("clock.desktop").length > 0
522                    text: i18n("Switch…")
523                    icon.name: "preferences-system-time"
524                    onClicked: KCMShell.openSystemSettings("clock")
525
526                    PlasmaComponents3.ToolTip {
527                        text: i18n("Switch to another timezone")
528                    }
529                }
530            }
531        }
532
533        // Clocks view itself
534        PlasmaComponents3.ScrollView {
535            id: worldClocks
536            visible: calendar.showClocks
537
538            Layout.fillWidth: true
539            Layout.fillHeight: !agenda.visible
540            Layout.minimumHeight: visible ? PlasmaCore.Units.gridUnit * 7 : 0
541            Layout.maximumHeight: agenda.visible ? PlasmaCore.Units.gridUnit * 10 : -1
542
543            ListView {
544                id: clocksList
545                anchors.left: parent.left
546                anchors.right: parent.right
547                anchors.rightMargin: PlasmaCore.Units.smallSpacing * 2
548
549                highlight: Item {}
550
551                model: {
552                    let timezones = [];
553                    for (let i = 0; i < plasmoid.configuration.selectedTimeZones.length; i++) {
554                        timezones.push(plasmoid.configuration.selectedTimeZones[i]);
555                    }
556
557                    return timezones;
558                }
559
560                delegate: PlasmaComponents3.ItemDelegate {
561                    id: listItem
562                    readonly property bool isCurrentTimeZone: modelData === plasmoid.configuration.lastSelectedTimezone
563                    width: clocksList.width
564                    padding: calendar.paddings
565                    leftPadding: calendar.paddings + PlasmaCore.Units.smallSpacing * 2
566
567                    contentItem: RowLayout {
568                        PlasmaComponents3.Label {
569                            text: root.nameForZone(modelData)
570                            font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal
571                            maximumLineCount: 1
572                            elide: Text.ElideRight
573                        }
574
575                        PlasmaComponents3.Label {
576                            Layout.fillWidth: true
577                            horizontalAlignment: Qt.AlignRight
578                            text: root.timeForZone(modelData)
579                            font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal
580                            elide: Text.ElideRight
581                            maximumLineCount: 1
582                        }
583                    }
584                }
585            }
586        }
587    }
588
589    // Vertical separator line between columns
590    // =======================================
591    PlasmaCore.SvgItem {
592        id: mainSeparator
593        visible: leftColumn.visible
594        anchors {
595            right: monthViewWrapper.left
596            top: parent.top
597            bottom: parent.bottom
598        }
599        width: 1
600
601        elementId: "vertical-line"
602        svg: PlasmaCore.Svg {
603            imagePath: "widgets/line"
604        }
605    }
606
607    // Right column containing calendar
608    // ===============================
609    FocusScope {
610        id: monthViewWrapper
611        width: calendar.showAgenda || calendar.showClocks ? parent.width / 2 : parent.width
612        anchors.right: parent.right
613        anchors.top: parent.top
614        anchors.bottom: parent.bottom
615        PlasmaCalendar.MonthView {
616            id: monthView
617            anchors.margins: PlasmaCore.Units.smallSpacing
618            borderOpacity: 0.25
619            today: root.tzDate
620            firstDayOfWeek: plasmoid.configuration.firstDayOfWeek > -1
621                ? plasmoid.configuration.firstDayOfWeek
622                : Qt.locale().firstDayOfWeek
623            showWeekNumbers: plasmoid.configuration.showWeekNumbers
624            showCustomHeader: true
625        }
626    }
627}
628