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
222 const QString &saveJsonFile = qEnvironmentVariable("QT_QPA_DBUS_SIGNALS_SAVE");
223 if (!saveJsonFile.isEmpty())
224 saveJson(saveJsonFile);
225}
226
227std::optional<QDBusListener::ChangeSignal>
228 QDBusListener::findSignal(const QString &location, const QString &key) const
229{
230 const DBusKey dkey(location, key);
231 std::optional<QDBusListener::ChangeSignal> ret;
232 const auto it = m_signalMap.find(dkey);
233 if (it != m_signalMap.cend())
234 ret.emplace(it.value());
235
236 return ret;
237}
238
239void QDBusListener::onSettingChanged(const QString &location, const QString &key, const QDBusVariant &value)
240{
241 auto sig = findSignal(location, key);
242 if (!sig.has_value())
243 return;
244
245 const Setting setting = sig.value().setting;
246 QVariant settingValue = value.variant();
247
248 switch (setting) {
249 case Setting::ColorScheme:
250 settingValue.setValue(QDBusSettings::XdgSettings::convertColorScheme(settingValue));
251 break;
252 case Setting::Contrast:
253 using namespace QDBusSettings;
254 // To unify the value, it's necessary to convert the DBus value to Qt::ContrastPreference.
255 // Then the users of the value don't need to parse the raw value.
256 if (key == XdgSettings::ContrastKey)
257 settingValue.setValue(XdgSettings::convertContrastPreference(settingValue));
258 else if (key == GnomeSettings::ContrastKey)
259 settingValue.setValue(GnomeSettings::convertContrastPreference(settingValue));
260 else
261 Q_UNREACHABLE_IMPL();
262 break;
263 default:
264 break;
265 }
266
267 emit settingChanged(sig.value().provider, setting, settingValue);
268}
269QT_END_NAMESPACE
\inmodule QtDBus
\inmodule QtCore
Definition qfile.h:69
\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)