* SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*
*/
#include "dateandtime.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QTimeZone>
#include <qqml.h>
#include <KLocalizedString>
#include <KPluginFactory>
#include "datetime_logging.h"
#include "message.h"
#include "timedated_interface.h"
using namespace Qt::StringLiterals;
K_PLUGIN_CLASS_WITH_JSON(DateAndTime, "kcm_clock.json")
namespace
{
constexpr inline auto NtpUnavailableKey = "ntpUnavailable"_L1;
}
DateAndTime::DateAndTime(QObject *parent, const KPluginMetaData &data)
: KQuickConfigModule(parent, data)
{
const auto uri = "org.kde.plasma.private.kcm_clock";
qmlRegisterUncreatableType<Message>(uri, 1, 0, "message", QString());
connect(&m_refreshTimer, &QTimer::timeout, this, [this]() {
refresh();
});
setButtons(Help | Apply);
}
QList<Message> DateAndTime::messages() const
{
return m_messages.values();
}
void DateAndTime::addMessage(const Message &message, bool isTemporary, const QString &id)
{
if (!isTemporary) {
m_messages[id] = message;
Q_EMIT messagesChanged();
return;
}
const auto tempId = QString::number(m_nextKey);
++m_nextKey;
m_messages[tempId] = message;
Q_EMIT messagesChanged();
QTimer::singleShot(5000, this, [this, tempId]() {
m_messages.remove(tempId);
Q_EMIT messagesChanged();
});
}
QString DateAndTime::timeZone() const
{
return timeZoneToUse().id();
}
QTimeZone DateAndTime::timeZoneToUse() const
{
return m_timeZone.isValid() ? m_timeZone : m_systemTimeZone;
}
void DateAndTime::setTimeZone(QString timeZone)
{
auto oldTimeZone = timeZoneToUse();
if (oldTimeZone.id() == timeZone) {
return;
}
if (timeZone == m_systemTimeZone.id()) {
m_timeZone = {};
} else {
m_timeZone = QTimeZone(timeZone.toLatin1());
}
checkNeedsSave();
Q_EMIT timeZoneChanged();
Q_EMIT dateTimeChanged();
}
QString DateAndTime::timeZoneString() const
{
return dateTime().toTimeZone(timeZoneToUse()).toString("tttt (t) UTC ttt "_L1);
}
bool DateAndTime::ntpAvailable() const
{
return m_ntpAvailable;
}
bool DateAndTime::ntpEnabled() const
{
return m_ntpEnabledSet ? m_ntpEnabled : m_systemNtpEnabled;
}
void DateAndTime::setNtpEnabled(bool ntpEnabled)
{
if (m_ntpEnabledSet && m_ntpEnabled == ntpEnabled) {
return;
}
m_ntpEnabledSet = true;
m_ntpEnabled = ntpEnabled;
if (m_ntpEnabled) {
m_dateTime = {};
}
checkNeedsSave();
Q_EMIT ntpChanged();
}
QDateTime DateAndTime::dateTime() const
{
return m_dateTime.isValid() ? m_dateTime : m_systemDateTime;
}
void DateAndTime::setDateTime(const QDateTime &dateTime)
{
if (m_ntpEnabled || m_dateTime == dateTime) {
return;
}
m_dateTime = dateTime;
checkNeedsSave();
Q_EMIT dateTimeChanged();
}
void DateAndTime::setDate(const QDate &date)
{
if (m_ntpEnabled || m_dateTime.date() == date) {
return;
}
m_dateTime.setDate(date);
checkNeedsSave();
Q_EMIT dateTimeChanged();
}
void DateAndTime::setTime(const QTime &time)
{
if (m_ntpEnabled || m_dateTime.time() == time) {
return;
}
if (!m_dateTime.isValid()) {
m_dateTime = m_systemDateTime;
}
m_dateTime.setTime(time);
checkNeedsSave();
Q_EMIT dateTimeChanged();
}
QString DateAndTime::dateTimeString() const
{
const auto dateString = dateTime().toTimeZone(timeZoneToUse()).toString(QLocale::system().dateFormat(QLocale::LongFormat));
auto longFormatNoTimeZone = QLocale::system().timeFormat(QLocale::LongFormat).remove(u"tttt"_s).trimmed();
const auto timeString = dateTime().toTimeZone(timeZoneToUse()).toString(longFormatNoTimeZone);
return u"%1 %2"_s.arg(dateString, timeString);
}
void DateAndTime::checkNeedsSave()
{
bool timeZoneChanged = m_systemTimeZone.isValid() && m_timeZone.isValid() && m_systemTimeZone != m_timeZone;
bool ntpChanged = m_systemNtpEnabledSet && m_ntpEnabledSet && m_systemNtpEnabled != m_ntpEnabled;
bool dateTimeChanged = m_systemDateTime.isValid() && m_dateTime.isValid() && m_systemDateTime != m_dateTime;
setNeedsSave(timeZoneChanged || ntpChanged || dateTimeChanged);
}
void DateAndTime::save()
{
if (timedateSave()) {
QDBusMessage msg = QDBusMessage::createSignal("/org/kde/kcmshell_clock"_L1, "org.kde.kcmshell_clock"_L1, "clockUpdated"_L1);
QDBusConnection::sessionBus().send(msg);
}
load();
}
void DateAndTime::load()
{
m_systemTimeZone = {};
m_timeZone = {};
m_systemNtpEnabled = false;
m_systemNtpEnabledSet = false;
m_ntpEnabled = false;
m_ntpEnabledSet = false;
m_systemDateTime = {};
m_dateTime = {};
setNeedsSave(false);
refresh();
m_refreshTimer.start(1000);
}
void DateAndTime::refresh()
{
if (!m_systemNtpEnabledSet || !m_ntpAvailable) {
OrgFreedesktopTimedate1Interface timeDatedIface("org.freedesktop.timedate1"_L1, "/org/freedesktop/timedate1"_L1, QDBusConnection::systemBus());
const auto oldNtpAvailable = std::exchange(m_ntpAvailable, timeDatedIface.canNTP());
if (m_ntpAvailable) {
m_messages.remove(NtpUnavailableKey);
} else {
addMessage(Message::error(i18n("Unable to change NTP settings")), false, NtpUnavailableKey);
}
const auto oldSystemNtpEnabled = std::exchange(m_systemNtpEnabled, timeDatedIface.nTP());
m_systemNtpEnabledSet = true;
if (oldNtpAvailable != m_ntpAvailable || oldSystemNtpEnabled != m_systemNtpEnabled) {
Q_EMIT ntpChanged();
}
}
if (!m_systemTimeZone.isValid()) {
const auto oldSystemTimeZone = std::exchange(m_systemTimeZone, QTimeZone::systemTimeZone());
if (oldSystemTimeZone != m_systemTimeZone) {
Q_EMIT timeZoneChanged();
}
}
const auto oldSystemDateTime = std::exchange(m_systemDateTime, QDateTime::currentDateTime());
if (oldSystemDateTime != m_systemDateTime) {
Q_EMIT dateTimeChanged();
Q_EMIT timeZoneChanged();
}
}
bool DateAndTime::timedateSave()
{
OrgFreedesktopTimedate1Interface timedateIface("org.freedesktop.timedate1"_L1, "/org/freedesktop/timedate1"_L1, QDBusConnection::systemBus());
if (m_ntpEnabledSet) {
auto reply = timedateIface.SetNTP(m_ntpEnabled, true);
reply.waitForFinished();
if (reply.isError()) {
if (reply.error().name() != QDBusError::errorString(QDBusError::AccessDenied)) {
addMessage(Message::error(i18n("Unable to change NTP settings")));
} else {
addMessage(Message::error(i18n("Permission to change NTP setting denied")));
}
qCWarning(KCM_CLOCK) << "Failed to enable NTP" << reply.error().name() << reply.error().message();
return false;
}
}
if (!m_ntpEnabled && m_dateTime.isValid()) {
auto reply = timedateIface.SetTime(m_dateTime.toMSecsSinceEpoch() * 1000, false, true);
reply.waitForFinished();
if (reply.isError()) {
if (reply.error().name() != QDBusError::errorString(QDBusError::AccessDenied)) {
addMessage(Message::error(i18n("Unable to set current time")));
} else {
addMessage(Message::error(i18n("Permission to change date and time setting denied")));
}
qCWarning(KCM_CLOCK) << "Failed to set current time" << reply.error().name() << reply.error().message();
return false;
}
}
if (m_timeZone.isValid()) {
auto reply = timedateIface.SetTimezone(m_timeZone.id(), true);
reply.waitForFinished();
if (reply.isError()) {
if (reply.error().name() != QDBusError::errorString(QDBusError::AccessDenied)) {
addMessage(Message::error(i18n("Unable to set time zone")));
} else {
addMessage(Message::error(i18n("Permission to change time zone denied")));
}
qCWarning(KCM_CLOCK) << "Failed to set time zone" << reply.error().name() << reply.error().message();
return false;
}
}
return true;
}
#include "dateandtime.moc"