From f22514528bf21851b3cfffae20cb95610000bfdd Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 11 May 2026 10:03:20 +0200 Subject: [PATCH] Show timezone selection feedback Keep the selected timezone visible in the wizard and update it before applying the system timezone. Filter by city or zone name and report timedatectl failures without discarding the staged choice. --- .../modules/time/package/contents/ui/main.qml | 181 +++++++++++------- initialstart/modules/time/timeutil.cpp | 33 +++- initialstart/modules/time/timeutil.h | 8 +- initialstart/modules/time/timezonemodel.cpp | 53 ++++- initialstart/modules/time/timezonemodel.h | 7 +- 5 files changed, 197 insertions(+), 85 deletions(-) diff --git a/initialstart/modules/time/package/contents/ui/main.qml b/initialstart/modules/time/package/contents/ui/main.qml index 00233c1d..22b8b3cb 100644 --- a/initialstart/modules/time/package/contents/ui/main.qml +++ b/initialstart/modules/time/package/contents/ui/main.qml @@ -18,90 +18,129 @@ InitialStartModule { readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) - ColumnLayout { + function selectedTimeZoneName() { + const timeZone = Time.TimeUtil.currentTimeZone.replace(/_/g, " "); + const parts = timeZone.split("/").filter(part => part.length > 0); + if (parts.length <= 1) { + return timeZone; + } + return i18nc("timezone city and region", "%1 (%2)", parts.slice(1).join(" / "), parts[0]); + } + + ScrollView { anchors { fill: parent topMargin: Kirigami.Units.gridUnit - bottomMargin: Kirigami.Units.largeSpacing } - width: root.width - spacing: Kirigami.Units.gridUnit + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: -1 - Label { - Layout.leftMargin: Kirigami.Units.gridUnit - Layout.rightMargin: Kirigami.Units.gridUnit - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true + ColumnLayout { + width: root.width + spacing: Kirigami.Units.gridUnit - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - text: i18n("Select your time zone and preferred time format.") - } - - FormCard.FormCard { - maximumWidth: root.cardWidth - - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - Layout.fillWidth: true - - FormCard.FormSwitchDelegate { + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop Layout.fillWidth: true - text: i18n("24-Hour Format") - checked: Time.TimeUtil.is24HourTime - onCheckedChanged: { - if (checked !== Time.TimeUtil.is24HourTime) { - Time.TimeUtil.is24HourTime = checked; + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + text: i18n("Select your time zone and preferred time format.") + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + FormCard.FormSwitchDelegate { + Layout.fillWidth: true + text: i18n("24-Hour Format") + checked: Time.TimeUtil.is24HourTime + onCheckedChanged: { + if (checked !== Time.TimeUtil.is24HourTime) { + Time.TimeUtil.is24HourTime = checked; + } + } + } + + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + + FormCard.FormTextDelegate { + text: i18n("Selected time zone") + description: root.selectedTimeZoneName() + } + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.fillWidth: true + Layout.preferredHeight: Math.max(Kirigami.Units.gridUnit * 14, root.height - Kirigami.Units.gridUnit * 18) + + ListView { + id: listView + + clip: true + anchors.fill: parent + model: Time.TimeUtil.timeZones + currentIndex: -1 // ensure focus is not on the listview + + header: Control { + width: listView.width + leftPadding: Kirigami.Units.largeSpacing + rightPadding: Kirigami.Units.largeSpacing + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + + contentItem: Kirigami.SearchField { + id: searchField + + placeholderText: i18n("Search city or region") + + onTextChanged: { + Time.TimeUtil.timeZones.filterString = text; + } + } + } + + delegate: FormCard.FormRadioDelegate { + required property string timeZoneId + required property string displayName + + width: ListView.view.width + text: displayName + description: timeZoneId + onClicked: { + Time.TimeUtil.currentTimeZone = timeZoneId; + } + + Binding on checked { + value: Time.TimeUtil.currentTimeZone === timeZoneId + } } } } - } - FormCard.FormCard { - maximumWidth: root.cardWidth - - Layout.fillHeight: true - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - Layout.fillWidth: true - - ListView { - id: listView - - clip: true + Label { + visible: Time.TimeUtil.timeZoneStatus.length > 0 + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit Layout.fillWidth: true - Layout.fillHeight: true - model: Time.TimeUtil.timeZones - currentIndex: -1 // ensure focus is not on the listview - - header: Control { - width: listView.width - leftPadding: Kirigami.Units.largeSpacing - rightPadding: Kirigami.Units.largeSpacing - topPadding: Kirigami.Units.largeSpacing - bottomPadding: Kirigami.Units.largeSpacing - - contentItem: Kirigami.SearchField { - id: searchField - - onTextChanged: { - Time.TimeUtil.timeZones.filterString = text; - } - } - } - - delegate: FormCard.FormRadioDelegate { - required property string timeZoneId - - width: ListView.view.width - text: timeZoneId - checked: Time.TimeUtil.currentTimeZone === timeZoneId - onCheckedChanged: { - if (checked && timeZoneId !== Time.TimeUtil.currentTimeZone) { - Time.TimeUtil.currentTimeZone = timeZoneId; - checked = Qt.binding(() => Time.TimeUtil.currentTimeZone === timeZoneId); - } - } - } + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + text: Time.TimeUtil.timeZoneStatus } } } diff --git a/initialstart/modules/time/timeutil.cpp b/initialstart/modules/time/timeutil.cpp index 47595f8c..89223dc9 100644 --- a/initialstart/modules/time/timeutil.cpp +++ b/initialstart/modules/time/timeutil.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #define FORMAT24H "HH:mm:ss" @@ -15,9 +16,14 @@ TimeUtil::TimeUtil(QObject *parent) : QObject{parent} + , m_currentTimeZone{QString::fromUtf8(QTimeZone::systemTimeZoneId())} , m_timeZoneModel{new TimeZoneModel{this}} , m_filterModel{new TimeZoneFilterProxy{this}} { + if (m_currentTimeZone.isEmpty() || !QTimeZone(m_currentTimeZone.toUtf8()).isValid()) { + m_currentTimeZone = QStringLiteral("UTC"); + } + m_filterModel->setSourceModel(m_timeZoneModel); // retrieve is24HourTime @@ -46,13 +52,36 @@ void TimeUtil::setIs24HourTime(bool is24HourTime) QString TimeUtil::currentTimeZone() const { - return QString{QTimeZone::systemTimeZoneId()}; + return m_currentTimeZone; } void TimeUtil::setCurrentTimeZone(QString timeZone) { - QProcess::execute("timedatectl", {"set-timezone", timeZone}); + if (timeZone == m_currentTimeZone) { + return; + } + + const QTimeZone zone(timeZone.toUtf8()); + if (!zone.isValid()) { + m_timeZoneStatus = i18n("This time zone is not available."); + Q_EMIT timeZoneStatusChanged(); + return; + } + + m_currentTimeZone = timeZone; Q_EMIT currentTimeZoneChanged(); + + const int exitCode = QProcess::execute(QStringLiteral("timedatectl"), {QStringLiteral("set-timezone"), timeZone}); + const QString status = exitCode == 0 ? QString() : i18n("Selected here, but the system time zone could not be changed."); + if (status != m_timeZoneStatus) { + m_timeZoneStatus = status; + Q_EMIT timeZoneStatusChanged(); + } +} + +QString TimeUtil::timeZoneStatus() const +{ + return m_timeZoneStatus; } TimeZoneFilterProxy *TimeUtil::timeZones() const diff --git a/initialstart/modules/time/timeutil.h b/initialstart/modules/time/timeutil.h index dbd06a31..80529d59 100644 --- a/initialstart/modules/time/timeutil.h +++ b/initialstart/modules/time/timeutil.h @@ -13,6 +13,7 @@ class TimeUtil : public QObject Q_OBJECT Q_PROPERTY(bool is24HourTime READ is24HourTime WRITE setIs24HourTime NOTIFY is24HourTimeChanged); Q_PROPERTY(QString currentTimeZone READ currentTimeZone WRITE setCurrentTimeZone NOTIFY currentTimeZoneChanged); + Q_PROPERTY(QString timeZoneStatus READ timeZoneStatus NOTIFY timeZoneStatusChanged); Q_PROPERTY(TimeZoneFilterProxy *timeZones READ timeZones CONSTANT); public: @@ -24,14 +25,19 @@ public: QString currentTimeZone() const; void setCurrentTimeZone(QString timeZone); + QString timeZoneStatus() const; + TimeZoneFilterProxy *timeZones() const; Q_SIGNALS: void is24HourTimeChanged(); void currentTimeZoneChanged(); + void timeZoneStatusChanged(); private: - bool m_is24HourTime; + bool m_is24HourTime = false; + QString m_currentTimeZone; + QString m_timeZoneStatus; TimeZoneModel *m_timeZoneModel; TimeZoneFilterProxy *m_filterModel; diff --git a/initialstart/modules/time/timezonemodel.cpp b/initialstart/modules/time/timezonemodel.cpp index 417fe072..3ea17b8c 100644 --- a/initialstart/modules/time/timezonemodel.cpp +++ b/initialstart/modules/time/timezonemodel.cpp @@ -8,9 +8,13 @@ #include #include +#include +#include #include #include +#include + TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent) : QSortFilterProxyModel(parent) { @@ -22,8 +26,10 @@ bool TimeZoneFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &so if (!sourceModel() || m_filterString.isEmpty()) { return true; } - const QString id = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::TimeZoneIdRole).toString(); - return m_stringMatcher.indexIn(id) != -1; + const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + const QString id = index.data(TimeZoneModel::TimeZoneIdRole).toString(); + const QString displayName = index.data(TimeZoneModel::DisplayNameRole).toString(); + return m_stringMatcher.indexIn(id) != -1 || m_stringMatcher.indexIn(displayName) != -1; } void TimeZoneFilterProxy::setFilterString(const QString &filterString) @@ -56,6 +62,8 @@ QVariant TimeZoneModel::data(const QModelIndex &index, int role) const switch (role) { case TimeZoneIdRole: return m_data[index.row()]; + case DisplayNameRole: + return displayNameForId(m_data[index.row()]); } } @@ -67,16 +75,28 @@ void TimeZoneModel::update() beginResetModel(); m_data.clear(); - QTimeZone localZone = QTimeZone(QTimeZone::systemTimeZoneId()); - m_data.append(localZone.id()); - - QStringList cities; - QHash zonesByCity; + QString localZone = QString::fromUtf8(QTimeZone::systemTimeZoneId()); + if (localZone.isEmpty() || !QTimeZone(localZone.toUtf8()).isValid()) { + localZone = QStringLiteral("UTC"); + } + QSet seenZones; const QList systemTimeZones = QTimeZone::availableTimeZoneIds(); + for (const QByteArray &timeZoneId : systemTimeZones) { + const QString zone = QString::fromUtf8(timeZoneId); + if (!seenZones.contains(zone)) { + seenZones.insert(zone); + m_data.append(zone); + } + } - for (QByteArray arr : systemTimeZones) { - m_data.append(arr); + std::sort(m_data.begin(), m_data.end(), [](const QString &left, const QString &right) { + return QString::localeAwareCompare(left, right) < 0; + }); + + const int localIndex = m_data.indexOf(localZone); + if (localIndex > 0) { + m_data.move(localIndex, 0); } endResetModel(); @@ -84,5 +104,18 @@ void TimeZoneModel::update() QHash TimeZoneModel::roleNames() const { - return {{TimeZoneIdRole, "timeZoneId"}}; + return {{TimeZoneIdRole, "timeZoneId"}, {DisplayNameRole, "displayName"}}; +} + +QString TimeZoneModel::displayNameForId(const QString &timeZoneId) const +{ + QString displayName = timeZoneId; + displayName.replace(QLatin1Char('_'), QLatin1Char(' ')); + + const QStringList parts = displayName.split(QLatin1Char('/'), Qt::SkipEmptyParts); + if (parts.size() <= 1) { + return displayName; + } + + return i18nc("timezone city and region", "%1 (%2)", parts.mid(1).join(QStringLiteral(" / ")), parts.first()); } diff --git a/initialstart/modules/time/timezonemodel.h b/initialstart/modules/time/timezonemodel.h index 1f20f411..f8ae7977 100644 --- a/initialstart/modules/time/timezonemodel.h +++ b/initialstart/modules/time/timezonemodel.h @@ -36,7 +36,10 @@ public: explicit TimeZoneModel(QObject *parent = nullptr); ~TimeZoneModel() override; - enum Roles { TimeZoneIdRole = Qt::UserRole + 1 }; + enum Roles { + TimeZoneIdRole = Qt::UserRole + 1, + DisplayNameRole + }; int rowCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; @@ -47,5 +50,7 @@ protected: QHash roleNames() const override; private: + QString displayNameForId(const QString &timeZoneId) const; + QList m_data; };