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.
This commit is contained in:
Marco Allegretti 2026-05-11 10:03:20 +02:00
parent 42d41351e2
commit f22514528b
5 changed files with 197 additions and 85 deletions

View file

@ -18,90 +18,129 @@ InitialStartModule {
readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) 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 { anchors {
fill: parent fill: parent
topMargin: Kirigami.Units.gridUnit topMargin: Kirigami.Units.gridUnit
bottomMargin: Kirigami.Units.largeSpacing
} }
width: root.width ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
spacing: Kirigami.Units.gridUnit contentWidth: -1
Label { ColumnLayout {
Layout.leftMargin: Kirigami.Units.gridUnit width: root.width
Layout.rightMargin: Kirigami.Units.gridUnit spacing: Kirigami.Units.gridUnit
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
wrapMode: Text.Wrap Label {
horizontalAlignment: Text.AlignHCenter Layout.leftMargin: Kirigami.Units.gridUnit
text: i18n("Select your time zone and preferred time format.") Layout.rightMargin: Kirigami.Units.gridUnit
} Layout.alignment: Qt.AlignTop
FormCard.FormCard {
maximumWidth: root.cardWidth
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Layout.fillWidth: true
FormCard.FormSwitchDelegate {
Layout.fillWidth: true Layout.fillWidth: true
text: i18n("24-Hour Format")
checked: Time.TimeUtil.is24HourTime wrapMode: Text.Wrap
onCheckedChanged: { horizontalAlignment: Text.AlignHCenter
if (checked !== Time.TimeUtil.is24HourTime) { text: i18n("Select your time zone and preferred time format.")
Time.TimeUtil.is24HourTime = checked; }
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 { Label {
maximumWidth: root.cardWidth visible: Time.TimeUtil.timeZoneStatus.length > 0
Layout.leftMargin: Kirigami.Units.gridUnit
Layout.fillHeight: true Layout.rightMargin: Kirigami.Units.gridUnit
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Layout.fillWidth: true
ListView {
id: listView
clip: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true horizontalAlignment: Text.AlignHCenter
model: Time.TimeUtil.timeZones wrapMode: Text.Wrap
currentIndex: -1 // ensure focus is not on the listview text: Time.TimeUtil.timeZoneStatus
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);
}
}
}
} }
} }
} }

View file

@ -8,6 +8,7 @@
#include <QTimeZone> #include <QTimeZone>
#include <KConfigGroup> #include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig> #include <KSharedConfig>
#define FORMAT24H "HH:mm:ss" #define FORMAT24H "HH:mm:ss"
@ -15,9 +16,14 @@
TimeUtil::TimeUtil(QObject *parent) TimeUtil::TimeUtil(QObject *parent)
: QObject{parent} : QObject{parent}
, m_currentTimeZone{QString::fromUtf8(QTimeZone::systemTimeZoneId())}
, m_timeZoneModel{new TimeZoneModel{this}} , m_timeZoneModel{new TimeZoneModel{this}}
, m_filterModel{new TimeZoneFilterProxy{this}} , m_filterModel{new TimeZoneFilterProxy{this}}
{ {
if (m_currentTimeZone.isEmpty() || !QTimeZone(m_currentTimeZone.toUtf8()).isValid()) {
m_currentTimeZone = QStringLiteral("UTC");
}
m_filterModel->setSourceModel(m_timeZoneModel); m_filterModel->setSourceModel(m_timeZoneModel);
// retrieve is24HourTime // retrieve is24HourTime
@ -46,13 +52,36 @@ void TimeUtil::setIs24HourTime(bool is24HourTime)
QString TimeUtil::currentTimeZone() const QString TimeUtil::currentTimeZone() const
{ {
return QString{QTimeZone::systemTimeZoneId()}; return m_currentTimeZone;
} }
void TimeUtil::setCurrentTimeZone(QString timeZone) 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(); 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 TimeZoneFilterProxy *TimeUtil::timeZones() const

View file

@ -13,6 +13,7 @@ class TimeUtil : public QObject
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool is24HourTime READ is24HourTime WRITE setIs24HourTime NOTIFY is24HourTimeChanged); Q_PROPERTY(bool is24HourTime READ is24HourTime WRITE setIs24HourTime NOTIFY is24HourTimeChanged);
Q_PROPERTY(QString currentTimeZone READ currentTimeZone WRITE setCurrentTimeZone NOTIFY currentTimeZoneChanged); 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); Q_PROPERTY(TimeZoneFilterProxy *timeZones READ timeZones CONSTANT);
public: public:
@ -24,14 +25,19 @@ public:
QString currentTimeZone() const; QString currentTimeZone() const;
void setCurrentTimeZone(QString timeZone); void setCurrentTimeZone(QString timeZone);
QString timeZoneStatus() const;
TimeZoneFilterProxy *timeZones() const; TimeZoneFilterProxy *timeZones() const;
Q_SIGNALS: Q_SIGNALS:
void is24HourTimeChanged(); void is24HourTimeChanged();
void currentTimeZoneChanged(); void currentTimeZoneChanged();
void timeZoneStatusChanged();
private: private:
bool m_is24HourTime; bool m_is24HourTime = false;
QString m_currentTimeZone;
QString m_timeZoneStatus;
TimeZoneModel *m_timeZoneModel; TimeZoneModel *m_timeZoneModel;
TimeZoneFilterProxy *m_filterModel; TimeZoneFilterProxy *m_filterModel;

