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