1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qdbuslistener_p.h"
5#include "qdbussettings_p.h"
6#include <private/qguiapplication_p.h>
7#include <qpa/qplatformintegration.h>
8#include <qpa/qplatformservices.h>
9#include <private/qdbustrayicon_p.h>
10#include <qjsonarray.h>
11#include <qjsondocument.h>
12#include <qjsonobject.h>
13
14QT_BEGIN_NAMESPACE
15using namespace Qt::StringLiterals;
16Q_STATIC_LOGGING_CATEGORY(lcQpaThemeDBus, "qt.qpa.theme.dbus")
17
18/*!
19 \internal
20 The QDBusListener class listens to the SettingChanged DBus signal
21 and translates it into combinations of the enums \c Provider and \c Setting.
22 Upon construction, it logs success/failure of the DBus connection.
23
24 The signal settingChanged delivers the normalized setting type and the new value as a string.
25 It is emitted on known setting types only.
26 */
27QDBusListener::QDBusListener(const QString &service,
28 const QString &path, const QString &interface, const QString &signal)
29{
30 init (service, path, interface, signal);
31}
32
33QDBusListener::QDBusListener()
34{
35 const auto service = u""_s;
36 const auto path = u"/org/freedesktop/portal/desktop"_s;
37 const auto interface = u"org.freedesktop.portal.Settings"_s;
38 const auto signal = u"SettingChanged"_s;
39
40 init (service, path, interface, signal);
41}
42
43namespace {
44namespace JsonKeys {
45constexpr auto dbusLocation() { return "DBusLocation"_L1; }
46constexpr auto dbusKey() { return "DBusKey"_L1; }
47constexpr auto provider() { return "Provider"_L1; }
48constexpr auto setting() { return "Setting"_L1; }
49constexpr auto dbusSignals() { return "DbusSignals"_L1; }
50constexpr auto root() { return "Q_L1.qpa.DBusSignals"_L1; }
51} // namespace JsonKeys
52} // namespace
53
54void QDBusListener::init(const QString &service, const QString &path,
55 const QString &interface, const QString &signal)
56{
57 QDBusConnection dbus = QDBusConnection::sessionBus();
58 const bool dBusRunning = dbus.isConnected();
59 bool dBusSignalConnected = false;
60#define LOG service << path << interface << signal;
61
62 if (dBusRunning) {
63 populateSignalMap();
64 qRegisterMetaType<QDBusVariant>();
65 dBusSignalConnected = dbus.connect(service, path, interface, name: signal, receiver: this,
66 SLOT(onSettingChanged(QString,QString,QDBusVariant)));
67 }
68
69 if (dBusSignalConnected) {
70 // Connection successful
71 qCDebug(lcQpaThemeDBus) << LOG;
72 } else {
73 if (dBusRunning) {
74 // DBus running, but connection failed
75 qCWarning(lcQpaThemeDBus) << "DBus connection failed:" << LOG;
76 } else {
77 // DBus not running
78 qCWarning(lcQpaThemeDBus) << "Session DBus not running.";
79 }
80 qCWarning(lcQpaThemeDBus) << "Application will not react to setting changes.\n"
81 << "Check your DBus installation.";
82 }
83#undef LOG
84}
85
86void QDBusListener::loadJson(const QString &fileName)
87{
88 Q_ASSERT(!fileName.isEmpty());
89#define CHECK(cond, warning)\
90 if (!cond) {\
91 qCWarning(lcQpaThemeDBus) << fileName << warning << "Falling back to default.";\
92 return;\
93 }
94
95#define PARSE(var, enumeration, string)\
96 enumeration var;\
97 {\
98 bool success;\
99 const int val = QMetaEnum::fromType<enumeration>().keyToValue(string.toLatin1(), &success);\
100 CHECK(success, "Parse Error: Invalid value" << string << "for" << #var);\
101 var = static_cast<enumeration>(val);\
102 }
103
104 QFile file(fileName);
105 CHECK(file.exists(), fileName << "doesn't exist.");
106 CHECK(file.open(QIODevice::ReadOnly), "could not be opened for reading.");
107
108 QJsonParseError error;
109 QJsonDocument doc = QJsonDocument::fromJson(json: file.readAll(), error: &error);
110 CHECK((error.error == QJsonParseError::NoError), error.errorString());
111 CHECK(doc.isObject(), "Parse Error: Expected root object" << JsonKeys::root());
112
113 const QJsonObject &root = doc.object();
114 CHECK(root.contains(JsonKeys::root()), "Parse Error: Expectned root object" << JsonKeys::root());
115 CHECK(root[JsonKeys::root()][JsonKeys::dbusSignals()].isArray(), "Parse Error: Expected array" << JsonKeys::dbusSignals());
116
117 const QJsonArray &sigs = root[JsonKeys::root()][JsonKeys::dbusSignals()].toArray();
118 CHECK((sigs.count() > 0), "Parse Error: Found empty array" << JsonKeys::dbusSignals());
119
120 for (auto sig = sigs.constBegin(); sig != sigs.constEnd(); ++sig) {
121 CHECK(sig->isObject(), "Parse Error: Expected object array" << JsonKeys::dbusSignals());
122 const QJsonObject &obj = sig->toObject();
123 CHECK(obj.contains(JsonKeys::dbusLocation()), "Parse Error: Expected key" << JsonKeys::dbusLocation());
124 CHECK(obj.contains(JsonKeys::dbusKey()), "Parse Error: Expected key" << JsonKeys::dbusKey());
125 CHECK(obj.contains(JsonKeys::provider()), "Parse Error: Expected key" << JsonKeys::provider());
126 CHECK(obj.contains(JsonKeys::setting()), "Parse Error: Expected key" << JsonKeys::setting());
127 const QString &location = obj[JsonKeys::dbusLocation()].toString();
128 const QString &key = obj[JsonKeys::dbusKey()].toString();
129 const QString &providerString = obj[JsonKeys::provider()].toString();
130 const QString &settingString = obj[JsonKeys::setting()].toString();
131 PARSE(provider, Provider, providerString);
132 PARSE(setting, Setting, settingString);
133 const DBusKey dkey(location, key);
134 CHECK (!m_signalMap.contains(dkey), "Duplicate key" << location << key);
135 m_signalMap.insert(key: dkey, value: ChangeSignal(provider, setting));
136 }
137#undef PARSE
138#undef CHECK
139
140 if (m_signalMap.count() > 0)
141 qCInfo(lcQpaThemeDBus) << "Successfully imported" << fileName;
142 else
143 qCWarning(lcQpaThemeDBus) << "No data imported from" << fileName << "falling back to default.";
144
145#ifdef QT_DEBUG
146 const int count = m_signalMap.count();
147 if (count == 0)
148 return;
149
150 qCDebug(lcQpaThemeDBus) << "Listening to" << count << "signals:";
151 for (auto it = m_signalMap.constBegin(); it != m_signalMap.constEnd(); ++it) {
152 qDebug() << it.key().key << it.key().location << "mapped to"
153 << it.value().provider << it.value().setting;
154 }
155
156#endif
157}
158
159void QDBusListener::saveJson(const QString &fileName) const
160{
161 Q_ASSERT(!m_signalMap.isEmpty());
162 Q_ASSERT(!fileName.isEmpty());
163 QFile file(fileName);
164 if (!file.open(flags: QIODevice::WriteOnly)) {
165 qCWarning(lcQpaThemeDBus) << fileName << "could not be opened for writing.";
166 return;
167 }
168
169 QJsonArray sigs;
170 for (auto sig = m_signalMap.constBegin(); sig != m_signalMap.constEnd(); ++sig) {
171 const DBusKey &dkey = sig.key();
172 const ChangeSignal &csig = sig.value();
173 QJsonObject obj;
174 obj[JsonKeys::dbusLocation()] = dkey.location;
175 obj[JsonKeys::dbusKey()] = dkey.key;
176 obj[JsonKeys::provider()] = QLatin1StringView(QMetaEnum::fromType<Provider>()
177 .valueToKey(value: static_cast<int>(csig.provider)));
178 obj[JsonKeys::setting()] = QLatin1StringView(QMetaEnum::fromType<Setting>()
179 .valueToKey(value: static_cast<int>(csig.setting)));
180 sigs.append(value: obj);
181 }
182 QJsonObject obj;
183 obj[JsonKeys::dbusSignals()] = sigs;
184 QJsonObject root;
185 root[JsonKeys::root()] = obj;
186 QJsonDocument doc(root);
187 file.write(data: doc.toJson());
188 file.close();
189}
190
191void QDBusListener::populateSignalMap()
192{
193 m_signalMap.clear();
194 const QString &loadJsonFile = qEnvironmentVariable(varName: "QT_QPA_DBUS_SIGNALS");
195 if (!loadJsonFile.isEmpty())
196 loadJson(fileName: loadJsonFile);
197 if (!m_signalMap.isEmpty())
198 return;
199
200 m_signalMap.insert(key: DBusKey("org.kde.kdeglobals.KDE"_L1, "widgetStyle"_L1),
201 value: ChangeSignal(Provider::Kde, Setting::ApplicationStyle));
202
203 m_signalMap.insert(key: DBusKey("org.kde.kdeglobals.General"_L1, "ColorScheme"_L1),
204 value: ChangeSignal(Provider::Kde, Setting::Theme));
205
206 m_signalMap.insert(key: DBusKey("org.gnome.desktop.interface"_L1, "gtk-theme"_L1),
207 value: ChangeSignal(Provider::Gtk, Setting::Theme));
208
209 using namespace QDBusSettings;
210 m_signalMap.insert(key: DBusKey(XdgSettings::AppearanceNamespace, XdgSettings::ColorSchemeKey),
211 value: ChangeSignal(Provider::Gnome, Setting::ColorScheme));
212
213 m_signalMap.insert(key: DBusKey(XdgSettings::AppearanceNamespace, XdgSettings::ContrastKey),
214 value: ChangeSignal(Provider::Gnome, Setting::Contrast));
215 // Alternative solution if XDG desktop portal setting is not accessible,
216 // e.g. when using the XDG portal version 1.
217 m_signalMap.insert(key: DBusKey(GnomeSettings::AllyNamespace, GnomeSettings::ContrastKey),
218 value: ChangeSignal(Provider::Gnome, Setting::Contrast));
219
220 const QString &saveJsonFile = qEnvironmentVariable(varName: "QT_QPA_DBUS_SIGNALS_SAVE");
221 if (!saveJsonFile.isEmpty())
222 saveJson(fileName: saveJsonFile);
223}
224
225std::optional<QDBusListener::ChangeSignal>
226 QDBusListener::findSignal(const QString &location, const QString &key) const
227{
228 const DBusKey dkey(location, key);
229 std::optional<QDBusListener::ChangeSignal> ret;
230 const auto it = m_signalMap.find(key: dkey);
231 if (it != m_signalMap.cend())
232 ret.emplace(args: it.value());
233
234 return ret;
235}
236
237void QDBusListener::onSettingChanged(const QString &location, const QString &key, const QDBusVariant &value)
238{
239 auto sig = findSignal(location, key);
240 if (!sig.has_value())
241 return;
242
243 const Setting setting = sig.value().setting;
244 QVariant settingValue = value.variant();
245
246 switch (setting) {
247 case Setting::ColorScheme:
248 settingValue.setValue(QDBusSettings::XdgSettings::convertColorScheme(value: settingValue));
249 break;
250 case Setting::Contrast:
251 using namespace QDBusSettings;
252 // To unify the value, it's necessary to convert the DBus value to Qt::ContrastPreference.
253 // Then the users of the value don't need to parse the raw value.
254 if (key == XdgSettings::ContrastKey)
255 settingValue.setValue(XdgSettings::convertContrastPreference(value: settingValue));
256 else if (key == GnomeSettings::ContrastKey)
257 settingValue.setValue(GnomeSettings::convertContrastPreference(value: settingValue));
258 else
259 Q_UNREACHABLE_IMPL();
260 break;
261 default:
262 break;
263 }
264
265 emit settingChanged(provider: sig.value().provider, setting, value: settingValue);
266}
267QT_END_NAMESPACE
268

source code of qtbase/src/gui/platform/unix/qdbuslistener.cpp