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