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