View file

@ -8,9 +8,13 @@
#include <KLocalizedString> #include <KLocalizedString>
#include <QDebug> #include <QDebug>
#include <QSet>
#include <QStringList>
#include <QStringMatcher> #include <QStringMatcher>
#include <QTimeZone> #include <QTimeZone>
#include <algorithm>
TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent) TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent)
: QSortFilterProxyModel(parent) : QSortFilterProxyModel(parent)
{ {
@ -22,8 +26,10 @@ bool TimeZoneFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &so
if (!sourceModel() || m_filterString.isEmpty()) { if (!sourceModel() || m_filterString.isEmpty()) {
return true; return true;
} }
const QString id = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::TimeZoneIdRole).toString(); const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
return m_stringMatcher.indexIn(id) != -1; 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) void TimeZoneFilterProxy::setFilterString(const QString &filterString)
@ -56,6 +62,8 @@ QVariant TimeZoneModel::data(const QModelIndex &index, int role) const
switch (role) { switch (role) {
case TimeZoneIdRole: case TimeZoneIdRole:
return m_data[index.row()]; return m_data[index.row()];
case DisplayNameRole:
return displayNameForId(m_data[index.row()]);
} }
} }
@ -67,16 +75,28 @@ void TimeZoneModel::update()
beginResetModel(); beginResetModel();
m_data.clear(); m_data.clear();
QTimeZone localZone = QTimeZone(QTimeZone::systemTimeZoneId()); QString localZone = QString::fromUtf8(QTimeZone::systemTimeZoneId());
m_data.append(localZone.id()); if (localZone.isEmpty() || !QTimeZone(localZone.toUtf8()).isValid()) {
localZone = QStringLiteral("UTC");
QStringList cities; }
QHash<QString, QTimeZone> zonesByCity; QSet<QString> seenZones;
const QList<QByteArray> systemTimeZones = QTimeZone::availableTimeZoneIds(); const QList<QByteArray> 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) { std::sort(m_data.begin(), m_data.end(), [](const QString &left, const QString &right) {
m_data.append(arr); return QString::localeAwareCompare(left, right) < 0;
});
const int localIndex = m_data.indexOf(localZone);
if (localIndex > 0) {
m_data.move(localIndex, 0);
} }
endResetModel(); endResetModel();
@ -84,5 +104,18 @@ void TimeZoneModel::update()
QHash<int, QByteArray> TimeZoneModel::roleNames() const QHash<int, QByteArray> 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());
} }

View file

@ -36,7 +36,10 @@ public:
explicit TimeZoneModel(QObject *parent = nullptr); explicit TimeZoneModel(QObject *parent = nullptr);
~TimeZoneModel() override; ~TimeZoneModel() override;
enum Roles { TimeZoneIdRole = Qt::UserRole + 1 }; enum Roles {
TimeZoneIdRole = Qt::UserRole + 1,
DisplayNameRole
};
int rowCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
@ -47,5 +50,7 @@ protected:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
private: private:
QString displayNameForId(const QString &timeZoneId) const;
QList<QString> m_data; QList<QString> m_data;
}; };