diff --git a/contents/config/config.qml b/contents/config/config.qml index 1af2286..50fa20a 100644 --- a/contents/config/config.qml +++ b/contents/config/config.qml @@ -5,12 +5,13 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ -import QtQuick 2.0 -import QtQml 2.2 +pragma ComponentBehavior: Bound -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.configuration 2.0 -import org.kde.plasma.workspace.calendar 2.0 as PlasmaCalendar +import QtQuick + +import org.kde.plasma.plasmoid +import org.kde.plasma.configuration +import org.kde.plasma.workspace.calendar as PlasmaCalendar ConfigModel { id: configModel @@ -29,23 +30,26 @@ ConfigModel { name: i18n("Time Zones") icon: "preferences-system-time" source: "configTimeZones.qml" - includeMargins: false } - property QtObject eventPluginsManager: PlasmaCalendar.EventPluginsManager { + readonly property PlasmaCalendar.EventPluginsManager eventPluginsManager: PlasmaCalendar.EventPluginsManager { Component.onCompleted: { populateEnabledPluginsList(Plasmoid.configuration.enabledCalendarPlugins); } } - property Instantiator __eventPlugins: Instantiator { - model: eventPluginsManager.model + readonly property Instantiator __eventPlugins: Instantiator { + model: configModel.eventPluginsManager.model delegate: ConfigCategory { - name: model.display - icon: model.decoration - source: model.configUi - includeMargins: false - visible: Plasmoid.configuration.enabledCalendarPlugins.indexOf(model.pluginId) > -1 + required property string display + required property string decoration + required property string configUi + required property string pluginId + + name: display + icon: decoration + source: configUi + visible: Plasmoid.configuration.enabledCalendarPlugins.indexOf(pluginId) > -1 } onObjectAdded: (index, object) => configModel.appendCategory(object) diff --git a/contents/config/main.xml b/contents/config/main.xml index 767a552..88ffea0 100644 --- a/contents/config/main.xml +++ b/contents/config/main.xml @@ -7,7 +7,7 @@ - + false @@ -71,15 +71,15 @@ Local - + Local - + false - + diff --git a/contents/ui/CalendarView.qml b/contents/ui/CalendarView.qml index 096f44c..1141fba 100644 --- a/contents/ui/CalendarView.qml +++ b/contents/ui/CalendarView.qml @@ -6,20 +6,21 @@ SPDX-License-Identifier: GPL-2.0-or-later */ -import QtQuick 2.4 -import QtQuick.Layouts 1.1 -import QtQml 2.15 - -import org.kde.kquickcontrolsaddons 2.0 // For kcmshell -import org.kde.plasma.plasmoid 2.0 -import org.kde.ksvg 1.0 as KSvg -import org.kde.plasma.workspace.calendar 2.0 as PlasmaCalendar -import org.kde.plasma.components 3.0 as PlasmaComponents3 -import org.kde.plasma.extras 2.0 as PlasmaExtras -import org.kde.plasma.private.digitalclock 1.0 -import org.kde.config // KAuthorized -import org.kde.kcmutils // KCMUtils -import org.kde.kirigami 2.20 as Kirigami + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts + +import org.kde.plasma.plasmoid +import org.kde.ksvg as KSvg +import org.kde.plasma.workspace.calendar as PlasmaCalendar +import org.kde.plasma.components as PlasmaComponents +import org.kde.plasma.extras as PlasmaExtras +import org.kde.plasma.private.digitalclock +import org.kde.config as KConfig +import org.kde.kcmutils as KCMUtils +import org.kde.kirigami as Kirigami // Top-level layout containing: // - Leading column with world clock and agenda view @@ -47,14 +48,16 @@ PlasmaExtras.Representation { readonly property int paddings: Kirigami.Units.largeSpacing readonly property bool showAgenda: eventPluginsManager.enabledPlugins.length > 0 - readonly property bool showClocks: Plasmoid.configuration.selectedTimeZones.length > 1 + readonly property bool showClocks: clocksList.count > 1 - property alias borderWidth: monthView.borderWidth - property alias monthView: monthView + readonly property alias monthView: monthView + // This helps synchronize the header of the agenda and the monthView. + // We cannot use Kirigami.SizeGroup here because monthView's header is not in a layout. + readonly property double headerHeight: Math.max(agendaHeader.implicitHeight, monthView.viewHeader.implicitHeight) - property bool debug: false - - Keys.onDownPressed: monthView.Keys.downPressed(event); + Keys.onDownPressed: event => { + monthView.Keys.downPressed(event); + } Connections { target: root @@ -92,8 +95,9 @@ PlasmaExtras.Representation { spacing: 0 PlasmaExtras.PlasmoidHeading { + id: agendaHeader + Layout.preferredHeight: calendar.headerHeight Layout.fillWidth: true - Layout.preferredHeight: monthView.viewHeader.height leftInset: 0 rightInset: 0 @@ -113,7 +117,7 @@ PlasmaExtras.Representation { textFormat: Text.PlainText } - PlasmaComponents3.Label { + PlasmaComponents.Label { visible: monthView.currentDateAuxilliaryText.length > 0 Layout.leftMargin: calendar.paddings @@ -146,7 +150,7 @@ PlasmaExtras.Representation { maximumLineCount: 1 elide: Text.ElideRight } - PlasmaComponents3.ToolButton { + PlasmaComponents.ToolButton { id: addEventButton visible: agenda.visible && ApplicationIntegration.calendarInstalled @@ -159,7 +163,7 @@ PlasmaExtras.Representation { KeyNavigation.right: monthView.viewHeader.tabBar onClicked: ApplicationIntegration.launchCalendar() - KeyNavigation.tab: calendar.showAgenda && holidaysList.count ? holidaysList : holidaysList.KeyNavigation.down + KeyNavigation.tab: calendar.showAgenda && eventsList.count ? eventsList : eventsList.KeyNavigation.down } } } @@ -174,57 +178,42 @@ PlasmaExtras.Representation { Layout.fillHeight: true Layout.minimumHeight: Kirigami.Units.gridUnit * 4 - function formatDateWithoutYear(date) { + function formatDateWithoutYear(date: date): string { // Unfortunatelly Qt overrides ECMA's Date.toLocaleDateString(), // which is able to return locale-specific date-and-month-only date // formats, with its dumb version that only supports Qt::DateFormat // enum subset. So to get a day-and-month-only date format string we // must resort to this magic and hope there are no locales that use // other separators... - var format = Qt.locale().dateFormat(Locale.ShortFormat).replace(/[./ ]*Y{2,4}[./ ]*/i, ''); + const format = Qt.locale().dateFormat(Locale.ShortFormat).replace(/[./ ]*Y{2,4}[./ ]*/i, ''); return Qt.formatDate(date, format); } - function dateEquals(date1, date2) { - const values1 = [ - date1.getFullYear(), - date1.getMonth(), - date1.getDate() - ]; - - const values2 = [ - date2.getFullYear(), - date2.getMonth(), - date2.getDate() - ]; - - return values1.every((value, index) => { - return (value === values2[index]); - }, false) + function dateEquals(date1: date, date2: date): bool { + // Compare two dates without taking time into account + return date1.getFullYear() === date2.getFullYear() + && date1.getMonth() === date2.getMonth() + && date1.getDate() === date2.getDate(); + } + + function updateEventsForCurrentDate() { + eventsList.model = monthView.daysModel.eventsForDate(monthView.currentDate); } Connections { target: monthView function onCurrentDateChanged() { - // Apparently this is needed because this is a simple QList being - // returned and if the list for the current day has 1 event and the - // user clicks some other date which also has 1 event, QML sees the - // sizes match and does not update the labels with the content. - // Resetting the model to null first clears it and then correct data - // are displayed. - holidaysList.model = null; - holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate); + agenda.updateEventsForCurrentDate(); } } Connections { target: monthView.daysModel - function onAgendaUpdated(updatedDate) { + function onAgendaUpdated(updatedDate: date) { if (agenda.dateEquals(updatedDate, monthView.currentDate)) { - holidaysList.model = null; - holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate); + agenda.updateEventsForCurrentDate(); } } } @@ -240,12 +229,12 @@ PlasmaExtras.Representation { text: timeString.length > dateString.length ? timeString : dateString } - PlasmaComponents3.ScrollView { - id: holidaysView + PlasmaComponents.ScrollView { + id: eventsView anchors.fill: parent ListView { - id: holidaysList + id: eventsList focus: false activeFocusOnTab: true @@ -253,7 +242,7 @@ PlasmaExtras.Representation { currentIndex: -1 KeyNavigation.down: switchTimeZoneButton.visible ? switchTimeZoneButton : clocksList - Keys.onRightPressed: switchTimeZoneButton.Keys.rightPressed(event); + Keys.onRightPressed: event => switchTimeZoneButton.Keys.rightPressed(event) onCurrentIndexChanged: if (!activeFocus) { currentIndex = -1; @@ -265,9 +254,13 @@ PlasmaExtras.Representation { currentIndex = -1; } - delegate: PlasmaComponents3.ItemDelegate { + delegate: PlasmaComponents.ItemDelegate { id: eventItem - width: holidaysList.width + + // Crashes if the type is declared as eventData (which is Q_GADGET) + required property /*PlasmaCalendar.eventData*/var modelData + + width: ListView.view.width leftPadding: calendar.paddings @@ -275,7 +268,7 @@ PlasmaExtras.Representation { hoverEnabled: true highlighted: ListView.isCurrentItem Accessible.description: modelData.description - property bool hasTime: { + readonly property bool hasTime: { // Explicitly all-day event if (modelData.isAllDay) { return false; @@ -289,19 +282,19 @@ PlasmaExtras.Representation { // Non-explicit all-day event const startIsMidnight = modelData.startDateTime.getHours() === 0 - && modelData.startDateTime.getMinutes() === 0; + && modelData.startDateTime.getMinutes() === 0; const endIsMidnight = modelData.endDateTime.getHours() === 0 - && modelData.endDateTime.getMinutes() === 0; + && modelData.endDateTime.getMinutes() === 0; const sameDay = modelData.startDateTime.getDate() === modelData.endDateTime.getDate() - && modelData.startDateTime.getDay() === modelData.endDateTime.getDay() + && modelData.startDateTime.getDay() === modelData.endDateTime.getDay(); return !(startIsMidnight && endIsMidnight && sameDay); } - PlasmaComponents3.ToolTip { - text: modelData.description + PlasmaComponents.ToolTip { + text: eventItem.modelData.description visible: text !== "" && eventItem.hovered } @@ -320,50 +313,50 @@ PlasmaExtras.Representation { Layout.rowSpan: 2 Layout.fillHeight: true - color: modelData.eventColor + color: eventItem.modelData.eventColor width: 5 - visible: modelData.eventColor !== "" + visible: eventItem.modelData.eventColor !== "" } - PlasmaComponents3.Label { + PlasmaComponents.Label { id: startTimeLabel - readonly property bool startsToday: modelData.startDateTime - monthView.currentDate >= 0 - readonly property bool startedYesterdayLessThan12HoursAgo: modelData.startDateTime - monthView.currentDate >= -43200000 //12hrs in ms + readonly property bool startsToday: eventItem.modelData.startDateTime - monthView.currentDate >= 0 + readonly property bool startedYesterdayLessThan12HoursAgo: eventItem.modelData.startDateTime - monthView.currentDate >= -43200000 //12hrs in ms Layout.row: 0 Layout.column: 1 Layout.minimumWidth: dateLabelMetrics.width text: startsToday || startedYesterdayLessThan12HoursAgo - ? Qt.formatTime(modelData.startDateTime) - : agenda.formatDateWithoutYear(modelData.startDateTime) + ? Qt.formatTime(eventItem.modelData.startDateTime) + : agenda.formatDateWithoutYear(eventItem.modelData.startDateTime) textFormat: Text.PlainText horizontalAlignment: Qt.AlignRight visible: eventItem.hasTime } - PlasmaComponents3.Label { + PlasmaComponents.Label { id: endTimeLabel - readonly property bool endsToday: modelData.endDateTime - monthView.currentDate <= 86400000 // 24hrs in ms - readonly property bool endsTomorrowInLessThan12Hours: modelData.endDateTime - monthView.currentDate <= 86400000 + 43200000 // 36hrs in ms + readonly property bool endsToday: eventItem.modelData.endDateTime - monthView.currentDate <= 86400000 // 24hrs in ms + readonly property bool endsTomorrowInLessThan12Hours: eventItem.modelData.endDateTime - monthView.currentDate <= 86400000 + 43200000 // 36hrs in ms Layout.row: 1 Layout.column: 1 Layout.minimumWidth: dateLabelMetrics.width text: endsToday || endsTomorrowInLessThan12Hours - ? Qt.formatTime(modelData.endDateTime) - : agenda.formatDateWithoutYear(modelData.endDateTime) + ? Qt.formatTime(eventItem.modelData.endDateTime) + : agenda.formatDateWithoutYear(eventItem.modelData.endDateTime) textFormat: Text.PlainText horizontalAlignment: Qt.AlignRight - opacity: 0.7 + opacity: 0.75 visible: eventItem.hasTime } - PlasmaComponents3.Label { + PlasmaComponents.Label { id: eventTitle Layout.row: 0 @@ -371,7 +364,7 @@ PlasmaExtras.Representation { Layout.fillWidth: true elide: Text.ElideRight - text: modelData.title + text: eventItem.modelData.title textFormat: Text.PlainText verticalAlignment: Text.AlignVCenter maximumLineCount: 2 @@ -383,14 +376,15 @@ PlasmaExtras.Representation { } PlasmaExtras.PlaceholderMessage { - anchors.centerIn: holidaysView - width: holidaysView.width - (Kirigami.Units.gridUnit * 8) + anchors.centerIn: eventsView + width: eventsView.width - (Kirigami.Units.gridUnit * 8) - visible: holidaysList.count == 0 + visible: eventsList.count === 0 iconName: "checkmark" - text: monthView.isToday(monthView.currentDate) ? i18n("No events for today") - : i18n("No events for this day"); + text: monthView.isToday(monthView.currentDate) + ? i18n("No events for today") + : i18n("No events for this day"); } } @@ -407,7 +401,7 @@ PlasmaExtras.Representation { // Clocks stuff // ------------ - // Header text + button to change time & timezone + // Header text + button to change time & time zone PlasmaExtras.PlasmoidHeading { visible: worldClocks.visible @@ -436,21 +430,24 @@ PlasmaExtras.Representation { Accessible.ignored: true } - PlasmaComponents3.ToolButton { + PlasmaComponents.ToolButton { id: switchTimeZoneButton - visible: KAuthorized.authorizeControlModule("kcm_clock.desktop") + visible: KConfig.KAuthorized.authorizeControlModule("kcm_clock.desktop") text: i18n("Switch…") - Accessible.name: i18n("Switch to another timezone") icon.name: "preferences-system-time" - Accessible.description: i18n("Switch to another timezone") + Accessible.name: i18n("Switch to another time zone") + Accessible.description: i18n("Switch to another time zone") + KeyNavigation.down: clocksList - Keys.onRightPressed: monthView.Keys.downPressed(event) + Keys.onRightPressed: event => { + monthView.Keys.downPressed(event); + } - onClicked: KCMLauncher.openSystemSettings("kcm_clock") + onClicked: KCMUtils.KCMLauncher.openSystemSettings("kcm_clock") - PlasmaComponents3.ToolTip { + PlasmaComponents.ToolTip { text: parent.Accessible.description } } @@ -458,7 +455,7 @@ PlasmaExtras.Representation { } // Clocks view itself - PlasmaComponents3.ScrollView { + PlasmaComponents.ScrollView { id: worldClocks visible: calendar.showClocks @@ -479,47 +476,33 @@ PlasmaExtras.Representation { currentIndex = -1; } - Keys.onRightPressed: switchTimeZoneButton.Keys.rightPressed(event); + Keys.onRightPressed: event => { + switchTimeZoneButton.Keys.rightPressed(event); + } // Can't use KeyNavigation.tab since the focus won't go to config button, instead it will be redirected to somewhere else because of // some existing code. Since now the header was in this file and this was not a problem. Now the header is also implicitly // inside the monthViewWrapper. - Keys.onTabPressed: { + Keys.onTabPressed: event => { monthView.viewHeader.configureButton.forceActiveFocus(Qt.BacktabFocusReason); } - model: { - let timezones = []; - for (let i = 0; i < Plasmoid.configuration.selectedTimeZones.length; i++) { - let thisTzData = Plasmoid.configuration.selectedTimeZones[i]; - - /* Don't add this item if it's the same as the local time zone, which - * would indicate that the user has deliberately added a dedicated entry - * for the city of their normal time zone. This is not an error condition - * because the user may have done this on purpose so that their normal - * local time zone shows up automatically while they're traveling and - * they've switched the current local time zone to something else. But - * with this use case, when they're back in their normal local time zone, - * the clocks list would show two entries for the same city. To avoid - * this, let's suppress the duplicate. - */ - if (!(thisTzData !== "Local" && root.nameForZone(thisTzData) === root.nameForZone("Local"))) { - timezones.push(Plasmoid.configuration.selectedTimeZones[i]); - } - } - return timezones; - } + model: root.selectedTimeZonesDeduplicatingExplicitLocalTimeZone() - delegate: PlasmaComponents3.ItemDelegate { + delegate: PlasmaComponents.ItemDelegate { id: listItem - readonly property bool isCurrentTimeZone: modelData === Plasmoid.configuration.lastSelectedTimezone + + required property string modelData + + readonly property bool isCurrentTimeZone: root.timeZoneResolvesToLastSelectedTimeZone(modelData) + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin leftPadding: calendar.paddings rightPadding: calendar.paddings highlighted: ListView.isCurrentItem - Accessible.name: root.nameForZone(modelData) + Accessible.name: root.displayStringForTimeZone(modelData) Accessible.description: root.timeForZone(modelData, Plasmoid.configuration.showSeconds === 2) // Only highlight with keyboard @@ -527,18 +510,20 @@ PlasmaExtras.Representation { hoverEnabled: false contentItem: RowLayout { - PlasmaComponents3.Label { + spacing: Kirigami.Units.smallSpacing + + PlasmaComponents.Label { Layout.fillWidth: true - text: root.nameForZone(modelData) + text: root.displayStringForTimeZone(listItem.modelData) textFormat: Text.PlainText font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal maximumLineCount: 1 elide: Text.ElideRight } - PlasmaComponents3.Label { + PlasmaComponents.Label { horizontalAlignment: Qt.AlignRight - text: root.timeForZone(modelData, Plasmoid.configuration.showSeconds === 2) + text: root.timeForZone(listItem.modelData, Plasmoid.configuration.showSeconds === 2) textFormat: Text.PlainText font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal elide: Text.ElideRight @@ -560,7 +545,7 @@ PlasmaExtras.Representation { right: monthViewWrapper.left bottom: parent.bottom // Stretch all the way to the top of a dialog. This magic comes - // from PlasmaCore.Dialog::margins and CompactApplet containment. + // from PlasmaCore.PlasmaWindow::topPadding and CompactApplet containment. topMargin: calendar.parent ? -calendar.parent.y : 0 } @@ -591,6 +576,7 @@ PlasmaExtras.Representation { PlasmaCalendar.MonthView { id: monthView + viewHeader.height: calendar.headerHeight anchors { fill: parent @@ -602,7 +588,7 @@ PlasmaExtras.Representation { borderOpacity: 0.25 eventPluginsManager: eventPluginsManager - today: root.tzDate + today: root.currentDateTimeInSelectedTimeZone firstDayOfWeek: Plasmoid.configuration.firstDayOfWeek > -1 ? Plasmoid.configuration.firstDayOfWeek : Qt.locale().firstDayOfWeek @@ -614,7 +600,9 @@ PlasmaExtras.Representation { KeyNavigation.left: KeyNavigation.tab KeyNavigation.tab: addEventButton.visible ? addEventButton : addEventButton.KeyNavigation.down - Keys.onUpPressed: viewHeader.tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason); + Keys.onUpPressed: event => { + viewHeader.tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason); + } } } } diff --git a/contents/ui/DigitalClock.qml b/contents/ui/DigitalClock.qml index 1f24a74..e6a60a9 100644 --- a/contents/ui/DigitalClock.qml +++ b/contents/ui/DigitalClock.qml @@ -7,30 +7,31 @@ SPDX-License-Identifier: GPL-2.0-or-later */ +pragma ComponentBehavior: Bound + import QtQuick -import QtQuick.Layouts 1.1 -import QtQuick.Window 2.2 -import org.kde.plasma.plasmoid 2.0 +import QtQuick.Layouts + +import org.kde.plasma.plasmoid import org.kde.plasma.core as PlasmaCore -import org.kde.plasma.components 3.0 as Components -import org.kde.plasma.private.digitalclock 1.0 -import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.components as PlasmaComponents +import org.kde.plasma.private.digitalclock +import org.kde.kirigami as Kirigami MouseArea { id: main objectName: "digital-clock-compactrepresentation" property string timeFormat + property string timeFormatOriginal property string timeFormatWithSeconds - - property bool showLocalTimezone: Plasmoid.configuration.showLocalTimezone - property bool showDate: Plasmoid.configuration.showDate + property string timeFormatWithSecondsOriginal property bool splitIfVertical: Plasmoid.configuration.splitIfVertical // This is quite convoluted in Qt 6: // Qt.formatDate with locale only accepts Locale.FormatType as format type, // no Qt.DateFormat (ISODate) and no format string. // Locale.toString on the other hand only formats a date *with* time... - property var dateFormatter: { + readonly property var dateFormatter: { if (Plasmoid.configuration.dateFormat === "custom") { Plasmoid.configuration.customDateFormat; // create a binding dependency on this property. return (d) => { @@ -51,19 +52,15 @@ MouseArea { } } - property string lastSelectedTimezone: Plasmoid.configuration.lastSelectedTimezone - property int displayTimezoneFormat: Plasmoid.configuration.displayTimezoneFormat - property int use24hFormat: Plasmoid.configuration.use24hFormat - property string lastDate: "" property int tzOffset - // This is the index in the list of user selected timezones + // This is the index in the list of user selected time zones property int tzIndex: 0 // if showing the date and the time in one line or - // if the date/timezone cannot be fit with the smallest font to its designated space - property bool oneLineMode: { + // if the date/time zone cannot be fit with the smallest font to its designated space + readonly property bool oneLineMode: { if (Plasmoid.configuration.dateDisplayFormat === 1) { // BesideTime return true; @@ -73,8 +70,8 @@ MouseArea { } else { // Adaptive return Plasmoid.formFactor === PlasmaCore.Types.Horizontal && - main.height <= 2 * Kirigami.Theme.smallFont.pixelSize && - (main.showDate || timezoneLabel.visible); + height <= 2 * Kirigami.Theme.smallFont.pixelSize && + (Plasmoid.configuration.showDate || timeZoneLabel.visible); } } @@ -82,20 +79,7 @@ MouseArea { property int wheelDelta: 0 Accessible.role: Accessible.Button - Accessible.onPressAction: main.clicked(null) - - onDateFormatterChanged: { - setupLabels(); - } - - onDisplayTimezoneFormatChanged: { setupLabels(); } - onStateChanged: { setupLabels(); } - - onLastSelectedTimezoneChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } - onShowLocalTimezoneChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } - onShowDateChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } - onSplitIfVerticalChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } - onUse24hFormatChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } + Accessible.onPressAction: clicked(null) Connections { target: Plasmoid @@ -108,30 +92,63 @@ MouseArea { Connections { target: Plasmoid.configuration function onSelectedTimeZonesChanged() { - // If the currently selected timezone was removed, + // If the currently selected time zone was removed, // default to the first one in the list - const lastSelectedTimezone = Plasmoid.configuration.lastSelectedTimezone; - if (Plasmoid.configuration.selectedTimeZones.indexOf(lastSelectedTimezone) === -1) { + if (Plasmoid.configuration.selectedTimeZones.indexOf(Plasmoid.configuration.lastSelectedTimezone) === -1) { Plasmoid.configuration.lastSelectedTimezone = Plasmoid.configuration.selectedTimeZones[0]; } - setupLabels(); - setTimezoneIndex(); + main.setupLabels(); + main.setTimeZoneIndex(); + } + + function onDisplayTimezoneFormatChanged() { + main.setupLabels(); + } + + function onLastSelectedTimezoneChanged() { + main.timeFormatCorrection(); + } + + function onShowLocalTimezoneChanged() { + main.timeFormatCorrection(); + } + + function onShowDateChanged() { + main.timeFormatCorrection(); + } + + function onUse24hFormatChanged() { + main.timeFormatCorrection(); + } + + function onSplitIfVerticalChanged(){ + timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } } - function getCurrentTime() { - // get the time for the given timezone from the dataengine - var now = dataSource.data[Plasmoid.configuration.lastSelectedTimezone]["DateTime"]; + function getCurrentTime(): date { + const data = dataSource.data[Plasmoid.configuration.lastSelectedTimezone]; + // The order of signal propagation is unspecified, so we might get + // here before the dataSource has updated. Alternatively, a buggy + // configuration view might set lastSelectedTimezone to a new time + // zone before applying the new list, or it may just be set to + // something invalid in the config file. + if (data === undefined) { + return new Date(); + } + + // get the time for the given time zone from the dataengine + const now = data["DateTime"]; // get current UTC time - var msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); + const msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); // add the dataengine TZ offset to it - var currentTime = new Date(msUTC + (dataSource.data[Plasmoid.configuration.lastSelectedTimezone]["Offset"] * 1000)); + const currentTime = new Date(msUTC + (data["Offset"] * 1000)); return currentTime; } - function pointToPixel(pointSize) { - var pixelsPerInch = Screen.pixelDensity * 25.4 + function pointToPixel(pointSize: int): int { + const pixelsPerInch = Screen.pixelDensity * 25.4 return Math.round(pointSize / 72 * pixelsPerInch) } @@ -151,15 +168,15 @@ MouseArea { PropertyChanges { target: contentItem - height: timeLabel.height + (main.showDate || timezoneLabel.visible ? 0.8 * timeLabel.height : 0) - width: Math.max(timeLabel.width + (main.showDate ? timezoneLabel.paintedWidth : 0), - timezoneLabel.paintedWidth, dateLabel.paintedWidth) + Kirigami.Units.largeSpacing + height: timeLabel.height + (Plasmoid.configuration.showDate || timeZoneLabel.visible ? 0.8 * timeLabel.height : 0) + width: Math.max(timeLabel.width + (Plasmoid.configuration.showDate ? timeZoneLabel.paintedWidth : 0), + timeZoneLabel.paintedWidth, dateLabel.paintedWidth) + Kirigami.Units.largeSpacing } PropertyChanges { target: labelsGrid - rows: main.showDate ? 1 : 2 + rows: Plasmoid.configuration.showDate ? 1 : 2 } AnchorChanges { @@ -172,18 +189,18 @@ MouseArea { target: timeLabel height: sizehelper.height - width: timeLabel.paintedWidth + width: sizehelper.contentWidth font.pixelSize: timeLabel.height } PropertyChanges { - target: timezoneLabel + target: timeZoneLabel - height: main.showDate ? 0.7 * timeLabel.height : 0.8 * timeLabel.height - width: main.showDate ? timezoneLabel.paintedWidth : timeLabel.width + height: Plasmoid.configuration.showDate ? 0.7 * timeLabel.height : 0.8 * timeLabel.height + width: Plasmoid.configuration.showDate ? timeZoneLabel.paintedWidth : timeLabel.width - font.pixelSize: timezoneLabel.height + font.pixelSize: timeZoneLabel.height } PropertyChanges { @@ -210,10 +227,10 @@ MouseArea { * The value 0.71 was picked by testing to give the clock the right * size (aligned with tray icons). * Value 0.56 seems to be chosen rather arbitrary as well such that - * the time label is slightly larger than the date or timezone label + * the time label is slightly larger than the date or time zone label * and still fits well into the panel with all the applied margins. */ - height: Math.min(main.showDate || timezoneLabel.visible ? main.height * 0.56 : main.height * 0.71, + height: Math.min(Plasmoid.configuration.showDate || timeZoneLabel.visible ? main.height * 0.56 : main.height * 0.71, fontHelper.font.pixelSize) font.pixelSize: sizehelper.height @@ -238,7 +255,7 @@ MouseArea { target: contentItem height: sizehelper.height - width: dateLabel.width + dateLabel.anchors.rightMargin + labelsGrid.width + width: (dateLabel.visible ? dateLabel.width + dateLabel.anchors.rightMargin : 0) + labelsGrid.width } AnchorChanges { @@ -255,7 +272,9 @@ MouseArea { font.pixelSize: 1024 verticalAlignment: Text.AlignVCenter - anchors.rightMargin: labelsGrid.columnSpacing + // between date and time; they are styled the same, so + // a space is more appropriate than smallSpacing + anchors.rightMargin: timeMetrics.advanceWidth(" ") fontSizeMode: Text.VerticalFit } @@ -271,16 +290,16 @@ MouseArea { target: timeLabel height: sizehelper.height - width: timeLabel.paintedWidth + width: sizehelper.contentWidth fontSizeMode: Text.VerticalFit } PropertyChanges { - target: timezoneLabel + target: timeZoneLabel height: 0.7 * timeLabel.height - width: timezoneLabel.paintedWidth + width: timeZoneLabel.paintedWidth fontSizeMode: Text.VerticalFit horizontalAlignment: Text.AlignHCenter @@ -311,7 +330,7 @@ MouseArea { PropertyChanges { target: contentItem - height: main.showDate ? labelsGrid.height + dateLabel.contentHeight : labelsGrid.height + height: Plasmoid.configuration.showDate ? labelsGrid.height + dateLabel.contentHeight : labelsGrid.height width: main.width } @@ -328,11 +347,11 @@ MouseArea { width: main.width font.pixelSize: Math.min(timeLabel.height, fontHelper.font.pixelSize) - fontSizeMode: Text.VerticalFit + fontSizeMode: Text.Fit } PropertyChanges { - target: timezoneLabel + target: timeZoneLabel height: Math.max(0.7 * timeLabel.height, minimumPixelSize) width: main.width @@ -410,7 +429,7 @@ MouseArea { } PropertyChanges { - target: timezoneLabel + target: timeZoneLabel height: 0.7 * timeLabel.height width: main.width @@ -443,12 +462,12 @@ MouseArea { target: sizehelper height: { - if (main.showDate) { - if (timezoneLabel.visible) { + if (Plasmoid.configuration.showDate) { + if (timeZoneLabel.visible) { return 0.4 * main.height } return 0.56 * main.height - } else if (timezoneLabel.visible) { + } else if (timeZoneLabel.visible) { return 0.59 * main.height } return main.height @@ -469,7 +488,7 @@ MouseArea { } var delta = (wheel.inverted ? -1 : 1) * (wheel.angleDelta.y ? wheel.angleDelta.y : wheel.angleDelta.x); - var newIndex = main.tzIndex; + var newIndex = tzIndex; wheelDelta += delta; // magic number 120 for common "one click" // See: https://doc.qt.io/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop @@ -488,9 +507,9 @@ MouseArea { newIndex = Plasmoid.configuration.selectedTimeZones.length - 1; } - if (newIndex !== main.tzIndex) { + if (newIndex !== tzIndex) { Plasmoid.configuration.lastSelectedTimezone = Plasmoid.configuration.selectedTimeZones[newIndex]; - main.tzIndex = newIndex; + tzIndex = newIndex; dataSource.dataChanged(); setupLabels(); @@ -513,18 +532,19 @@ MouseArea { verticalItemAlignment: Grid.AlignVCenter flow: Grid.TopToBottom + // between time and timezone; timezone is styled differently, so + // smallSpacing is more appropriate than a space columnSpacing: Kirigami.Units.smallSpacing - Components.Label { + PlasmaComponents.Label { id: timeLabel font { family: fontHelper.font.family weight: fontHelper.font.weight italic: fontHelper.font.italic + features: { "tnum": 1 } pixelSize: 1024 - pointSize: -1 // Because we're setting the pixel size instead - // TODO: remove once this label is ported to PC3 } minimumPixelSize: 1 @@ -535,14 +555,12 @@ MouseArea { horizontalAlignment: Text.AlignHCenter } - Components.Label { - id: timezoneLabel + PlasmaComponents.Label { + id: timeZoneLabel font.weight: timeLabel.font.weight font.italic: timeLabel.font.italic font.pixelSize: 1024 - font.pointSize: -1 // Because we're setting the pixel size instead - // TODO: remove once this label is ported to PC3 minimumPixelSize: 1 visible: text.length > 0 @@ -552,17 +570,15 @@ MouseArea { } } - Components.Label { + PlasmaComponents.Label { id: dateLabel - visible: main.showDate + visible: Plasmoid.configuration.showDate font.family: timeLabel.font.family font.weight: timeLabel.font.weight font.italic: timeLabel.font.italic font.pixelSize: 1024 - font.pointSize: -1 // Because we're setting the pixel size instead - // TODO: remove once this label is ported to PC3 minimumPixelSize: 1 horizontalAlignment: Text.AlignHCenter @@ -575,7 +591,7 @@ MouseArea { * */ - Components.Label { + PlasmaComponents.Label { id: sizehelper font.family: timeLabel.font.family @@ -588,7 +604,7 @@ MouseArea { } // To measure Label.height for maximum-sized font in VerticalFit mode - Components.Label { + PlasmaComponents.Label { id: fontHelper height: 1024 @@ -597,7 +613,6 @@ MouseArea { font.weight: Plasmoid.configuration.autoFontAndSize ? Kirigami.Theme.defaultFont.weight : Plasmoid.configuration.fontWeight font.italic: Plasmoid.configuration.autoFontAndSize ? Kirigami.Theme.defaultFont.italic : Plasmoid.configuration.italicText font.pixelSize: Plasmoid.configuration.autoFontAndSize ? 3 * Kirigami.Theme.defaultFont.pixelSize : pointToPixel(Plasmoid.configuration.fontSize) - font.pointSize: -1 fontSizeMode: Text.VerticalFit visible: false @@ -613,76 +628,102 @@ MouseArea { } // Qt's QLocale does not offer any modular time creating like Klocale did - // eg. no "gimme time with seconds" or "gimme time without seconds and with timezone". + // eg. no "gimme time with seconds" or "gimme time without seconds and with time zone". // QLocale supports only two formats - Long and Short. Long is unusable in many situations // and Short does not provide seconds. So if seconds are enabled, we need to add it here. // // What happens here is that it looks for the delimiter between "h" and "m", takes it // and appends it after "mm" and then appends "ss" for the seconds. - function timeFormatCorrection(timeFormatString) { + function timeFormatCorrection(timeFormatString = Qt.locale().timeFormat(Locale.ShortFormat)) { const regexp = /(hh*)(.+)(mm)/i const match = regexp.exec(timeFormatString); const hours = match[1]; - let delimiter = match[2]; + const delimiter = match[2]; const minutes = match[3] const seconds = "ss"; const amPm = "AP"; - let amPmDelimiter = " "; + const amPmDelimiter = " "; + const splitDelimiter = "\n"; const uses24hFormatByDefault = timeFormatString.toLowerCase().indexOf("ap") === -1; - if (Plasmoid.formFactor === PlasmaCore.Types.Vertical - && main.splitIfVertical === true) { - delimiter = "\n"; - amPmDelimiter = "\n"; - } - // because QLocale is incredibly stupid and does not convert 12h/24h clock format // when uppercase H is used for hours, needs to be h or hh, so toLowerCase() let result = hours.toLowerCase() + delimiter + minutes; let result_sec = result + delimiter + seconds; + let result_split = hours.toLowerCase() + splitDelimiter + minutes; + + let result_sec_split = result_split + splitDelimiter + seconds; + // add "AM/PM" either if the setting is the default and locale uses it OR if the user unchecked "use 24h format" - if ((main.use24hFormat == Qt.PartiallyChecked && !uses24hFormatByDefault) || main.use24hFormat == Qt.Unchecked) { + if ((Plasmoid.configuration.use24hFormat === Qt.PartiallyChecked && !uses24hFormatByDefault) || Plasmoid.configuration.use24hFormat === Qt.Unchecked) { result += amPmDelimiter + amPm; result_sec += amPmDelimiter + amPm; + result_split += splitDelimiter + amPm; + result_sec_split += splitDelimiter + amPm; } - main.timeFormat = result; - main.timeFormatWithSeconds = result_sec; + timeFormatOriginal = result; + timeFormatWithSecondsOriginal = result_sec; + + if (Plasmoid.formFactor === PlasmaCore.Types.Vertical + && main.splitIfVertical === true) { + timeFormat = result_split; + timeFormatWithSeconds = result_sec_split; + } else { + timeFormat = result; + timeFormatWithSeconds = result_sec; + } setupLabels(); } function setupLabels() { - const showTimezone = main.showLocalTimezone || (Plasmoid.configuration.lastSelectedTimezone !== "Local" - && dataSource.data["Local"]["Timezone City"] !== dataSource.data[Plasmoid.configuration.lastSelectedTimezone]["Timezone City"]); + const lastSelectedData = dataSource.data[Plasmoid.configuration.lastSelectedTimezone]; + const localData = dataSource.data["Local"]; + // The order of signal propagation is unspecified, so we might get + // here before the dataSource has updated. Alternatively, a buggy + // configuration view might set lastSelectedTimezone to a new time + // zone before applying the new list, or it may just be set to + // something invalid in the config file. + if (lastSelectedData === undefined || localData === undefined) { + return; + } + + const showTimezone = Plasmoid.configuration.showLocalTimezone + || (Plasmoid.configuration.lastSelectedTimezone !== "Local" + && lastSelectedData["Timezone City"] !== localData["Timezone City"]); let timezoneString = ""; if (showTimezone) { - // format timezone as tz code, city or UTC offset - if (displayTimezoneFormat === 0) { - timezoneString = dataSource.data[lastSelectedTimezone]["Timezone Abbreviation"] - } else if (displayTimezoneFormat === 1) { - timezoneString = TimezonesI18n.i18nCity(dataSource.data[lastSelectedTimezone]["Timezone"]); - } else if (displayTimezoneFormat === 2) { - const lastOffset = dataSource.data[lastSelectedTimezone]["Offset"]; + // format time zone as tz code, city or UTC offset + switch (Plasmoid.configuration.displayTimezoneFormat) { + case 0: // Code + timezoneString = lastSelectedData["Timezone Abbreviation"] + break; + case 1: // City + timezoneString = TimeZonesI18n.i18nCity(lastSelectedData["Timezone"]); + break; + case 2: // Offset from UTC time + const lastOffset = lastSelectedData["Offset"]; const symbol = lastOffset > 0 ? '+' : ''; const hours = Math.floor(lastOffset / 3600); const minutes = Math.floor(lastOffset % 3600 / 60); timezoneString = "UTC" + symbol + hours.toString().padStart(2, '0') + ":" + minutes.toString().padStart(2, '0'); + break; + } + if ((Plasmoid.configuration.showDate || oneLineMode) && Plasmoid.formFactor === PlasmaCore.Types.Horizontal) { + timezoneString = `(${timezoneString})`; } - - timezoneLabel.text = (main.showDate || main.oneLineMode) && Plasmoid.formFactor === PlasmaCore.Types.Horizontal ? "(" + timezoneString + ")" : timezoneString; - } else { - // this clears the label and that makes it hidden - timezoneLabel.text = timezoneString; } + // an empty string clears the label and that makes it hidden + timeZoneLabel.text = timezoneString; - if (main.showDate) { - dateLabel.text = main.dateFormatter(main.getCurrentTime()); + if (Plasmoid.configuration.showDate) { + dateLabel.text = dateFormatter(getCurrentTime()); } else { // clear it so it doesn't take space in the layout dateLabel.text = ""; @@ -699,7 +740,7 @@ MouseArea { } } // replace all placeholders with the widest number (two digits) - const format = main.timeFormat.replace(/(h+|m+|s+)/g, "" + maximumWidthNumber + maximumWidthNumber); // make sure maximumWidthNumber is formatted as string + const format = (Plasmoid.configuration.showSeconds === 2 ? main.timeFormatWithSeconds : main.timeFormat).replace(/(h+|m+|s+)/g, "" + maximumWidthNumber + maximumWidthNumber); // make sure maximumWidthNumber is formatted as string // build the time string twice, once with an AM time and once with a PM time const date = new Date(2000, 0, 1, 1, 0, 0); const timeAm = Qt.formatTime(date, format); @@ -719,45 +760,59 @@ MouseArea { function dateTimeChanged() { let doCorrections = false; - if (main.showDate) { + if (Plasmoid.configuration.showDate) { // If the date has changed, force size recalculation, because the day name // or the month name can now be longer/shorter, so we need to adjust applet size - const currentDate = Qt.formatDateTime(main.getCurrentTime(), "yyyy-MM-dd"); - if (main.lastDate !== currentDate) { + const currentDate = Qt.formatDateTime(getCurrentTime(), "yyyy-MM-dd"); + if (lastDate !== currentDate) { doCorrections = true; - main.lastDate = currentDate + lastDate = currentDate } } - const currentTZOffset = dataSource.data["Local"]["Offset"] / 60; - if (currentTZOffset !== tzOffset) { + const currentTimeZoneOffset = dataSource.data["Local"]["Offset"] / 60; + if (currentTimeZoneOffset !== tzOffset) { doCorrections = true; - tzOffset = currentTZOffset; + tzOffset = currentTimeZoneOffset; Date.timeZoneUpdated(); // inform the QML JS engine about TZ change } if (doCorrections) { - timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)); + timeFormatCorrection(); } } - function setTimezoneIndex() { - main.tzIndex = Plasmoid.configuration.selectedTimeZones.indexOf(Plasmoid.configuration.lastSelectedTimezone); + function setTimeZoneIndex() { + tzIndex = Plasmoid.configuration.selectedTimeZones.indexOf(Plasmoid.configuration.lastSelectedTimezone); } Component.onCompleted: { - // Sort the timezones according to their offset + // Sort the time zones according to their offset // Calling sort() directly on Plasmoid.configuration.selectedTimeZones // has no effect, so sort a copy and then assign the copy to it - const sortedTimeZones = Plasmoid.configuration.selectedTimeZones; - const byOffset = (a, b) => dataSource.data[a]["Offset"] - dataSource.data[b]["Offset"]; + const byOffset = (a, b) => a.offset - b.offset; + const sortedTimeZones = Plasmoid.configuration.selectedTimeZones + .map(timeZone => ({ + timeZone, + // If not found, move it to the bottom by giving it the highest offset as a fallback + offset: dataSource.data[timeZone]?.["Offset"] ?? 86400, + })); sortedTimeZones.sort(byOffset); - Plasmoid.configuration.selectedTimeZones = sortedTimeZones; + Plasmoid.configuration.selectedTimeZones = sortedTimeZones + .map(({ timeZone }) => timeZone); - setTimezoneIndex(); + setTimeZoneIndex(); tzOffset = -(new Date().getTimezoneOffset()); dateTimeChanged(); - timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)); - dataSource.onDataChanged.connect(dateTimeChanged); + timeFormatCorrection(); + + dataSource.dataChanged + .connect(dateTimeChanged); + + dateFormatterChanged + .connect(setupLabels); + + stateChanged + .connect(setupLabels); } } diff --git a/contents/ui/Messages.sh b/contents/ui/Messages.sh new file mode 100644 index 0000000..7fe4ba1 --- /dev/null +++ b/contents/ui/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.digitalclock.pot diff --git a/contents/ui/NoTimezoneWarning.qml b/contents/ui/NoTimezoneWarning.qml new file mode 100644 index 0000000..61d849c --- /dev/null +++ b/contents/ui/NoTimezoneWarning.qml @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2024 Niccolò Venerandi + + SPDX-License-Identifier: GPL-2.0-or-later + */ +import QtQuick +import QtQuick.Layouts + +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.components as PlasmaComponents +import org.kde.plasma.plasmoid 2.0 +import org.kde.kirigami 2.20 as Kirigami + +import org.kde.kcmutils as KCM + +MouseArea { + id: root + + Layout.minimumWidth: mainLayout.implicitWidth + Layout.minimumHeight: mainLayout.implicitHeight + + onClicked: KCM.KCMLauncher.openSystemSettings("kcm_clock") + + + RowLayout { + id: mainLayout + anchors.fill: parent + spacing: Kirigami.Units.largeSpacing + + Kirigami.Icon { + source: "appointment-missed-symbolic" + Layout.fillWidth: true + Layout.fillHeight: true + } + + PlasmaComponents.Label { + text: i18nc("@label Shown in place of digital clock when no timezone is set", "Time zone is not set; click here to open Date & Time settings and set one") + visible: Plasmoid.formFactor == PlasmaCore.Types.Horizontal + } + } +} diff --git a/contents/ui/Tooltip.qml b/contents/ui/Tooltip.qml index ecf4201..f67ea9d 100644 --- a/contents/ui/Tooltip.qml +++ b/contents/ui/Tooltip.qml @@ -4,15 +4,17 @@ SPDX-License-Identifier: LGPL-2.0-or-later */ -import QtQml -import QtQuick 2.0 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 3.0 as PlasmaComponents3 -import org.kde.plasma.plasmoid 2.0 -import org.kde.kirigami 2.20 as Kirigami +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts + +import org.kde.plasma.components as PlasmaComponents +import org.kde.plasma.plasmoid +import org.kde.kirigami as Kirigami Item { - id: tooltipContentItem + id: toolTipContentItem property int preferredTextWidth: Kirigami.Units.gridUnit * 20 @@ -21,6 +23,7 @@ Item { LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true + Kirigami.Theme.colorSet: Kirigami.Theme.Window Kirigami.Theme.inherit: false @@ -30,9 +33,9 @@ Item { */ Accessible.name: i18nc("@info:tooltip %1 is a localized long date", "Today is %1", tooltipSubtext.text) Accessible.description: { - let description = tooltipSubLabelText.visible ? [tooltipSubLabelText.text] : []; - for (let i = 0; i < timezoneRepeater.count; i += 2) { - description.push(`${timezoneRepeater.itemAt(i).text}: ${timezoneRepeater.itemAt(i + 1).text}`); + const description = tooltipSubLabelText.visible ? [tooltipSubLabelText.text] : []; + for (let i = 0; i < timeZoneRepeater.count; i += 2) { + description.push(`${timeZoneRepeater.itemAt(i).text}: ${timeZoneRepeater.itemAt(i + 1).text}`); } return description.join('; '); } @@ -51,94 +54,94 @@ Item { Kirigami.Heading { id: tooltipMaintext - Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) - Layout.maximumWidth: preferredTextWidth + Layout.minimumWidth: Math.min(implicitWidth, toolTipContentItem.preferredTextWidth) + Layout.maximumWidth: toolTipContentItem.preferredTextWidth level: 3 elide: Text.ElideRight // keep this consistent with toolTipMainText in analog-clock - text: clocks.visible ? Qt.formatDate(tzDate, Qt.locale(), Locale.LongFormat) : Qt.formatDate(tzDate,"dddd") + property var mainText: clocks.visible ? Qt.formatDate(root.currentDateTimeInSelectedTimeZone, Qt.locale(), Locale.LongFormat) : Qt.locale().toString(root.currentDateTimeInSelectedTimeZone, "dddd") + property bool anyTimezoneSet: !!mainText + text: anyTimezoneSet ? mainText : i18nc("@label main text shown in digital clock's tooltip when timezone is missing", "Time zone is not set") textFormat: Text.PlainText } - PlasmaComponents3.Label { + PlasmaComponents.Label { id: tooltipSubtext - Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) - Layout.maximumWidth: preferredTextWidth + Layout.minimumWidth: Math.min(implicitWidth, toolTipContentItem.preferredTextWidth) + Layout.maximumWidth: toolTipContentItem.preferredTextWidth + maximumLineCount: 2 + wrapMode: Text.Wrap - text: { + property var subText: { if (Plasmoid.configuration.showSeconds === 0) { - return Qt.formatDate(tzDate, Qt.locale(), dateFormatString); + return Qt.formatDate(root.currentDateTimeInSelectedTimeZone, Qt.locale(), root.dateFormatString); } else { return "%1\n%2" - .arg(Qt.formatTime(tzDate, Qt.locale(), Locale.LongFormat)) - .arg(Qt.formatDate(tzDate, Qt.locale(), dateFormatString)) + .arg(Qt.formatTime(root.currentDateTimeInSelectedTimeZone, Qt.locale(), Locale.LongFormat)) + .arg(Qt.formatDate(root.currentDateTimeInSelectedTimeZone, Qt.locale(), root.dateFormatString)) } } - opacity: 0.6 + text: tooltipMaintext.anyTimezoneSet ? subText : i18nc("@label sub text shown in digital clock's tooltip when timezone is missing", "Click the clock icon to open Date & Time settings and set a time zone.") + opacity: 0.75 visible: !clocks.visible + font.features: { "tnum": 1 } } - PlasmaComponents3.Label { + PlasmaComponents.Label { id: tooltipSubLabelText - Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) - Layout.maximumWidth: preferredTextWidth - text: root.fullRepresentationItem ? root.fullRepresentationItem.monthView.todayAuxilliaryText : "" + Layout.minimumWidth: Math.min(implicitWidth, toolTipContentItem.preferredTextWidth) + Layout.maximumWidth: toolTipContentItem.preferredTextWidth + text: (root.fullRepresentationItem as CalendarView)?.monthView.todayAuxilliaryText ?? "" textFormat: Text.PlainText - opacity: 0.6 + opacity: 0.75 visible: !clocks.visible && text.length > 0 } GridLayout { id: clocks - Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) - Layout.maximumWidth: preferredTextWidth + Layout.minimumWidth: Math.min(implicitWidth, toolTipContentItem.preferredTextWidth) + Layout.maximumWidth: toolTipContentItem.preferredTextWidth Layout.minimumHeight: childrenRect.height - visible: timezoneRepeater.count > 2 + visible: timeZoneRepeater.count > 0 && tooltipMaintext.anyTimezoneSet columns: 2 rowSpacing: 0 Repeater { - id: timezoneRepeater - - model: { - let timezones = []; - for (let i = 0; i < Plasmoid.configuration.selectedTimeZones.length; i++) { - let thisTzData = Plasmoid.configuration.selectedTimeZones[i]; - - /* Don't add this item if it's the same as the local time zone, which - * would indicate that the user has deliberately added a dedicated entry - * for the city of their normal time zone. This is not an error condition - * because the user may have done this on purpose so that their normal - * local time zone shows up automatically while they're traveling and - * they've switched the current local time zone to something else. But - * with this use case, when they're back in their normal local time zone, - * the clocks list would show two entries for the same city. To avoid - * this, let's suppress the duplicate. - */ - if (!(thisTzData !== "Local" && nameForZone(thisTzData) === nameForZone("Local"))) { - timezones.push(thisTzData); - timezones.push(thisTzData); - } - } + id: timeZoneRepeater - return timezones; - } + model: root.selectedTimeZonesDeduplicatingExplicitLocalTimeZone() + // Duplicate each entry, because that's how we do "tables" with 2 columns in QML. :-\ + // An alternative would be a nested Repeater with an ObjectModel. + .reduce((array, item) => { + array.push(item, item); + return array; + }, []) + + PlasmaComponents.Label { + required property int index + required property string modelData - PlasmaComponents3.Label { // Layout.fillWidth is buggy here Layout.alignment: index % 2 === 0 ? Qt.AlignRight : Qt.AlignLeft text: { if (index % 2 === 0) { - return i18nc("@label %1 is a city or time zone name", "%1:", nameForZone(modelData)); + return i18nc("@label %1 is a city or time zone name", "%1:", root.displayStringForTimeZone(modelData)); } else { return timeForZone(modelData, Plasmoid.configuration.showSeconds > 0); } } textFormat: Text.PlainText - font.weight: modelData === Plasmoid.configuration.lastSelectedTimezone ? Font.Bold : Font.Normal + font.weight: root.timeZoneResolvesToLastSelectedTimeZone(modelData) ? Font.Bold : Font.Normal + font.features: { + if (index % 2 === 1) { + return { "tnum": 1 } + } else { + return {} + } + } wrapMode: Text.NoWrap elide: Text.ElideNone } diff --git a/contents/ui/configAppearance.qml b/contents/ui/configAppearance.qml index 39ef5ec..46e1a6d 100644 --- a/contents/ui/configAppearance.qml +++ b/contents/ui/configAppearance.qml @@ -7,34 +7,37 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ -import QtQuick 2.15 -import QtQuick.Controls 2.15 as QQC2 -import QtQuick.Layouts 1.15 -import QtQuick.Dialogs 6.3 as QtDialogs -import org.kde.plasma.plasmoid 2.0 +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import Qt.labs.platform as Platform + +import org.kde.plasma.plasmoid import org.kde.plasma.core as PlasmaCore -import org.kde.kcmutils // For KCMLauncher -import org.kde.config // For KAuthorized -import org.kde.kirigami 2.20 as Kirigami +import org.kde.config as KConfig +import org.kde.kcmutils as KCMUtils +import org.kde.kirigami as Kirigami -SimpleKCM { +KCMUtils.SimpleKCM { id: appearancePage property alias cfg_autoFontAndSize: autoFontAndSizeRadioButton.checked // boldText and fontStyleName are not used in DigitalClock.qml // However, they are necessary to remember the exact font style chosen. // Otherwise, when the user open the font dialog again, the style will be lost. - property alias cfg_fontFamily : fontDialog.fontChosen.family - property alias cfg_boldText : fontDialog.fontChosen.bold - property alias cfg_italicText : fontDialog.fontChosen.italic - property alias cfg_fontWeight : fontDialog.fontChosen.weight - property alias cfg_fontStyleName : fontDialog.fontChosen.styleName - property alias cfg_fontSize : fontDialog.fontChosen.pointSize + property alias cfg_fontFamily: fontDialog.fontChosen.family + property alias cfg_boldText: fontDialog.fontChosen.bold + property alias cfg_italicText: fontDialog.fontChosen.italic + property alias cfg_fontWeight: fontDialog.fontChosen.weight + property alias cfg_fontStyleName: fontDialog.fontChosen.styleName + property alias cfg_fontSize: fontDialog.fontChosen.pointSize property alias cfg_splitIfVertical: splitIfVertical.checked property string cfg_timeFormat: "" - property alias cfg_showLocalTimezone: showLocalTimezone.checked - property alias cfg_displayTimezoneFormat: displayTimezoneFormat.currentIndex + property alias cfg_showLocalTimezone: showLocalTimeZone.checked + property alias cfg_displayTimezoneFormat: displayTimeZoneFormat.currentIndex property alias cfg_showSeconds: showSecondsComboBox.currentIndex property alias cfg_showDate: showDate.checked @@ -43,10 +46,18 @@ SimpleKCM { property alias cfg_use24hFormat: use24hFormat.currentIndex property alias cfg_dateDisplayFormat: dateDisplayFormat.currentIndex + property real comboBoxWidth: Math.max(dateDisplayFormat.implicitWidth, + showSecondsComboBox.implicitWidth, + displayTimeZoneFormat.implicitWidth, + use24hFormat.implicitWidth, + dateFormat.implicitWidth) + + Kirigami.FormLayout { RowLayout { Kirigami.FormData.label: i18n("Information:") + spacing: Kirigami.Units.smallSpacing QQC2.CheckBox { id: showDate @@ -58,6 +69,7 @@ SimpleKCM { id: dateDisplayFormat enabled: showDate.checked visible: Plasmoid.formFactor !== PlasmaCore.Types.Vertical + Layout.preferredWidth: appearancePage.comboBoxWidth model: [ i18n("Adaptive location"), i18n("Always beside time"), @@ -67,8 +79,13 @@ SimpleKCM { } } + Item { + Kirigami.FormData.isSection: true + } + QQC2.ComboBox { id: showSecondsComboBox + Layout.preferredWidth: appearancePage.comboBoxWidth Kirigami.FormData.label: i18n("Show seconds:") model: [ i18nc("@option:check", "Never"), @@ -85,6 +102,7 @@ SimpleKCM { ColumnLayout { Kirigami.FormData.label: i18n("Show time zone:") Kirigami.FormData.buddyFor: showLocalTimeZoneWhenDifferent + spacing: Kirigami.Units.smallSpacing QQC2.RadioButton { id: showLocalTimeZoneWhenDifferent @@ -92,7 +110,7 @@ SimpleKCM { } QQC2.RadioButton { - id: showLocalTimezone + id: showLocalTimeZone text: i18n("Always") } } @@ -103,9 +121,14 @@ SimpleKCM { RowLayout { Kirigami.FormData.label: i18n("Display time zone as:") + Kirigami.FormData.buddyFor: displayTimeZoneFormat + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing QQC2.ComboBox { - id: displayTimezoneFormat + id: displayTimeZoneFormat + + Layout.preferredWidth: appearancePage.comboBoxWidth model: [ i18n("Code"), i18n("City"), @@ -113,6 +136,14 @@ SimpleKCM { ] onActivated: cfg_displayTimezoneFormat = currentIndex } + QQC2.Button { + id: switchTimeZoneButton + Layout.preferredWidth: Math.max(changeRegionalSettingsButton.implicitWidth, switchTimeZoneButton.implicitWidth, dateExampleLabel.implicitWidth) + visible: KConfig.KAuthorized.authorizeControlModule("kcm_clock") + text: i18nc("@action:button opens kcm", "Switch Time Zone…") + icon.name: "preferences-system-time" + onClicked: KCMUtils.KCMLauncher.openSystemSettings("kcm_clock") + } } Item { @@ -121,23 +152,27 @@ SimpleKCM { RowLayout { Layout.fillWidth: true - Kirigami.FormData.label: i18n("Time display:") + Kirigami.FormData.label: i18nc("@label:listbox", "Time display:") + spacing: Kirigami.Units.smallSpacing QQC2.ComboBox { id: use24hFormat + Layout.preferredWidth: appearancePage.comboBoxWidth model: [ - i18n("12-Hour"), - i18n("Use Region Defaults"), - i18n("24-Hour") + i18nc("@item:inlistbox time display option", "12-Hour"), + i18nc("@item:inlistbox time display option", "Use region defaults"), + i18nc("@item:inlistbox time display option", "24-Hour") ] - onCurrentIndexChanged: cfg_use24hFormat = currentIndex + onActivated: cfg_use24hFormat = currentIndex } QQC2.Button { - visible: KAuthorized.authorizeControlModule("kcm_regionandlang") - text: i18n("Change Regional Settings…") + id: changeRegionalSettingsButton + visible: KConfig.KAuthorized.authorizeControlModule("kcm_regionandlang") + Layout.preferredWidth: Math.max(changeRegionalSettingsButton.implicitWidth, switchTimeZoneButton.implicitWidth, dateExampleLabel.implicitWidth) + text: i18nc("@action:button opens kcm", "Change Regional Settings…") icon.name: "preferences-desktop-locale" - onClicked: KCMLauncher.openSystemSettings("kcm_regionandlang") + onClicked: KCMUtils.KCMLauncher.openSystemSettings("kcm_regionandlang") } } @@ -147,43 +182,45 @@ SimpleKCM { } RowLayout { - Kirigami.FormData.label: i18n("Date format:") + Kirigami.FormData.label: i18nc("@label:listbox", "Date format:") enabled: showDate.checked + spacing: Kirigami.Units.smallSpacing QQC2.ComboBox { id: dateFormat + Layout.preferredWidth: appearancePage.comboBoxWidth textRole: "label" model: [ { - label: i18n("Long Date"), + label: i18nc("@item:inlistbox date display option, includes e.g. day of week and month as word", "Long date"), name: "longDate", - formatter: (d) => { + formatter(d) { return Qt.formatDate(d, Qt.locale(), Locale.LongFormat); - } + }, }, { - label: i18n("Short Date"), + label: i18nc("@item:inlistbox date display option, e.g. all numeric", "Short date"), name: "shortDate", - formatter: (d) => { + formatter(d) { return Qt.formatDate(d, Qt.locale(), Locale.ShortFormat); - } + }, }, { - label: i18n("ISO Date"), + label: i18nc("@item:inlistbox date display option, yyyy-mm-dd", "ISO date"), name: "isoDate", - formatter: (d) => { + formatter(d) { return Qt.formatDate(d, Qt.ISODate); - } + }, }, { - label: i18nc("custom date format", "Custom"), + label: i18nc("@item:inlistbox custom date format", "Custom"), name: "custom", - formatter: (d) => { + formatter(d) { return Qt.locale().toString(d, customDateFormat.text); - } - } + }, + }, ] - onCurrentIndexChanged: cfg_dateFormat = model[currentIndex]["name"]; + onActivated: cfg_dateFormat = model[currentIndex]["name"]; Component.onCompleted: { const isConfiguredDateFormat = item => item["name"] === Plasmoid.configuration.dateFormat; @@ -192,7 +229,9 @@ SimpleKCM { } QQC2.Label { - Layout.fillWidth: true + id: dateExampleLabel + Layout.preferredWidth: Math.max(changeRegionalSettingsButton.implicitWidth, switchTimeZoneButton.implicitWidth, dateExampleLabel.implicitWidth) + horizontalAlignment: Text.AlignHCenter textFormat: Text.PlainText text: dateFormat.model[dateFormat.currentIndex].formatter(new Date()); } @@ -229,28 +268,36 @@ SimpleKCM { buttons: [autoFontAndSizeRadioButton, manualFontAndSizeRadioButton] } - QQC2.CheckBox { - Kirigami.FormData.label: i18nc("@label:group", "Text display:") - id: splitIfVertical - text: i18n("Split text if vertical") - enabled: !showDate.checked - } + ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.FormData.label: i18nc("@label:group", "Text display:") + Kirigami.FormData.buddyFor: autoFontAndSizeRadioButton - QQC2.RadioButton { - id: autoFontAndSizeRadioButton - text: i18nc("@option:radio", "Automatic") - } + QQC2.CheckBox { + Kirigami.FormData.label: i18nc("@label:group", "Text display:") + id: splitIfVertical + text: i18n("Split text if vertical") + enabled: !showDate.checked + } + QQC2.RadioButton { + id: autoFontAndSizeRadioButton + text: i18nc("@option:radio", "Automatic") + } - QQC2.Label { - text: i18nc("@label", "Text will follow the system font and expand to fill the available space.") - textFormat: Text.PlainText - Layout.fillWidth: true - wrapMode: Text.Wrap - font: Kirigami.Theme.smallFont + QQC2.Label { + text: i18nc("@label", "Text will follow the system font and expand to fill the available space.") + Layout.leftMargin: autoFontAndSizeRadioButton.indicator.width + autoFontAndSizeRadioButton.spacing + textFormat: Text.PlainText + Layout.fillWidth: true + wrapMode: Text.Wrap + font: Kirigami.Theme.smallFont + } } RowLayout { + spacing: Kirigami.Units.smallSpacing + QQC2.RadioButton { id: manualFontAndSizeRadioButton text: i18nc("@option:radio setting for manually configuring the font settings", "Manual") @@ -267,36 +314,53 @@ SimpleKCM { icon.name: "settings-configure" enabled: manualFontAndSizeRadioButton.checked onClicked: { - fontDialog.selectedFont = fontDialog.fontChosen + fontDialog.currentFont = fontDialog.fontChosen fontDialog.open() } } } - QQC2.Label { - visible: manualFontAndSizeRadioButton.checked - text: i18nc("@info %1 is the font size, %2 is the font family", "%1pt %2", cfg_fontSize, fontDialog.fontChosen.family) - textFormat: Text.PlainText - font: fontDialog.fontChosen + ColumnLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + visible: manualFontAndSizeRadioButton.checked + Layout.leftMargin: manualFontAndSizeRadioButton.indicator.width + manualFontAndSizeRadioButton.spacing + text: i18nc("@info %1 is the font size, %2 is the font family", "%1pt %2", cfg_fontSize, fontDialog.fontChosen.family) + textFormat: Text.PlainText + font: fontDialog.fontChosen + } + QQC2.Label { + visible: manualFontAndSizeRadioButton.checked + Layout.leftMargin: manualFontAndSizeRadioButton.indicator.width + manualFontAndSizeRadioButton.spacing + text: i18nc("@info", "Note: size may be reduced if the panel is not thick enough.") + textFormat: Text.PlainText + font: Kirigami.Theme.smallFont + } } } - QtDialogs.FontDialog { + // Use the Qt.Labs font dialog so it looks okay, or else we get the half-baked + // QML version shipped in Qt 6, which doesn't look good. + // Port back to the standard QtDialogs version when one of the following happens: + // Qt's QML font dialog implementation looks better + // We override the default dialog with our own in plasma-integration + Platform.FontDialog { id: fontDialog title: i18nc("@title:window", "Choose a Font") modality: Qt.WindowModal parentWindow: appearancePage.Window.window - property font fontChosen: Qt.font() + property font fontChosen: null onAccepted: { - fontChosen = selectedFont + fontChosen = font } } Component.onCompleted: { - if (!Plasmoid.configuration.showLocalTimezone) { + if (!Plasmoid.configuration.showLocalTimeZone) { showLocalTimeZoneWhenDifferent.checked = true; } } diff --git a/contents/ui/configCalendar.qml b/contents/ui/configCalendar.qml index 245be75..371304e 100644 --- a/contents/ui/configCalendar.qml +++ b/contents/ui/configCalendar.qml @@ -5,27 +5,39 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.workspace.calendar 2.0 as PlasmaCalendar -import org.kde.kirigami 2.20 as Kirigami -import org.kde.kcmutils as KCM -KCM.SimpleKCM { +import org.kde.plasma.plasmoid +import org.kde.plasma.workspace.calendar as PlasmaCalendar +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCMUtils + +KCMUtils.ScrollViewKCM { id: calendarPage signal configurationChanged() property alias cfg_showWeekNumbers: showWeekNumbers.checked property int cfg_firstDayOfWeek + property bool unsavedChanges: false + + extraFooterTopPadding: true function saveConfig() { Plasmoid.configuration.enabledCalendarPlugins = eventPluginsManager.enabledPlugins; + Plasmoid.configuration.writeConfig() + unsavedChanges = false + } + + function checkUnsavedChanges() { + calendarPage.unsavedChanges = !(Plasmoid.configuration.enabledCalendarPlugins.every(entry => eventPluginsManager.enabledPlugins.includes(entry)) && eventPluginsManager.enabledPlugins.every(entry => Plasmoid.configuration.enabledCalendarPlugins.includes(entry))) } - Kirigami.FormLayout { + header: Kirigami.FormLayout { PlasmaCalendar.EventPluginsManager { id: eventPluginsManager Component.onCompleted: { @@ -35,63 +47,68 @@ KCM.SimpleKCM { QQC2.CheckBox { id: showWeekNumbers - Kirigami.FormData.label: i18n("General:") - text: i18n("Show week numbers") + Kirigami.FormData.label: i18nc("@option:check formdata label", "General:") + text: i18nc("@option:check", "Show week numbers") } - RowLayout { + + QQC2.ComboBox { + id: firstDayOfWeekCombo + + Kirigami.FormData.label: i18nc("@label:listbox", "First day of week:") Layout.fillWidth: true - Kirigami.FormData.label: i18n("First day of week:") - - QQC2.ComboBox { - id: firstDayOfWeekCombo - textRole: "text" - model: [-1, 0, 1, 5, 6].map(day => ({ - day, - text: day === -1 ? i18n("Use Region Defaults") : Qt.locale().dayName(day), - })) - onActivated: cfg_firstDayOfWeek = model[index].day - currentIndex: model.findIndex(item => item.day === cfg_firstDayOfWeek) + + textRole: "text" + model: [-1, 0, 1, 5, 6].map(day => ({ + day, + text: day === -1 ? i18nc("@item:inlistbox first day of week option", "Use region defaults") : Qt.locale().dayName(day), + })) + onActivated: index => { + cfg_firstDayOfWeek = model[index].day; } + currentIndex: model.findIndex(item => item.day === cfg_firstDayOfWeek) } Item { Kirigami.FormData.isSection: true } + } - ColumnLayout { - id: calendarPluginsLayout - - Kirigami.FormData.label: i18n("Available Plugins:") - - Repeater { - id: calendarPluginsRepeater - - model: eventPluginsManager.model - - delegate: QQC2.CheckBox { - text: model.display - checked: model.checked - - Accessible.onPressAction: { - toggle(); - clicked(); - } + view: ListView { + id: pluginListView + activeFocusOnTab: true + model: eventPluginsManager.model + header: Kirigami.InlineViewHeader { + text: i18nc("@title:column", "Available Add-Ons") + width: pluginListView.width + } + headerPositioning: ListView.OverlayHeader + delegate: QQC2.CheckDelegate { + id: delegate + required property var model + width: pluginListView.width + checked: model.checked + text: model.display + property string subtitle: model.toolTip + icon.source: model.decoration + + Accessible.onPressAction: { + toggle(); + clicked(); + } - onClicked: { - //needed for model's setData to be called - model.checked = checked; - calendarPage.configurationChanged(); - } - } + onClicked: { + //needed for model's setData to be called + model.checked = checked; + calendarPage.checkUnsavedChanges(); + } - onItemAdded: (index, item) => { - if (index === 0) { - // Set buddy once, for an item in the first row. - // No, it doesn't work as a binding on children list. - calendarPluginsLayout.Kirigami.FormData.buddyFor = item; - } - } + contentItem: Kirigami.IconTitleSubtitle { + width: delegate.availableWidth + icon: icon.fromControlsIcon(delegate.icon) + title: delegate.text + subtitle: delegate.subtitle + selected: delegate.highlighted || delegate.pressed } } } diff --git a/contents/ui/configTimeZones.qml b/contents/ui/configTimeZones.qml index 7a9ec13..d5dd706 100644 --- a/contents/ui/configTimeZones.qml +++ b/contents/ui/configTimeZones.qml @@ -4,251 +4,272 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ +pragma ComponentBehavior: Bound + import QtQuick -import QtQuick.Controls 2.15 as QQC2 -import QtQuick.Layouts 1.15 +import QtQuick.Controls as QQC2 +import QtQuick.Layouts -import org.kde.kquickcontrolsaddons 2.0 // For kcmshell -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.private.digitalclock 1.0 +import org.kde.plasma.private.digitalclock import org.kde.kirigami as Kirigami - +import org.kde.config as KConfig import org.kde.kcmutils as KCMUtils -import org.kde.config // KAuthorized +import org.kde.plasma.workspace.timezoneselector as TimeZone -KCMUtils.ScrollViewKCM { - id: timeZonesPage +Kirigami.PageRow { + id: timeZonesRow + property string title + property string cfg_lastSelectedTimezone property alias cfg_selectedTimeZones: timeZones.selectedTimeZones property alias cfg_wheelChangesTimezone: enableWheelCheckBox.checked - TimeZoneModel { - id: timeZones + defaultColumnWidth: timeZonesRow.width + globalToolBar.style: Kirigami.ApplicationHeaderStyle.Auto - onSelectedTimeZonesChanged: { - if (selectedTimeZones.length === 0) { - // Don't let the user remove all time zones - messageWidget.visible = true; - timeZones.selectLocalTimeZone(); - } - } - } + /*Component.onCompleted: { + applicationWindow().footer.visible = Qt.binding(function() { + return timeZonesRow.currentIndex !== 1 || !timeZonesRow.visible + }) + }*/ - header: ColumnLayout { - spacing: Kirigami.Units.smallSpacing + Component.onCompleted: { + timeZonesRow.realFooter = applicationWindow().footer + } - QQC2.Label { - Layout.fillWidth: true - text: i18n("Tip: if you travel frequently, add your home time zone to this list. It will only appear when you change the systemwide time zone to something else.") - textFormat: Text.PlainText - wrapMode: Text.Wrap + onVisibleChanged: { + if (!visible && timeZonesRow.fakeFooter.parent) { + applicationWindow().footer = timeZonesRow.realFooter + timeZonesRow.fakeFooter.parent = null } } - view: ListView { - id: configuredTimezoneList - clip: true // Avoid visual glitches - focus: true // keyboard navigation - activeFocusOnTab: true // keyboard navigation - - model: TimeZoneFilterProxy { - sourceModel: timeZones - onlyShowChecked: true + onCurrentIndexChanged: { + if (currentIndex == 1) { + applicationWindow().footer = timeZonesRow.fakeFooter + timeZonesRow.realFooter.parent = null + } else { + applicationWindow().footer = timeZonesRow.realFooter + timeZonesRow.fakeFooter.parent = null } - // We have no concept of selection in this list, so don't pre-select - // the first item - currentIndex: -1 + } - delegate: Kirigami.RadioSubtitleDelegate { - id: timeZoneListItem + property Item realFooter + property Item fakeFooter: QQC2.DialogButtonBox { + background: Item { + Kirigami.Separator { + id: bottomSeparator + anchors { + left: parent.left + right: parent.right + top: parent.top + } + } + } + QQC2.Button { + text: i18n("Cancel") + onClicked: { + timeZonesRow.currentIndex = 0 + timeZoneSelector.selectedTimeZone = "" + } + } + QQC2.Button { + text: i18n("Add Selected Time Zone") + icon.name: "list-add" + enabled: timeZoneSelector.selectedTimeZone + onClicked: { + timeZones.selectedTimeZones = [...timeZones.selectedTimeZones, timeZoneSelector.selectedTimeZone] + timeZoneSelector.selectedTimeZone = "" + timeZonesRow.currentIndex = 0 + } + } + } - readonly property bool isCurrent: Plasmoid.configuration.lastSelectedTimezone === model.timeZoneId - readonly property bool isIdenticalToLocal: !model.isLocalTimeZone && model.city === timeZones.localTimeZoneCity() + initialPage: KCMUtils.ScrollViewKCM { - width: ListView.view.width + title: timeZonesRow.title - font.bold: isCurrent + actions: [ + Kirigami.Action { + text: i18n("Add Time Zone…") + icon.name: "list-add-symbolic" + Accessible.name: text // https://bugreports.qt.io/browse/QTBUG-130360 + onTriggered: { + if (timeZonesRow.depth == 1) { + timeZonesRow.push(timeZonesRow.addTimeZonePage) + } else { + timeZonesRow.currentIndex = 1 + } + } + } + ] - // Stripes help the eye line up the text on the left and the button on the right - Kirigami.Theme.useAlternateBackgroundColor: true + TimeZoneModel { + id: timeZones - text: model.city - subtitle: { - if (configuredTimezoneList.count > 1) { - if (isCurrent) { - return i18n("Clock is currently using this time zone"); - } else if (isIdenticalToLocal) { - return i18nc("@label This list item shows a time zone city name that is identical to the local time zone's city, and will be hidden in the timezone display in the plasmoid's popup", "Hidden while this is the local time zone's city"); - } + onSelectedTimeZonesChanged: { + if (selectedTimeZones.length === 0) { + // Don't let the user remove all time zones + messageWidget.visible = true; + timeZones.selectLocalTimeZone(); } - return ""; } + } - checked: isCurrent - onToggled: Plasmoid.configuration.lastSelectedTimezone = model.timeZoneId + view: ListView { + id: configuredTimeZoneList + clip: true // Avoid visual glitches + focus: true // keyboard navigation + activeFocusOnTab: true // keyboard navigation - contentItem: RowLayout { - spacing: Kirigami.Units.smallSpacing + model: TimeZoneFilterProxy { + sourceModel: timeZones + onlyShowChecked: true + } + // We have no concept of selection in this list, so don't pre-select + // the first item + currentIndex: -1 - Kirigami.TitleSubtitle { - Layout.fillWidth: true + delegate: Kirigami.RadioSubtitleDelegate { + id: timeZoneListItem - opacity: timeZoneListItem.isIdenticalToLocal ? 0.6 : 1.0 + required property int index // indirectly required by useAlternateBackgroundColor + required property var model - title: timeZoneListItem.text - subtitle: timeZoneListItem.subtitle + readonly property bool isCurrent: timeZonesRow.cfg_lastSelectedTimezone === model.timeZoneId + readonly property bool isIdenticalToLocal: !model.isLocalTimeZone && model.city === timeZones.localTimeZoneCity() - reserveSpaceForSubtitle: true - } + width: ListView.view.width - QQC2.Button { - visible: model.isLocalTimeZone && KAuthorized.authorizeControlModule("kcm_clock.desktop") - text: i18n("Switch Systemwide Time Zone…") - icon.name: "preferences-system-time" - font.bold: false - onClicked: KCMUtils.KCMLauncher.openSystemSettings("kcm_clock") + font.bold: isCurrent + + // Stripes help the eye line up the text on the left and the button on the right + Kirigami.Theme.useAlternateBackgroundColor: true + + text: model.city + subtitle: { + if (configuredTimeZoneList.count > 1) { + if (isCurrent) { + return i18n("Clock is currently using this time zone"); + } else if (isIdenticalToLocal) { + return i18nc("@label This list item shows a time zone city name that is identical to the local time zone's city, and will be hidden in the time zone display in the plasmoid's popup", "Hidden while this is the local time zone's city"); + } + } + return ""; } - QQC2.Button { - visible: !model.isLocalTimeZone && configuredTimezoneList.count > 1 - icon.name: "edit-delete" - font.bold: false - onClicked: model.checked = false; - QQC2.ToolTip { - text: i18n("Remove this time zone") + checked: isCurrent + + onToggled: { + if (checked) { + timeZonesRow.cfg_lastSelectedTimezone = model.timeZoneId; } } - } - } - section { - property: "isLocalTimeZone" - delegate: Kirigami.ListSectionHeader { - width: configuredTimezoneList.width - label: section === "true" ? i18n("Systemwide Time Zone") : i18n("Additional Time Zones") - } - } + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing - Kirigami.PlaceholderMessage { - visible: configuredTimezoneList.count === 1 - anchors { - top: parent.verticalCenter // Visual offset for system timezone and header - left: parent.left - right: parent.right - leftMargin: Kirigami.Units.largeSpacing * 6 - rightMargin: Kirigami.Units.largeSpacing * 6 - } - text: i18n("Add more time zones to display all of them in the applet's pop-up, or use one of them for the clock itself") - } - } + Kirigami.TitleSubtitle { + Layout.fillWidth: true - footer: ColumnLayout { - spacing: Kirigami.Units.smallSpacing + opacity: timeZoneListItem.isIdenticalToLocal ? 0.75 : 1.0 - QQC2.Button { - Layout.alignment: Qt.AlignLeft // Explicitly set so it gets reversed for LTR mode - text: i18n("Add Time Zones…") - icon.name: "list-add" - onClicked: timezoneSheet.open() - } + title: timeZoneListItem.text + subtitle: timeZoneListItem.subtitle - QQC2.CheckBox { - id: enableWheelCheckBox - enabled: configuredTimezoneList.count > 1 - Layout.fillWidth: true - Layout.topMargin: Kirigami.Units.largeSpacing - text: i18n("Switch displayed time zone by scrolling over clock applet") - } + reserveSpaceForSubtitle: true + } - QQC2.Label { - Layout.fillWidth: true - Layout.leftMargin: Kirigami.Units.largeSpacing * 2 - Layout.rightMargin: Kirigami.Units.largeSpacing * 2 - text: i18n("Using this feature does not change the systemwide time zone. When you travel, switch the systemwide time zone instead.") - textFormat: Text.PlainText - font: Kirigami.Theme.smallFont - wrapMode: Text.Wrap - } - } + QQC2.Button { + visible: timeZoneListItem.model.isLocalTimeZone && KConfig.KAuthorized.authorizeControlModule("kcm_clock.desktop") + text: i18n("Switch Systemwide Time Zone…") + icon.name: "preferences-system-time" + font.bold: false + onClicked: KCMUtils.KCMLauncher.openSystemSettings("kcm_clock") + } - Kirigami.OverlaySheet { - id: timezoneSheet + QQC2.Button { + visible: !timeZoneListItem.model.isLocalTimeZone && configuredTimeZoneList.count > 1 + icon.name: "edit-delete-remove" + font.bold: false + onClicked: timeZoneListItem.model.checked = false; + QQC2.ToolTip { + text: i18n("Remove this time zone") + } + } + } + } + + section { + property: "isLocalTimeZone" + delegate: Kirigami.ListSectionHeader { + required property string section - parent: timeZonesPage.QQC2.Overlay.overlay + width: configuredTimeZoneList.width + label: section === "true" ? i18n("Systemwide Time Zone") : i18n("Additional Time Zones") + } + } - onVisibleChanged: { - filter.text = ""; - messageWidget.visible = false; - if (visible) { - filter.forceActiveFocus() + Kirigami.PlaceholderMessage { + visible: configuredTimeZoneList.count === 1 + anchors { + top: parent.verticalCenter // Visual offset for system time zone and header + left: parent.left + right: parent.right + leftMargin: Kirigami.Units.largeSpacing * 6 + rightMargin: Kirigami.Units.largeSpacing * 6 + } + text: i18n("Add more time zones to display all of them in the applet's pop-up, or use one of them for the clock itself") } } - header: ColumnLayout { - Kirigami.Heading { + // Re-add separator line between footer and list view + extraFooterTopPadding: true + footer: ColumnLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { Layout.fillWidth: true - text: i18n("Add More Timezones") + leftPadding: Application.layoutDirection === Qt.LeftToRight ? enableWheelCheckBox.spacing : Kirigami.Units.largeSpacing * 2 + rightPadding: Application.layoutDirection === Qt.LeftToRight ? Kirigami.Units.largeSpacing * 2 : enableWheelCheckBox.spacing + text: i18nc("@info:usagetip shown below listview", "Tip: Add your home time zone to this list to see the time there even when you're traveling. It will not be shown twice while at home.") + font: Kirigami.Theme.smallFont textFormat: Text.PlainText wrapMode: Text.Wrap } - Kirigami.SearchField { - id: filter + QQC2.CheckBox { + id: enableWheelCheckBox + enabled: configuredTimeZoneList.count > 1 Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + text: i18n("Switch displayed time zone by scrolling over clock applet") } - Kirigami.InlineMessage { - id: messageWidget + + QQC2.Label { + enabled: enableWheelCheckBox.enabled + color: enabled ? Kirigami.Theme.textColor : Kirigami.Theme.disabledTextColor Layout.fillWidth: true - type: Kirigami.MessageType.Warning - text: i18n("At least one time zone needs to be enabled. Your local timezone was enabled automatically.") - showCloseButton: true + Layout.leftMargin: enableWheelCheckBox.indicator.width + enableWheelCheckBox.spacing + Layout.rightMargin: Kirigami.Units.largeSpacing * 2 + text: i18n("Using this feature does not change the systemwide time zone. When you travel, switch the systemwide time zone instead.") + textFormat: Text.PlainText + font: Kirigami.Theme.smallFont + wrapMode: Text.Wrap } } + } - footer: QQC2.DialogButtonBox { - standardButtons: QQC2.DialogButtonBox.Ok - onAccepted: timezoneSheet.close() - } - - ListView { - focus: true // keyboard navigation - activeFocusOnTab: true // keyboard navigation - clip: true - implicitWidth: Math.max(timeZonesPage.width/2, Kirigami.Units.gridUnit * 25) - - model: TimeZoneFilterProxy { - sourceModel: timeZones - filterString: filter.text - } - - delegate: QQC2.CheckDelegate { - required property int index - required property var model - - required checked - required property string city - required property string comment - required property string region - - width: ListView.view.width - focus: true // keyboard navigation - text: { - if (!city || city.indexOf("UTC") === 0) { - return comment; - } else if (comment) { - return i18n("%1, %2 (%3)", city, region, comment); - } else { - return i18n("%1, %2", city, region) - } - } + property Item addTimeZonePage: Kirigami.Page { + padding: 0 + title: i18n("Choose Time Zone") - onToggled: { - model.checked = checked + Layout.fillHeight: true + Layout.fillWidth: true - ListView.view.currentIndex = index // highlight - ListView.view.forceActiveFocus() // keyboard navigation - } - highlighted: ListView.isCurrentItem - } + TimeZone.TimezoneSelector { + id: timeZoneSelector + anchors.fill: parent } } + } diff --git a/contents/ui/main.qml b/contents/ui/main.qml index 0dab0f0..caac42a 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -4,91 +4,162 @@ SPDX-License-Identifier: GPL-2.0-or-later */ -import QtQuick 2.15 -import QtQuick.Layouts 1.1 -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core as PlasmaCore -import org.kde.plasma.plasma5support 2.0 as P5Support -import org.kde.plasma.private.digitalclock 1.0 -import org.kde.kquickcontrolsaddons 2.0 -import org.kde.kirigami 2.20 as Kirigami -import org.kde.kcmutils // KCMLauncher -import org.kde.config // KAuthorized +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts + +import org.kde.plasma.plasmoid +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.plasma5support as P5Support +import org.kde.plasma.private.digitalclock +import org.kde.kirigami as Kirigami +import org.kde.config as KConfig +import org.kde.kcmutils as KCMUtils PlasmoidItem { id: root width: Kirigami.Units.gridUnit * 10 height: Kirigami.Units.gridUnit * 4 - property string dateFormatString: setDateFormatString() + Plasmoid.backgroundHints: PlasmaCore.Types.ShadowBackground | PlasmaCore.Types.ConfigurableBackground - property date tzDate: { + + readonly property string dateFormatString: setDateFormatString() + + readonly property date currentDateTimeInSelectedTimeZone: { const data = dataSource.data[Plasmoid.configuration.lastSelectedTimezone]; + // The order of signal propagation is unspecified, so we might get + // here before the dataSource has updated. Alternatively, a buggy + // configuration view might set lastSelectedTimezone to a new time + // zone before applying the new list, or it may just be set to + // something invalid in the config file. if (data === undefined) { return new Date(); } - // get the time for the given timezone from the dataengine + // get the time for the given time zone from the dataengine const now = data["DateTime"]; // get current UTC time - const msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); - // add the dataengine TZ offset to it - return new Date(msUTC + (data["Offset"] * 1000)); + const nowUtcMilliseconds = now.getTime() + (now.getTimezoneOffset() * 60000); + const selectedTimeZoneOffsetMilliseconds = data["Offset"] * 1000; + // add the selected time zone's offset to it + return new Date(nowUtcMilliseconds + selectedTimeZoneOffsetMilliseconds); } - function initTimezones() { - const tz = [] + function initTimeZones() { + const timeZones = []; if (Plasmoid.configuration.selectedTimeZones.indexOf("Local") === -1) { - tz.push("Local"); + timeZones.push("Local"); } - root.allTimezones = tz.concat(Plasmoid.configuration.selectedTimeZones); + root.allTimeZones = timeZones.concat(Plasmoid.configuration.selectedTimeZones); } - function timeForZone(zone, showSecondsForZone) { + function timeForZone(timeZone: string, showSeconds: bool): string { if (!compactRepresentationItem) { return ""; } - // get the time for the given timezone from the dataengine - const now = dataSource.data[zone]["DateTime"]; + const data = dataSource.data[timeZone]; + if (data === undefined) { + return ""; + } + + // get the time for the given time zone from the dataengine + const now = data["DateTime"]; // get current UTC time const msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); // add the dataengine TZ offset to it - const dateTime = new Date(msUTC + (dataSource.data[zone]["Offset"] * 1000)); + const dateTime = new Date(msUTC + (data["Offset"] * 1000)); let formattedTime; - if (showSecondsForZone) { - formattedTime = Qt.formatTime(dateTime, compactRepresentationItem.timeFormatWithSeconds); + if (showSeconds) { + formattedTime = Qt.formatTime(dateTime, compactRepresentationItem.timeFormatWithSecondsOriginal); } else { - formattedTime = Qt.formatTime(dateTime, compactRepresentationItem.timeFormat); + formattedTime = Qt.formatTime(dateTime, compactRepresentationItem.timeFormatOriginal); } if (dateTime.getDay() !== dataSource.data["Local"]["DateTime"].getDay()) { - formattedTime += " (" + compactRepresentationItem.dateFormatter(dateTime) + ")"; + formattedTime += " (" + compactRepresentationItem.item.dateFormatter(dateTime) + ")"; } return formattedTime; } - function nameForZone(zone) { - // add the timezone string to the clock + function displayStringForTimeZone(timeZone: string): string { + const data = dataSource.data[timeZone]; + if (data === undefined) { + return timeZone; + } + + // add the time zone string to the clock if (Plasmoid.configuration.displayTimezoneAsCode) { - return dataSource.data[zone]["Timezone Abbreviation"]; + return data["Timezone Abbreviation"]; } else { - return TimezonesI18n.i18nCity(dataSource.data[zone]["Timezone"]); + return TimeZonesI18n.i18nCity(data["Timezone"]); } } - preferredRepresentation: compactRepresentation - compactRepresentation: DigitalClock { - activeFocusOnTab: true - hoverEnabled: true + function selectedTimeZonesDeduplicatingExplicitLocalTimeZone():/* [string] */var { + const displayStringForLocalTimeZone = displayStringForTimeZone("Local"); + /* + * Don't add this item if it's the same as the local time zone, which + * would indicate that the user has deliberately added a dedicated entry + * for the city of their normal time zone. This is not an error condition + * because the user may have done this on purpose so that their normal + * local time zone shows up automatically while they're traveling and + * they've switched the current local time zone to something else. But + * with this use case, when they're back in their normal local time zone, + * the clocks list would show two entries for the same city. To avoid + * this, let's suppress the duplicate. + */ + const isLiterallyLocalOrResolvesToSomethingOtherThanLocal = timeZone => + timeZone === "Local" || displayStringForTimeZone(timeZone) !== displayStringForLocalTimeZone; + + return Plasmoid.configuration.selectedTimeZones + .filter(isLiterallyLocalOrResolvesToSomethingOtherThanLocal) + .sort((a, b) => dataSource.data[a]["Offset"] - dataSource.data[b]["Offset"]); + } - Accessible.name: tooltipLoader.item.Accessible.name - Accessible.description: tooltipLoader.item.Accessible.description + function timeZoneResolvesToLastSelectedTimeZone(timeZone: string): bool { + return timeZone === Plasmoid.configuration.lastSelectedTimezone + || displayStringForTimeZone(timeZone) === displayStringForTimeZone(Plasmoid.configuration.lastSelectedTimezone); } + + preferredRepresentation: compactRepresentation + fullRepresentation: CalendarView { } + compactRepresentation: Loader { + id: conditionalLoader + + property bool containsMouse: item?.containsMouse ?? false + Layout.minimumWidth: item.Layout.minimumWidth + Layout.minimumHeight: item.Layout.minimumHeight + Layout.preferredWidth: item.Layout.preferredWidth + Layout.preferredHeight: item.Layout.preferredHeight + Layout.maximumWidth: item.Layout.maximumWidth + Layout.maximumHeight: item.Layout.maximumHeight + + sourceComponent: (currentDateTimeInSelectedTimeZone == "Invalid Date") ? noTimezoneComponent : digitalClockComponent + } + + Component { + id: digitalClockComponent + DigitalClock { + activeFocusOnTab: true + hoverEnabled: true + + Accessible.name: tooltipLoader.item.Accessible.name + Accessible.description: tooltipLoader.item.Accessible.description + } + } + + Component { + id: noTimezoneComponent + NoTimezoneWarning { } + } + toolTipItem: Loader { id: tooltipLoader @@ -102,23 +173,21 @@ PlasmoidItem { //We need Local to be *always* present, even if not disaplayed as //it's used for formatting in ToolTip.dateTimeChanged() - property var allTimezones + property list allTimeZones + Connections { target: Plasmoid.configuration - function onSelectedTimeZonesChanged() { root.initTimezones(); } + function onSelectedTimeZonesChanged() { + root.initTimeZones(); + } } - Binding { - target: root - property: "hideOnWindowDeactivate" - value: !Plasmoid.configuration.pin - restoreMode: Binding.RestoreBinding - } + hideOnWindowDeactivate: !Plasmoid.configuration.pin P5Support.DataSource { id: dataSource engine: "time" - connectedSources: allTimezones + connectedSources: allTimeZones interval: intervalAlignment === P5Support.Types.NoAlignment ? 1000 : 60000 intervalAlignment: { if (Plasmoid.configuration.showSeconds === 2 @@ -147,24 +216,12 @@ PlasmoidItem { id: clipboardAction text: i18n("Copy to Clipboard") icon.name: "edit-copy" - }, - PlasmaCore.Action { - text: i18n("Adjust Date and Time…") - icon.name: "clock" - visible: KAuthorized.authorize("kcm_clock") - onTriggered: KCMLauncher.openSystemSettings("kcm_clock") - }, - PlasmaCore.Action { - text: i18n("Set Time Format…") - icon.name: "gnumeric-format-thousand-separator" - visible: KAuthorized.authorizeControlModule("kcm_regionandlang") - onTriggered: KCMLauncher.openSystemSettings("kcm_regionandlang") } ] Component.onCompleted: { ClipboardMenu.setupMenu(clipboardAction); - root.initTimezones(); + initTimeZones(); } }