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)
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
}
}
}

View file

@ -8,6 +8,7 @@
#include <QTimeZone>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#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

View file

@ -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;

View file

@ -8,9 +8,13 @@
#include <KLocalizedString>
#include <QDebug>
#include <QSet>
#include <QStringList>
#include <QStringMatcher>
#include <QTimeZone>
#include <algorithm>
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<QString, QTimeZone> zonesByCity;
QString localZone = QString::fromUtf8(QTimeZone::systemTimeZoneId());
if (localZone.isEmpty() || !QTimeZone(localZone.toUtf8()).isValid()) {
localZone = QStringLiteral("UTC");
}
QSet<QString> seenZones;
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) {
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<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);
~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<int, QByteArray> roleNames() const override;
private:
QString displayNameForId(const QString &timeZoneId) const;
QList<QString> m_data;
};