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