Warning, /plasma/plasma-workspace/applets/digital-clock/package/contents/ui/CalendarView.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
0003     SPDX-FileCopyrightText: 2015 Martin Klapetek <mklapetek@kde.org>
0004     SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
0005     SPDX-FileCopyrightText: 2023 ivan tkachenko <me@ratijas.tk>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 import QtQuick 2.4
0010 import QtQuick.Layouts 1.1
0011 import QtQml 2.15
0012 
0013 import org.kde.kquickcontrolsaddons 2.0 // For kcmshell
0014 import org.kde.plasma.plasmoid 2.0
0015 import org.kde.ksvg 1.0 as KSvg
0016 import org.kde.plasma.workspace.calendar 2.0 as PlasmaCalendar
0017 import org.kde.plasma.components 3.0 as PlasmaComponents3
0018 import org.kde.plasma.extras 2.0 as PlasmaExtras
0019 import org.kde.plasma.private.digitalclock 1.0
0020 import org.kde.config // KAuthorized
0021 import org.kde.kcmutils // KCMUtils
0022 import org.kde.kirigami 2.20 as Kirigami
0023 
0024 // Top-level layout containing:
0025 // - Leading column with world clock and agenda view
0026 // - Trailing column with current date header and calendar
0027 //
0028 // Trailing column fills exactly half of the popup width, then there's 1
0029 // logical pixel wide separator, and the rest is left for the Leading.
0030 // Representation's header is intentionally zero-sized, because Calendar view
0031 // brings its own header, and there's currently no other way to stack them.
0032 PlasmaExtras.Representation {
0033     id: calendar
0034 
0035     readonly property var appletInterface: root
0036 
0037     Kirigami.Theme.colorSet: Kirigami.Theme.Window
0038     Kirigami.Theme.inherit: false
0039 
0040     Layout.minimumWidth: (calendar.showAgenda || calendar.showClocks) ? Kirigami.Units.gridUnit * 45 : Kirigami.Units.gridUnit * 22
0041     Layout.maximumWidth: Kirigami.Units.gridUnit * 80
0042 
0043     Layout.minimumHeight: Kirigami.Units.gridUnit * 25
0044     Layout.maximumHeight: Kirigami.Units.gridUnit * 40
0045 
0046     collapseMarginsHint: true
0047 
0048     readonly property int paddings: Kirigami.Units.largeSpacing
0049     readonly property bool showAgenda: eventPluginsManager.enabledPlugins.length > 0
0050     readonly property bool showClocks: Plasmoid.configuration.selectedTimeZones.length > 1
0051 
0052     property alias borderWidth: monthView.borderWidth
0053     property alias monthView: monthView
0054 
0055     property bool debug: false
0056 
0057     Keys.onDownPressed: monthView.Keys.downPressed(event);
0058 
0059     Connections {
0060         target: root
0061 
0062         function onExpandedChanged() {
0063             // clear all the selections when the plasmoid is showing/hiding
0064             monthView.resetToToday();
0065         }
0066     }
0067 
0068     PlasmaCalendar.EventPluginsManager {
0069         id: eventPluginsManager
0070         enabledPlugins: Plasmoid.configuration.enabledCalendarPlugins
0071     }
0072 
0073     // Having this in place helps preserving top margins for Pin and Configure
0074     // buttons somehow. Actual headers are spread across leading and trailing
0075     // columns.
0076     header: Item {}
0077 
0078     // Leading column containing agenda view and time zones
0079     // ==================================================
0080     ColumnLayout {
0081         id: leadingColumn
0082 
0083         visible: calendar.showAgenda || calendar.showClocks
0084 
0085         anchors {
0086             top: parent.top
0087             left: parent.left
0088             right: mainSeparator.left
0089             bottom: parent.bottom
0090         }
0091 
0092         spacing: 0
0093 
0094         PlasmaExtras.PlasmoidHeading {
0095             Layout.fillWidth: true
0096             Layout.preferredHeight: monthView.viewHeader.height
0097             leftInset: 0
0098             rightInset: 0
0099 
0100             // Agenda view header
0101             // -----------------
0102             contentItem: ColumnLayout {
0103                 spacing: 0
0104 
0105                 Kirigami.Heading {
0106                     Layout.alignment: Qt.AlignTop
0107                     // Match calendar title
0108                     Layout.leftMargin: calendar.paddings
0109                     Layout.rightMargin: calendar.paddings
0110                     Layout.fillWidth: true
0111 
0112                     text: monthView.currentDate.toLocaleDateString(Qt.locale(), Locale.LongFormat)
0113                     textFormat: Text.PlainText
0114                 }
0115 
0116                 PlasmaComponents3.Label {
0117                     visible: monthView.currentDateAuxilliaryText.length > 0
0118 
0119                     Layout.leftMargin: calendar.paddings
0120                     Layout.rightMargin: calendar.paddings
0121                     Layout.fillWidth: true
0122 
0123                     font.pixelSize: Kirigami.Theme.smallFont.pixelSize
0124                     text: monthView.currentDateAuxilliaryText
0125                     textFormat: Text.PlainText
0126                 }
0127 
0128                 RowLayout {
0129                     spacing: Kirigami.Units.smallSpacing
0130 
0131                     Layout.alignment: Qt.AlignBottom
0132                     Layout.bottomMargin: Kirigami.Units.mediumSpacing
0133 
0134                     // Heading text
0135                     Kirigami.Heading {
0136                         visible: agenda.visible
0137 
0138                         Layout.fillWidth: true
0139                         Layout.leftMargin: calendar.paddings
0140                         Layout.rightMargin: calendar.paddings
0141 
0142                         level: 2
0143 
0144                         text: i18n("Events")
0145                         textFormat: Text.PlainText
0146                         maximumLineCount: 1
0147                         elide: Text.ElideRight
0148                     }
0149                     PlasmaComponents3.ToolButton {
0150                         id: addEventButton
0151 
0152                         visible: agenda.visible && ApplicationIntegration.calendarInstalled
0153                         text: i18nc("@action:button Add event", "Add…")
0154                         Layout.rightMargin: Kirigami.Units.smallSpacing
0155                         icon.name: "list-add"
0156 
0157                         Accessible.description: i18nc("@info:tooltip", "Add a new event")
0158                         KeyNavigation.down: KeyNavigation.tab
0159                         KeyNavigation.right: monthView.viewHeader.tabBar
0160 
0161                         onClicked: ApplicationIntegration.launchCalendar()
0162                         KeyNavigation.tab: calendar.showAgenda && holidaysList.count ? holidaysList : holidaysList.KeyNavigation.down
0163                     }
0164                 }
0165             }
0166         }
0167 
0168         // Agenda view itself
0169         Item {
0170             id: agenda
0171             visible: calendar.showAgenda
0172 
0173             Layout.fillWidth: true
0174             Layout.fillHeight: true
0175             Layout.minimumHeight: Kirigami.Units.gridUnit * 4
0176 
0177             function formatDateWithoutYear(date) {
0178                 // Unfortunatelly Qt overrides ECMA's Date.toLocaleDateString(),
0179                 // which is able to return locale-specific date-and-month-only date
0180                 // formats, with its dumb version that only supports Qt::DateFormat
0181                 // enum subset. So to get a day-and-month-only date format string we
0182                 // must resort to this magic and hope there are no locales that use
0183                 // other separators...
0184                 var format = Qt.locale().dateFormat(Locale.ShortFormat).replace(/[./ ]*Y{2,4}[./ ]*/i, '');
0185                 return Qt.formatDate(date, format);
0186             }
0187 
0188             function dateEquals(date1, date2) {
0189                 const values1 = [
0190                     date1.getFullYear(),
0191                     date1.getMonth(),
0192                     date1.getDate()
0193                 ];
0194 
0195                 const values2 = [
0196                     date2.getFullYear(),
0197                     date2.getMonth(),
0198                     date2.getDate()
0199                 ];
0200 
0201                 return values1.every((value, index) => {
0202                     return (value === values2[index]);
0203                 }, false)
0204             }
0205 
0206             Connections {
0207                 target: monthView
0208 
0209                 function onCurrentDateChanged() {
0210                     // Apparently this is needed because this is a simple QList being
0211                     // returned and if the list for the current day has 1 event and the
0212                     // user clicks some other date which also has 1 event, QML sees the
0213                     // sizes match and does not update the labels with the content.
0214                     // Resetting the model to null first clears it and then correct data
0215                     // are displayed.
0216                     holidaysList.model = null;
0217                     holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate);
0218                 }
0219             }
0220 
0221             Connections {
0222                 target: monthView.daysModel
0223 
0224                 function onAgendaUpdated(updatedDate) {
0225                     if (agenda.dateEquals(updatedDate, monthView.currentDate)) {
0226                         holidaysList.model = null;
0227                         holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate);
0228                     }
0229                 }
0230             }
0231 
0232             TextMetrics {
0233                 id: dateLabelMetrics
0234 
0235                 // Date/time are arbitrary values with all parts being two-digit
0236                 readonly property string timeString: Qt.formatTime(new Date(2000, 12, 12, 12, 12, 12, 12))
0237                 readonly property string dateString: agenda.formatDateWithoutYear(new Date(2000, 12, 12, 12, 12, 12))
0238 
0239                 font: Kirigami.Theme.defaultFont
0240                 text: timeString.length > dateString.length ? timeString : dateString
0241             }
0242 
0243             PlasmaComponents3.ScrollView {
0244                 id: holidaysView
0245                 anchors.fill: parent
0246 
0247                 ListView {
0248                     id: holidaysList
0249 
0250                     focus: false
0251                     activeFocusOnTab: true
0252                     highlight: null
0253                     currentIndex: -1
0254 
0255                     KeyNavigation.down: switchTimeZoneButton.visible ? switchTimeZoneButton : clocksList
0256                     Keys.onRightPressed: switchTimeZoneButton.Keys.rightPressed(event);
0257 
0258                     onCurrentIndexChanged: if (!activeFocus) {
0259                         currentIndex = -1;
0260                     }
0261 
0262                     onActiveFocusChanged: if (activeFocus) {
0263                         currentIndex = 0;
0264                     } else {
0265                         currentIndex = -1;
0266                     }
0267 
0268                     delegate: PlasmaComponents3.ItemDelegate {
0269                         id: eventItem
0270                         width: holidaysList.width
0271 
0272                         leftPadding: calendar.paddings
0273 
0274                         text: eventTitle.text
0275                         hoverEnabled: true
0276                         highlighted: ListView.isCurrentItem
0277                         Accessible.description: modelData.description
0278                         property bool hasTime: {
0279                             // Explicitly all-day event
0280                             if (modelData.isAllDay) {
0281                                 return false;
0282                             }
0283                             // Multi-day event which does not start or end today (so
0284                             // is all-day from today's point of view)
0285                             if (modelData.startDateTime - monthView.currentDate < 0 &&
0286                                 modelData.endDateTime - monthView.currentDate > 86400000) { // 24hrs in ms
0287                                 return false;
0288                             }
0289 
0290                             // Non-explicit all-day event
0291                             const startIsMidnight = modelData.startDateTime.getHours() === 0
0292                                             && modelData.startDateTime.getMinutes() === 0;
0293 
0294                             const endIsMidnight = modelData.endDateTime.getHours() === 0
0295                                             && modelData.endDateTime.getMinutes() === 0;
0296 
0297                             const sameDay = modelData.startDateTime.getDate() === modelData.endDateTime.getDate()
0298                                     && modelData.startDateTime.getDay() === modelData.endDateTime.getDay()
0299 
0300                             return !(startIsMidnight && endIsMidnight && sameDay);
0301                         }
0302 
0303                         PlasmaComponents3.ToolTip {
0304                             text: modelData.description
0305                             visible: text !== "" && eventItem.hovered
0306                         }
0307 
0308                         contentItem: GridLayout {
0309                             id: eventGrid
0310                             columns: 3
0311                             rows: 2
0312                             rowSpacing: 0
0313                             columnSpacing: Kirigami.Units.largeSpacing
0314 
0315                             Rectangle {
0316                                 id: eventColor
0317 
0318                                 Layout.row: 0
0319                                 Layout.column: 0
0320                                 Layout.rowSpan: 2
0321                                 Layout.fillHeight: true
0322 
0323                                 color: modelData.eventColor
0324                                 width: 5
0325                                 visible: modelData.eventColor !== ""
0326                             }
0327 
0328                             PlasmaComponents3.Label {
0329                                 id: startTimeLabel
0330 
0331                                 readonly property bool startsToday: modelData.startDateTime - monthView.currentDate >= 0
0332                                 readonly property bool startedYesterdayLessThan12HoursAgo: modelData.startDateTime - monthView.currentDate >= -43200000 //12hrs in ms
0333 
0334                                 Layout.row: 0
0335                                 Layout.column: 1
0336                                 Layout.minimumWidth: dateLabelMetrics.width
0337 
0338                                 text: startsToday || startedYesterdayLessThan12HoursAgo
0339                                         ? Qt.formatTime(modelData.startDateTime)
0340                                         : agenda.formatDateWithoutYear(modelData.startDateTime)
0341                                 textFormat: Text.PlainText
0342                                 horizontalAlignment: Qt.AlignRight
0343                                 visible: eventItem.hasTime
0344                             }
0345 
0346                             PlasmaComponents3.Label {
0347                                 id: endTimeLabel
0348 
0349                                 readonly property bool endsToday: modelData.endDateTime - monthView.currentDate <= 86400000 // 24hrs in ms
0350                                 readonly property bool endsTomorrowInLessThan12Hours: modelData.endDateTime - monthView.currentDate <= 86400000 + 43200000 // 36hrs in ms
0351 
0352                                 Layout.row: 1
0353                                 Layout.column: 1
0354                                 Layout.minimumWidth: dateLabelMetrics.width
0355 
0356                                 text: endsToday || endsTomorrowInLessThan12Hours
0357                                         ? Qt.formatTime(modelData.endDateTime)
0358                                         : agenda.formatDateWithoutYear(modelData.endDateTime)
0359                                 textFormat: Text.PlainText
0360                                 horizontalAlignment: Qt.AlignRight
0361                                 opacity: 0.7
0362 
0363                                 visible: eventItem.hasTime
0364                             }
0365 
0366                             PlasmaComponents3.Label {
0367                                 id: eventTitle
0368 
0369                                 Layout.row: 0
0370                                 Layout.column: 2
0371                                 Layout.fillWidth: true
0372 
0373                                 elide: Text.ElideRight
0374                                 text: modelData.title
0375                                 textFormat: Text.PlainText
0376                                 verticalAlignment: Text.AlignVCenter
0377                                 maximumLineCount: 2
0378                                 wrapMode: Text.Wrap
0379                             }
0380                         }
0381                     }
0382                 }
0383             }
0384 
0385             PlasmaExtras.PlaceholderMessage {
0386                 anchors.centerIn: holidaysView
0387                 width: holidaysView.width - (Kirigami.Units.gridUnit * 8)
0388 
0389                 visible: holidaysList.count == 0
0390 
0391                 iconName: "checkmark"
0392                 text: monthView.isToday(monthView.currentDate) ? i18n("No events for today")
0393                                                                : i18n("No events for this day");
0394             }
0395         }
0396 
0397         // Horizontal separator line between events and time zones
0398         KSvg.SvgItem {
0399             visible: worldClocks.visible && agenda.visible
0400 
0401             Layout.fillWidth: true
0402             Layout.preferredHeight: naturalSize.height
0403 
0404             imagePath: "widgets/line"
0405             elementId: "horizontal-line"
0406         }
0407 
0408         // Clocks stuff
0409         // ------------
0410         // Header text + button to change time & timezone
0411         PlasmaExtras.PlasmoidHeading {
0412             visible: worldClocks.visible
0413 
0414             enabledBorders: Qt.TopEdge | Qt.BottomEdge
0415             // Normally gets some positive/negative values from base component.
0416             topInset: 0
0417             topPadding: Kirigami.Units.smallSpacing
0418 
0419             leftInset: 0
0420             rightInset: 0
0421             leftPadding: mirrored ? Kirigami.Units.smallSpacing : calendar.paddings
0422             rightPadding: mirrored ? calendar.paddings : Kirigami.Units.smallSpacing
0423 
0424             contentItem: RowLayout {
0425                 spacing: Kirigami.Units.smallSpacing
0426 
0427                 Kirigami.Heading {
0428                     Layout.fillWidth: true
0429 
0430                     level: 2
0431 
0432                     text: i18n("Time Zones")
0433                     textFormat: Text.PlainText
0434                     maximumLineCount: 1
0435                     elide: Text.ElideRight
0436                     Accessible.ignored: true
0437                 }
0438 
0439                 PlasmaComponents3.ToolButton {
0440                     id: switchTimeZoneButton
0441 
0442                     visible: KAuthorized.authorizeControlModule("kcm_clock.desktop")
0443                     text: i18n("Switch…")
0444                     Accessible.name: i18n("Switch to another timezone")
0445                     icon.name: "preferences-system-time"
0446 
0447                     Accessible.description: i18n("Switch to another timezone")
0448                     KeyNavigation.down: clocksList
0449                     Keys.onRightPressed: monthView.Keys.downPressed(event)
0450 
0451                     onClicked: KCMLauncher.openSystemSettings("kcm_clock")
0452 
0453                     PlasmaComponents3.ToolTip {
0454                         text: parent.Accessible.description
0455                     }
0456                 }
0457             }
0458         }
0459 
0460         // Clocks view itself
0461         PlasmaComponents3.ScrollView {
0462             id: worldClocks
0463             visible: calendar.showClocks
0464 
0465             Layout.fillWidth: true
0466             Layout.fillHeight: !agenda.visible
0467             Layout.minimumHeight: visible ? Kirigami.Units.gridUnit * 7 : 0
0468             Layout.maximumHeight: agenda.visible ? Kirigami.Units.gridUnit * 10 : -1
0469 
0470             ListView {
0471                 id: clocksList
0472                 activeFocusOnTab: true
0473 
0474                 highlight: null
0475                 currentIndex: -1
0476                 onActiveFocusChanged: if (activeFocus) {
0477                     currentIndex = 0;
0478                 } else {
0479                     currentIndex = -1;
0480                 }
0481 
0482                 Keys.onRightPressed: switchTimeZoneButton.Keys.rightPressed(event);
0483 
0484                 // Can't use KeyNavigation.tab since the focus won't go to config button, instead it will be redirected to somewhere else because of
0485                 // some existing code. Since now the header was in this file and this was not a problem. Now the header is also implicitly
0486                 // inside the monthViewWrapper.
0487                 Keys.onTabPressed: {
0488                     monthView.viewHeader.configureButton.forceActiveFocus(Qt.BacktabFocusReason);
0489                 }
0490 
0491                 model: {
0492                     let timezones = [];
0493                     for (let i = 0; i < Plasmoid.configuration.selectedTimeZones.length; i++) {
0494                         let thisTzData = Plasmoid.configuration.selectedTimeZones[i];
0495 
0496                         /* Don't add this item if it's the same as the local time zone, which
0497                          * would indicate that the user has deliberately added a dedicated entry
0498                          * for the city of their normal time zone. This is not an error condition
0499                          * because the user may have done this on purpose so that their normal
0500                          * local time zone shows up automatically while they're traveling and
0501                          * they've switched the current local time zone to something else. But
0502                          * with this use case, when they're back in their normal local time zone,
0503                          * the clocks list would show two entries for the same city. To avoid
0504                          * this, let's suppress the duplicate.
0505                          */
0506                         if (!(thisTzData !== "Local" && root.nameForZone(thisTzData) === root.nameForZone("Local"))) {
0507                             timezones.push(Plasmoid.configuration.selectedTimeZones[i]);
0508                         }
0509                     }
0510                     return timezones;
0511                 }
0512 
0513                 delegate: PlasmaComponents3.ItemDelegate {
0514                     id: listItem
0515                     readonly property bool isCurrentTimeZone: modelData === Plasmoid.configuration.lastSelectedTimezone
0516                     width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin
0517 
0518                     leftPadding: calendar.paddings
0519                     rightPadding: calendar.paddings
0520 
0521                     highlighted: ListView.isCurrentItem
0522                     Accessible.name: root.nameForZone(modelData)
0523                     Accessible.description: root.timeForZone(modelData, Plasmoid.configuration.showSeconds === 2)
0524                     hoverEnabled: false
0525 
0526                     contentItem: RowLayout {
0527                         PlasmaComponents3.Label {
0528                             Layout.fillWidth: true
0529                             text: root.nameForZone(modelData)
0530                             textFormat: Text.PlainText
0531                             font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal
0532                             maximumLineCount: 1
0533                             elide: Text.ElideRight
0534                         }
0535 
0536                         PlasmaComponents3.Label {
0537                             horizontalAlignment: Qt.AlignRight
0538                             text: root.timeForZone(modelData, Plasmoid.configuration.showSeconds === 2)
0539                             textFormat: Text.PlainText
0540                             font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal
0541                             elide: Text.ElideRight
0542                             maximumLineCount: 1
0543                         }
0544                     }
0545                 }
0546             }
0547         }
0548     }
0549 
0550     // Vertical separator line between columns
0551     // =======================================
0552     KSvg.SvgItem {
0553         id: mainSeparator
0554 
0555         anchors {
0556             top: parent.top
0557             right: monthViewWrapper.left
0558             bottom: parent.bottom
0559             // Stretch all the way to the top of a dialog. This magic comes
0560             // from PlasmaCore.Dialog::margins and CompactApplet containment.
0561             topMargin: calendar.parent ? -calendar.parent.y : 0
0562         }
0563 
0564         width: naturalSize.width
0565         visible: calendar.showAgenda || calendar.showClocks
0566 
0567         imagePath: "widgets/line"
0568         elementId: "vertical-line"
0569     }
0570 
0571     // Trailing column containing calendar
0572     // ===============================
0573     FocusScope {
0574         id: monthViewWrapper
0575 
0576         anchors {
0577             top: parent.top
0578             right: parent.right
0579             bottom: parent.bottom
0580         }
0581 
0582         // Not anchoring to horizontalCenter to avoid sub-pixel misalignments
0583         width: (calendar.showAgenda || calendar.showClocks) ? Math.round(parent.width / 2) : parent.width
0584 
0585         onActiveFocusChanged: if (activeFocus) {
0586             monthViewWrapper.nextItemInFocusChain().forceActiveFocus();
0587         }
0588 
0589         PlasmaCalendar.MonthView {
0590             id: monthView
0591 
0592             anchors {
0593                 fill: parent
0594                 leftMargin: Kirigami.Units.smallSpacing
0595                 rightMargin: Kirigami.Units.smallSpacing
0596                 bottomMargin: Kirigami.Units.smallSpacing
0597             }
0598 
0599             borderOpacity: 0.25
0600 
0601             eventPluginsManager: eventPluginsManager
0602             today: root.tzDate
0603             firstDayOfWeek: Plasmoid.configuration.firstDayOfWeek > -1
0604                 ? Plasmoid.configuration.firstDayOfWeek
0605                 : Qt.locale().firstDayOfWeek
0606             showWeekNumbers: Plasmoid.configuration.showWeekNumbers
0607 
0608             showDigitalClockHeader: true
0609             digitalClock: Plasmoid
0610             eventButton: addEventButton
0611 
0612             KeyNavigation.left: KeyNavigation.tab
0613             KeyNavigation.tab: addEventButton.visible ? addEventButton : addEventButton.KeyNavigation.down
0614             Keys.onUpPressed: viewHeader.tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason);
0615         }
0616     }
0617 }