1 | // Copyright (C) 2022 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 "qgenericunixthemes_p.h" |
5 | |
6 | #include <QPalette> |
7 | #include <QFont> |
8 | #include <QGuiApplication> |
9 | #include <QDir> |
10 | #include <QFileInfo> |
11 | #include <QFile> |
12 | #include <QDebug> |
13 | #include <QHash> |
14 | #include <QLoggingCategory> |
15 | #include <QVariant> |
16 | #include <QStandardPaths> |
17 | #include <QStringList> |
18 | #if QT_CONFIG(mimetype) |
19 | #include <QMimeDatabase> |
20 | #endif |
21 | #if QT_CONFIG(settings) |
22 | #include <QSettings> |
23 | #endif |
24 | |
25 | #include <qpa/qplatformfontdatabase.h> // lcQpaFonts |
26 | #include <qpa/qplatformintegration.h> |
27 | #include <qpa/qplatformservices.h> |
28 | #include <qpa/qplatformdialoghelper.h> |
29 | #include <qpa/qplatformtheme_p.h> |
30 | |
31 | #include <private/qguiapplication_p.h> |
32 | #ifndef QT_NO_DBUS |
33 | #include <QDBusConnectionInterface> |
34 | #include <private/qdbusplatformmenu_p.h> |
35 | #include <private/qdbusmenubar_p.h> |
36 | #include <private/qflatmap_p.h> |
37 | #include <QJsonDocument> |
38 | #include <QJsonArray> |
39 | #include <QJsonObject> |
40 | #include <QJsonValue> |
41 | #include <QJsonParseError> |
42 | #endif |
43 | #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) |
44 | #include <private/qdbustrayicon_p.h> |
45 | #endif |
46 | |
47 | #include <algorithm> |
48 | |
49 | QT_BEGIN_NAMESPACE |
50 | #ifndef QT_NO_DBUS |
51 | Q_LOGGING_CATEGORY(lcQpaThemeDBus, "qt.qpa.theme.dbus" ) |
52 | #endif |
53 | |
54 | using namespace Qt::StringLiterals; |
55 | |
56 | Q_DECLARE_LOGGING_CATEGORY(qLcTray) |
57 | |
58 | ResourceHelper::ResourceHelper() |
59 | { |
60 | std::fill(palettes, palettes + QPlatformTheme::NPalettes, static_cast<QPalette *>(nullptr)); |
61 | std::fill(fonts, fonts + QPlatformTheme::NFonts, static_cast<QFont *>(nullptr)); |
62 | } |
63 | |
64 | void ResourceHelper::clear() |
65 | { |
66 | qDeleteAll(begin: palettes, end: palettes + QPlatformTheme::NPalettes); |
67 | qDeleteAll(begin: fonts, end: fonts + QPlatformTheme::NFonts); |
68 | std::fill(palettes, palettes + QPlatformTheme::NPalettes, static_cast<QPalette *>(nullptr)); |
69 | std::fill(fonts, fonts + QPlatformTheme::NFonts, static_cast<QFont *>(nullptr)); |
70 | } |
71 | |
72 | const char *QGenericUnixTheme::name = "generic" ; |
73 | |
74 | // Default system font, corresponding to the value returned by 4.8 for |
75 | // XRender/FontConfig which we can now assume as default. |
76 | static const char defaultSystemFontNameC[] = "Sans Serif" ; |
77 | static const char defaultFixedFontNameC[] = "monospace" ; |
78 | enum { defaultSystemFontSize = 9 }; |
79 | |
80 | #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) |
81 | static bool isDBusTrayAvailable() { |
82 | static bool dbusTrayAvailable = false; |
83 | static bool dbusTrayAvailableKnown = false; |
84 | if (!dbusTrayAvailableKnown) { |
85 | QDBusMenuConnection conn; |
86 | if (conn.isWatcherRegistered()) |
87 | dbusTrayAvailable = true; |
88 | dbusTrayAvailableKnown = true; |
89 | qCDebug(qLcTray) << "D-Bus tray available:" << dbusTrayAvailable; |
90 | } |
91 | return dbusTrayAvailable; |
92 | } |
93 | #endif |
94 | |
95 | static QString mouseCursorTheme() |
96 | { |
97 | static QString themeName = qEnvironmentVariable(varName: "XCURSOR_THEME" ); |
98 | return themeName; |
99 | } |
100 | |
101 | static QSize mouseCursorSize() |
102 | { |
103 | constexpr int defaultCursorSize = 24; |
104 | static const int xCursorSize = qEnvironmentVariableIntValue(varName: "XCURSOR_SIZE" ); |
105 | static const int s = xCursorSize > 0 ? xCursorSize : defaultCursorSize; |
106 | return QSize(s, s); |
107 | } |
108 | |
109 | #ifndef QT_NO_DBUS |
110 | static bool () |
111 | { |
112 | const QDBusConnection connection = QDBusConnection::sessionBus(); |
113 | static const QString registrarService = QStringLiteral("com.canonical.AppMenu.Registrar" ); |
114 | if (const auto iface = connection.interface()) |
115 | return iface->isServiceRegistered(serviceName: registrarService); |
116 | return false; |
117 | } |
118 | |
119 | static bool () |
120 | { |
121 | static bool = checkDBusGlobalMenuAvailable(); |
122 | return dbusGlobalMenuAvailable; |
123 | } |
124 | |
125 | /*! |
126 | * \internal |
127 | * The QGenericUnixThemeDBusListener class listens to the SettingChanged DBus signal |
128 | * and translates it into combinations of the enums \c Provider and \c Setting. |
129 | * Upon construction, it logs success/failure of the DBus connection. |
130 | * |
131 | * The signal settingChanged delivers the normalized setting type and the new value as a string. |
132 | * It is emitted on known setting types only. |
133 | */ |
134 | |
135 | class QGenericUnixThemeDBusListener : public QObject |
136 | { |
137 | Q_OBJECT |
138 | |
139 | public: |
140 | |
141 | enum class Provider { |
142 | Kde, |
143 | Gtk, |
144 | Gnome, |
145 | }; |
146 | Q_ENUM(Provider) |
147 | |
148 | enum class Setting { |
149 | Theme, |
150 | ApplicationStyle, |
151 | ColorTheme, |
152 | }; |
153 | Q_ENUM(Setting) |
154 | |
155 | QGenericUnixThemeDBusListener(); |
156 | QGenericUnixThemeDBusListener(const QString &service, const QString &path, |
157 | const QString &interface, const QString &signal); |
158 | |
159 | private Q_SLOTS: |
160 | void onSettingChanged(const QString &location, const QString &key, const QDBusVariant &value); |
161 | |
162 | Q_SIGNALS: |
163 | void settingChanged(QGenericUnixThemeDBusListener::Provider provider, |
164 | QGenericUnixThemeDBusListener::Setting setting, |
165 | const QString &value); |
166 | |
167 | private: |
168 | struct DBusKey |
169 | { |
170 | QString location; |
171 | QString key; |
172 | DBusKey(const QString &loc, const QString &k) : location(loc), key(k) {}; |
173 | bool operator<(const DBusKey &other) const |
174 | { |
175 | return location + key < other.location + other.key; |
176 | } |
177 | }; |
178 | |
179 | struct ChangeSignal |
180 | { |
181 | Provider provider; |
182 | Setting setting; |
183 | ChangeSignal(Provider p, Setting s) : provider(p), setting(s) {} |
184 | ChangeSignal() {} |
185 | }; |
186 | |
187 | // Json keys |
188 | static constexpr QLatin1StringView s_dbusLocation = QLatin1StringView("DBusLocation" ); |
189 | static constexpr QLatin1StringView s_dbusKey = QLatin1StringView("DBusKey" ); |
190 | static constexpr QLatin1StringView s_provider = QLatin1StringView("Provider" ); |
191 | static constexpr QLatin1StringView s_setting = QLatin1StringView("Setting" ); |
192 | static constexpr QLatin1StringView s_signals = QLatin1StringView("DbusSignals" ); |
193 | static constexpr QLatin1StringView s_root = QLatin1StringView("Qt.qpa.DBusSignals" ); |
194 | |
195 | QFlatMap <DBusKey, ChangeSignal> m_signalMap; |
196 | |
197 | void init(const QString &service, const QString &path, |
198 | const QString &interface, const QString &signal); |
199 | |
200 | std::optional<ChangeSignal> findSignal(const QString &location, const QString &key) const; |
201 | void populateSignalMap(); |
202 | void loadJson(const QString &fileName); |
203 | void saveJson(const QString &fileName) const; |
204 | }; |
205 | |
206 | QGenericUnixThemeDBusListener::QGenericUnixThemeDBusListener(const QString &service, |
207 | const QString &path, const QString &interface, const QString &signal) |
208 | { |
209 | init (service, path, interface, signal); |
210 | } |
211 | |
212 | QGenericUnixThemeDBusListener::QGenericUnixThemeDBusListener() |
213 | { |
214 | static constexpr QLatin1StringView service("" ); |
215 | static constexpr QLatin1StringView path("/org/freedesktop/portal/desktop" ); |
216 | static constexpr QLatin1StringView interface("org.freedesktop.portal.Settings" ); |
217 | static constexpr QLatin1StringView signal("SettingChanged" ); |
218 | |
219 | init (service, path, interface, signal); |
220 | } |
221 | |
222 | void QGenericUnixThemeDBusListener::init(const QString &service, const QString &path, |
223 | const QString &interface, const QString &signal) |
224 | { |
225 | QDBusConnection dbus = QDBusConnection::sessionBus(); |
226 | const bool dBusRunning = dbus.isConnected(); |
227 | bool dBusSignalConnected = false; |
228 | #define LOG service << path << interface << signal; |
229 | |
230 | if (dBusRunning) { |
231 | populateSignalMap(); |
232 | qRegisterMetaType<QDBusVariant>(); |
233 | dBusSignalConnected = dbus.connect(service, path, interface, name: signal, receiver: this, |
234 | SLOT(onSettingChanged(QString,QString,QDBusVariant))); |
235 | } |
236 | |
237 | if (dBusSignalConnected) { |
238 | // Connection successful |
239 | qCDebug(lcQpaThemeDBus) << LOG; |
240 | } else { |
241 | if (dBusRunning) { |
242 | // DBus running, but connection failed |
243 | qCWarning(lcQpaThemeDBus) << "DBus connection failed:" << LOG; |
244 | } else { |
245 | // DBus not running |
246 | qCWarning(lcQpaThemeDBus) << "Session DBus not running." ; |
247 | } |
248 | qCWarning(lcQpaThemeDBus) << "Application will not react to setting changes.\n" |
249 | << "Check your DBus installation." ; |
250 | } |
251 | #undef LOG |
252 | } |
253 | |
254 | void QGenericUnixThemeDBusListener::loadJson(const QString &fileName) |
255 | { |
256 | Q_ASSERT(!fileName.isEmpty()); |
257 | #define CHECK(cond, warning)\ |
258 | if (!cond) {\ |
259 | qCWarning(lcQpaThemeDBus) << fileName << warning << "Falling back to default.";\ |
260 | return;\ |
261 | } |
262 | |
263 | #define PARSE(var, enumeration, string)\ |
264 | enumeration var;\ |
265 | {\ |
266 | bool success;\ |
267 | const int val = QMetaEnum::fromType<enumeration>().keyToValue(string.toLatin1(), &success);\ |
268 | CHECK(success, "Parse Error: Invalid value" << string << "for" << #var);\ |
269 | var = static_cast<enumeration>(val);\ |
270 | } |
271 | |
272 | QFile file(fileName); |
273 | CHECK(file.exists(), fileName << "doesn't exist." ); |
274 | CHECK(file.open(QIODevice::ReadOnly), "could not be opened for reading." ); |
275 | |
276 | QJsonParseError error; |
277 | QJsonDocument doc = QJsonDocument::fromJson(json: file.readAll(), error: &error); |
278 | CHECK((error.error == QJsonParseError::NoError), error.errorString()); |
279 | CHECK(doc.isObject(), "Parse Error: Expected root object" << s_root); |
280 | |
281 | const QJsonObject &root = doc.object(); |
282 | CHECK(root.contains(s_root), "Parse Error: Expected root object" << s_root); |
283 | CHECK(root[s_root][s_signals].isArray(), "Parse Error: Expected array" << s_signals); |
284 | |
285 | const QJsonArray &sigs = root[s_root][s_signals].toArray(); |
286 | CHECK((sigs.count() > 0), "Parse Error: Found empty array" << s_signals); |
287 | |
288 | for (auto sig = sigs.constBegin(); sig != sigs.constEnd(); ++sig) { |
289 | CHECK(sig->isObject(), "Parse Error: Expected object array" << s_signals); |
290 | const QJsonObject &obj = sig->toObject(); |
291 | CHECK(obj.contains(s_dbusLocation), "Parse Error: Expected key" << s_dbusLocation); |
292 | CHECK(obj.contains(s_dbusKey), "Parse Error: Expected key" << s_dbusKey); |
293 | CHECK(obj.contains(s_provider), "Parse Error: Expected key" << s_provider); |
294 | CHECK(obj.contains(s_setting), "Parse Error: Expected key" << s_setting); |
295 | const QString &location = obj[s_dbusLocation].toString(); |
296 | const QString &key = obj[s_dbusKey].toString(); |
297 | const QString &providerString = obj[s_provider].toString(); |
298 | const QString &settingString = obj[s_setting].toString(); |
299 | PARSE(provider, Provider, providerString); |
300 | PARSE(setting, Setting, settingString); |
301 | const DBusKey dkey(location, key); |
302 | CHECK (!m_signalMap.contains(dkey), "Duplicate key" << location << key); |
303 | m_signalMap.insert(key: dkey, value: ChangeSignal(provider, setting)); |
304 | } |
305 | #undef PARSE |
306 | #undef CHECK |
307 | |
308 | if (m_signalMap.count() > 0) |
309 | qCInfo(lcQpaThemeDBus) << "Successfully imported" << fileName; |
310 | else |
311 | qCWarning(lcQpaThemeDBus) << "No data imported from" << fileName << "falling back to default." ; |
312 | |
313 | #ifdef QT_DEBUG |
314 | const int count = m_signalMap.count(); |
315 | if (count == 0) |
316 | return; |
317 | |
318 | qCDebug(lcQpaThemeDBus) << "Listening to" << count << "signals:" ; |
319 | for (auto it = m_signalMap.constBegin(); it != m_signalMap.constEnd(); ++it) { |
320 | qDebug() << it.key().key << it.key().location << "mapped to" |
321 | << it.value().provider << it.value().setting; |
322 | } |
323 | |
324 | #endif |
325 | } |
326 | |
327 | void QGenericUnixThemeDBusListener::saveJson(const QString &fileName) const |
328 | { |
329 | Q_ASSERT(!m_signalMap.isEmpty()); |
330 | Q_ASSERT(!fileName.isEmpty()); |
331 | QFile file(fileName); |
332 | if (!file.open(flags: QIODevice::WriteOnly)) { |
333 | qCWarning(lcQpaThemeDBus) << fileName << "could not be opened for writing." ; |
334 | return; |
335 | } |
336 | |
337 | QJsonArray sigs; |
338 | for (auto sig = m_signalMap.constBegin(); sig != m_signalMap.constEnd(); ++sig) { |
339 | const DBusKey &dkey = sig.key(); |
340 | const ChangeSignal &csig = sig.value(); |
341 | QJsonObject obj; |
342 | obj[s_dbusLocation] = dkey.location; |
343 | obj[s_dbusKey] = dkey.key; |
344 | obj[s_provider] = QLatin1StringView(QMetaEnum::fromType<Provider>() |
345 | .valueToKey(value: static_cast<int>(csig.provider))); |
346 | obj[s_setting] = QLatin1StringView(QMetaEnum::fromType<Setting>() |
347 | .valueToKey(value: static_cast<int>(csig.setting))); |
348 | sigs.append(value: obj); |
349 | } |
350 | QJsonObject obj; |
351 | obj[s_signals] = sigs; |
352 | QJsonObject root; |
353 | root[s_root] = obj; |
354 | QJsonDocument doc(root); |
355 | file.write(data: doc.toJson()); |
356 | file.close(); |
357 | } |
358 | |
359 | void QGenericUnixThemeDBusListener::populateSignalMap() |
360 | { |
361 | m_signalMap.clear(); |
362 | const QString &loadJsonFile = qEnvironmentVariable(varName: "QT_QPA_DBUS_SIGNALS" ); |
363 | if (!loadJsonFile.isEmpty()) |
364 | loadJson(fileName: loadJsonFile); |
365 | if (!m_signalMap.isEmpty()) |
366 | return; |
367 | |
368 | m_signalMap.insert(key: DBusKey("org.kde.kdeglobals.KDE"_L1 , "widgetStyle"_L1 ), |
369 | value: ChangeSignal(Provider::Kde, Setting::ApplicationStyle)); |
370 | |
371 | m_signalMap.insert(key: DBusKey("org.kde.kdeglobals.General"_L1 , "ColorScheme"_L1 ), |
372 | value: ChangeSignal(Provider::Kde, Setting::Theme)); |
373 | |
374 | m_signalMap.insert(key: DBusKey("org.gnome.desktop.interface"_L1 , "gtk-theme"_L1 ), |
375 | value: ChangeSignal(Provider::Gtk, Setting::Theme)); |
376 | |
377 | m_signalMap.insert(key: DBusKey("org.freedesktop.appearance"_L1 , "color-scheme"_L1 ), |
378 | value: ChangeSignal(Provider::Gnome, Setting::ColorTheme)); |
379 | |
380 | const QString &saveJsonFile = qEnvironmentVariable(varName: "QT_QPA_DBUS_SIGNALS_SAVE" ); |
381 | if (!saveJsonFile.isEmpty()) |
382 | saveJson(fileName: saveJsonFile); |
383 | } |
384 | |
385 | std::optional<QGenericUnixThemeDBusListener::ChangeSignal> |
386 | QGenericUnixThemeDBusListener::findSignal(const QString &location, const QString &key) const |
387 | { |
388 | const DBusKey dkey(location, key); |
389 | std::optional<QGenericUnixThemeDBusListener::ChangeSignal> ret; |
390 | if (m_signalMap.contains(key: dkey)) |
391 | ret.emplace(args: m_signalMap.value(key: dkey)); |
392 | |
393 | return ret; |
394 | } |
395 | |
396 | void QGenericUnixThemeDBusListener::onSettingChanged(const QString &location, const QString &key, const QDBusVariant &value) |
397 | { |
398 | auto sig = findSignal(location, key); |
399 | if (!sig.has_value()) |
400 | return; |
401 | |
402 | emit settingChanged(provider: sig.value().provider, setting: sig.value().setting, value: value.variant().toString()); |
403 | } |
404 | |
405 | #endif //QT_NO_DBUS |
406 | |
407 | class QGenericUnixThemePrivate : public QPlatformThemePrivate |
408 | { |
409 | public: |
410 | QGenericUnixThemePrivate() |
411 | : QPlatformThemePrivate() |
412 | , systemFont(QLatin1StringView(defaultSystemFontNameC), defaultSystemFontSize) |
413 | , fixedFont(QLatin1StringView(defaultFixedFontNameC), systemFont.pointSize()) |
414 | { |
415 | fixedFont.setStyleHint(QFont::TypeWriter); |
416 | qCDebug(lcQpaFonts) << "default fonts: system" << systemFont << "fixed" << fixedFont; |
417 | } |
418 | |
419 | const QFont systemFont; |
420 | QFont fixedFont; |
421 | }; |
422 | |
423 | QGenericUnixTheme::QGenericUnixTheme() |
424 | : QPlatformTheme(new QGenericUnixThemePrivate()) |
425 | { |
426 | } |
427 | |
428 | const QFont *QGenericUnixTheme::font(Font type) const |
429 | { |
430 | Q_D(const QGenericUnixTheme); |
431 | switch (type) { |
432 | case QPlatformTheme::SystemFont: |
433 | return &d->systemFont; |
434 | case QPlatformTheme::FixedFont: |
435 | return &d->fixedFont; |
436 | default: |
437 | return nullptr; |
438 | } |
439 | } |
440 | |
441 | // Helper to return the icon theme paths from XDG. |
442 | QStringList QGenericUnixTheme::xdgIconThemePaths() |
443 | { |
444 | QStringList paths; |
445 | // Add home directory first in search path |
446 | const QFileInfo homeIconDir(QDir::homePath() + "/.icons"_L1 ); |
447 | if (homeIconDir.isDir()) |
448 | paths.prepend(t: homeIconDir.absoluteFilePath()); |
449 | |
450 | paths.append(l: QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, |
451 | QStringLiteral("icons" ), |
452 | options: QStandardPaths::LocateDirectory)); |
453 | |
454 | return paths; |
455 | } |
456 | |
457 | QStringList QGenericUnixTheme::iconFallbackPaths() |
458 | { |
459 | QStringList paths; |
460 | const QFileInfo pixmapsIconsDir(QStringLiteral("/usr/share/pixmaps" )); |
461 | if (pixmapsIconsDir.isDir()) |
462 | paths.append(t: pixmapsIconsDir.absoluteFilePath()); |
463 | |
464 | return paths; |
465 | } |
466 | |
467 | #ifndef QT_NO_DBUS |
468 | QPlatformMenuBar *QGenericUnixTheme::() const |
469 | { |
470 | if (isDBusGlobalMenuAvailable()) |
471 | return new QDBusMenuBar(); |
472 | return nullptr; |
473 | } |
474 | #endif |
475 | |
476 | #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) |
477 | QPlatformSystemTrayIcon *QGenericUnixTheme::createPlatformSystemTrayIcon() const |
478 | { |
479 | if (isDBusTrayAvailable()) |
480 | return new QDBusTrayIcon(); |
481 | return nullptr; |
482 | } |
483 | #endif |
484 | |
485 | QVariant QGenericUnixTheme::themeHint(ThemeHint hint) const |
486 | { |
487 | switch (hint) { |
488 | case QPlatformTheme::SystemIconFallbackThemeName: |
489 | return QVariant(QString(QStringLiteral("hicolor" ))); |
490 | case QPlatformTheme::IconThemeSearchPaths: |
491 | return xdgIconThemePaths(); |
492 | case QPlatformTheme::IconFallbackSearchPaths: |
493 | return iconFallbackPaths(); |
494 | case QPlatformTheme::DialogButtonBoxButtonsHaveIcons: |
495 | return QVariant(true); |
496 | case QPlatformTheme::StyleNames: { |
497 | QStringList styleNames; |
498 | styleNames << QStringLiteral("Fusion" ) << QStringLiteral("Windows" ); |
499 | return QVariant(styleNames); |
500 | } |
501 | case QPlatformTheme::KeyboardScheme: |
502 | return QVariant(int(X11KeyboardScheme)); |
503 | case QPlatformTheme::UiEffects: |
504 | return QVariant(int(HoverEffect)); |
505 | case QPlatformTheme::MouseCursorTheme: |
506 | return QVariant(mouseCursorTheme()); |
507 | case QPlatformTheme::MouseCursorSize: |
508 | return QVariant(mouseCursorSize()); |
509 | default: |
510 | break; |
511 | } |
512 | return QPlatformTheme::themeHint(hint); |
513 | } |
514 | |
515 | // Helper functions for implementing QPlatformTheme::fileIcon() for XDG icon themes. |
516 | static QList<QSize> availableXdgFileIconSizes() |
517 | { |
518 | return QIcon::fromTheme(QStringLiteral("inode-directory" )).availableSizes(); |
519 | } |
520 | |
521 | #if QT_CONFIG(mimetype) |
522 | static QIcon xdgFileIcon(const QFileInfo &fileInfo) |
523 | { |
524 | QMimeDatabase mimeDatabase; |
525 | QMimeType mimeType = mimeDatabase.mimeTypeForFile(fileInfo); |
526 | if (!mimeType.isValid()) |
527 | return QIcon(); |
528 | const QString &iconName = mimeType.iconName(); |
529 | if (!iconName.isEmpty()) { |
530 | const QIcon icon = QIcon::fromTheme(name: iconName); |
531 | if (!icon.isNull()) |
532 | return icon; |
533 | } |
534 | const QString &genericIconName = mimeType.genericIconName(); |
535 | return genericIconName.isEmpty() ? QIcon() : QIcon::fromTheme(name: genericIconName); |
536 | } |
537 | #endif |
538 | |
539 | #if QT_CONFIG(settings) |
540 | class QKdeThemePrivate : public QPlatformThemePrivate |
541 | { |
542 | |
543 | public: |
544 | QKdeThemePrivate(const QStringList &kdeDirs, int kdeVersion); |
545 | |
546 | static QString kdeGlobals(const QString &kdeDir, int kdeVersion) |
547 | { |
548 | if (kdeVersion > 4) |
549 | return kdeDir + "/kdeglobals"_L1 ; |
550 | return kdeDir + "/share/config/kdeglobals"_L1 ; |
551 | } |
552 | |
553 | void refresh(); |
554 | static QVariant readKdeSetting(const QString &key, const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings); |
555 | static void readKdeSystemPalette(const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings, QPalette *pal); |
556 | static QFont *kdeFont(const QVariant &fontValue); |
557 | static QStringList kdeIconThemeSearchPaths(const QStringList &kdeDirs); |
558 | |
559 | const QStringList kdeDirs; |
560 | const int kdeVersion; |
561 | |
562 | ResourceHelper resources; |
563 | QString iconThemeName; |
564 | QString iconFallbackThemeName; |
565 | QStringList styleNames; |
566 | int toolButtonStyle = Qt::ToolButtonTextBesideIcon; |
567 | int toolBarIconSize = 0; |
568 | bool singleClick = true; |
569 | bool showIconsOnPushButtons = true; |
570 | int wheelScrollLines = 3; |
571 | int doubleClickInterval = 400; |
572 | int startDragDist = 10; |
573 | int startDragTime = 500; |
574 | int cursorBlinkRate = 1000; |
575 | Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; |
576 | void updateColorScheme(const QString &themeName); |
577 | |
578 | #ifndef QT_NO_DBUS |
579 | private: |
580 | std::unique_ptr<QGenericUnixThemeDBusListener> dbus; |
581 | bool initDbus(); |
582 | void settingChangedHandler(QGenericUnixThemeDBusListener::Provider provider, |
583 | QGenericUnixThemeDBusListener::Setting setting, |
584 | const QString &value); |
585 | #endif // QT_NO_DBUS |
586 | }; |
587 | |
588 | #ifndef QT_NO_DBUS |
589 | void QKdeThemePrivate::settingChangedHandler(QGenericUnixThemeDBusListener::Provider provider, |
590 | QGenericUnixThemeDBusListener::Setting setting, |
591 | const QString &value) |
592 | { |
593 | if (provider != QGenericUnixThemeDBusListener::Provider::Kde) |
594 | return; |
595 | |
596 | switch (setting) { |
597 | case QGenericUnixThemeDBusListener::Setting::ColorTheme: |
598 | qCDebug(lcQpaThemeDBus) << "KDE color theme changed to:" << value; |
599 | break; |
600 | case QGenericUnixThemeDBusListener::Setting::Theme: |
601 | qCDebug(lcQpaThemeDBus) << "KDE global theme changed to:" << value; |
602 | break; |
603 | case QGenericUnixThemeDBusListener::Setting::ApplicationStyle: |
604 | qCDebug(lcQpaThemeDBus) << "KDE application style changed to:" << value; |
605 | break; |
606 | } |
607 | |
608 | refresh(); |
609 | } |
610 | |
611 | bool QKdeThemePrivate::initDbus() |
612 | { |
613 | dbus.reset(p: new QGenericUnixThemeDBusListener()); |
614 | Q_ASSERT(dbus); |
615 | |
616 | // Wrap slot in a lambda to avoid inheriting QKdeThemePrivate from QObject |
617 | auto wrapper = [this](QGenericUnixThemeDBusListener::Provider provider, |
618 | QGenericUnixThemeDBusListener::Setting setting, |
619 | const QString &value) { |
620 | settingChangedHandler(provider, setting, value); |
621 | }; |
622 | |
623 | return QObject::connect(sender: dbus.get(), signal: &QGenericUnixThemeDBusListener::settingChanged, slot&: wrapper); |
624 | } |
625 | #endif // QT_NO_DBUS |
626 | |
627 | QKdeThemePrivate::QKdeThemePrivate(const QStringList &kdeDirs, int kdeVersion) |
628 | : kdeDirs(kdeDirs), kdeVersion(kdeVersion) |
629 | { |
630 | #ifndef QT_NO_DBUS |
631 | initDbus(); |
632 | #endif // QT_NO_DBUS |
633 | } |
634 | |
635 | void QKdeThemePrivate::refresh() |
636 | { |
637 | resources.clear(); |
638 | |
639 | toolButtonStyle = Qt::ToolButtonTextBesideIcon; |
640 | toolBarIconSize = 0; |
641 | styleNames.clear(); |
642 | if (kdeVersion >= 5) |
643 | styleNames << QStringLiteral("breeze" ); |
644 | styleNames << QStringLiteral("Oxygen" ) << QStringLiteral("Fusion" ) << QStringLiteral("windows" ); |
645 | if (kdeVersion >= 5) |
646 | iconFallbackThemeName = iconThemeName = QStringLiteral("breeze" ); |
647 | else |
648 | iconFallbackThemeName = iconThemeName = QStringLiteral("oxygen" ); |
649 | |
650 | QHash<QString, QSettings*> kdeSettings; |
651 | |
652 | QPalette systemPalette = QPalette(); |
653 | readKdeSystemPalette(kdeDirs, kdeVersion, kdeSettings, pal: &systemPalette); |
654 | resources.palettes[QPlatformTheme::SystemPalette] = new QPalette(systemPalette); |
655 | //## TODO tooltip color |
656 | |
657 | const QVariant styleValue = readKdeSetting(QStringLiteral("widgetStyle" ), kdeDirs, kdeVersion, kdeSettings); |
658 | if (styleValue.isValid()) { |
659 | const QString style = styleValue.toString(); |
660 | if (style != styleNames.front()) |
661 | styleNames.push_front(t: style); |
662 | } |
663 | |
664 | const QVariant colorScheme = readKdeSetting(QStringLiteral("ColorScheme" ), kdeDirs, |
665 | kdeVersion, kdeSettings); |
666 | |
667 | if (colorScheme.isValid()) |
668 | updateColorScheme(themeName: colorScheme.toString()); |
669 | else |
670 | m_colorScheme = Qt::ColorScheme::Unknown; |
671 | |
672 | const QVariant singleClickValue = readKdeSetting(QStringLiteral("KDE/SingleClick" ), kdeDirs, kdeVersion, kdeSettings); |
673 | if (singleClickValue.isValid()) |
674 | singleClick = singleClickValue.toBool(); |
675 | |
676 | const QVariant showIconsOnPushButtonsValue = readKdeSetting(QStringLiteral("KDE/ShowIconsOnPushButtons" ), kdeDirs, kdeVersion, kdeSettings); |
677 | if (showIconsOnPushButtonsValue.isValid()) |
678 | showIconsOnPushButtons = showIconsOnPushButtonsValue.toBool(); |
679 | |
680 | const QVariant themeValue = readKdeSetting(QStringLiteral("Icons/Theme" ), kdeDirs, kdeVersion, kdeSettings); |
681 | if (themeValue.isValid()) |
682 | iconThemeName = themeValue.toString(); |
683 | |
684 | const QVariant toolBarIconSizeValue = readKdeSetting(QStringLiteral("ToolbarIcons/Size" ), kdeDirs, kdeVersion, kdeSettings); |
685 | if (toolBarIconSizeValue.isValid()) |
686 | toolBarIconSize = toolBarIconSizeValue.toInt(); |
687 | |
688 | const QVariant toolbarStyleValue = readKdeSetting(QStringLiteral("Toolbar style/ToolButtonStyle" ), kdeDirs, kdeVersion, kdeSettings); |
689 | if (toolbarStyleValue.isValid()) { |
690 | const QString toolBarStyle = toolbarStyleValue.toString(); |
691 | if (toolBarStyle == "TextBesideIcon"_L1 ) |
692 | toolButtonStyle = Qt::ToolButtonTextBesideIcon; |
693 | else if (toolBarStyle == "TextOnly"_L1 ) |
694 | toolButtonStyle = Qt::ToolButtonTextOnly; |
695 | else if (toolBarStyle == "TextUnderIcon"_L1 ) |
696 | toolButtonStyle = Qt::ToolButtonTextUnderIcon; |
697 | } |
698 | |
699 | const QVariant wheelScrollLinesValue = readKdeSetting(QStringLiteral("KDE/WheelScrollLines" ), kdeDirs, kdeVersion, kdeSettings); |
700 | if (wheelScrollLinesValue.isValid()) |
701 | wheelScrollLines = wheelScrollLinesValue.toInt(); |
702 | |
703 | const QVariant doubleClickIntervalValue = readKdeSetting(QStringLiteral("KDE/DoubleClickInterval" ), kdeDirs, kdeVersion, kdeSettings); |
704 | if (doubleClickIntervalValue.isValid()) |
705 | doubleClickInterval = doubleClickIntervalValue.toInt(); |
706 | |
707 | const QVariant startDragDistValue = readKdeSetting(QStringLiteral("KDE/StartDragDist" ), kdeDirs, kdeVersion, kdeSettings); |
708 | if (startDragDistValue.isValid()) |
709 | startDragDist = startDragDistValue.toInt(); |
710 | |
711 | const QVariant startDragTimeValue = readKdeSetting(QStringLiteral("KDE/StartDragTime" ), kdeDirs, kdeVersion, kdeSettings); |
712 | if (startDragTimeValue.isValid()) |
713 | startDragTime = startDragTimeValue.toInt(); |
714 | |
715 | const QVariant cursorBlinkRateValue = readKdeSetting(QStringLiteral("KDE/CursorBlinkRate" ), kdeDirs, kdeVersion, kdeSettings); |
716 | if (cursorBlinkRateValue.isValid()) { |
717 | cursorBlinkRate = cursorBlinkRateValue.toInt(); |
718 | cursorBlinkRate = cursorBlinkRate > 0 ? qBound(min: 200, val: cursorBlinkRate, max: 2000) : 0; |
719 | } |
720 | |
721 | // Read system font, ignore 'smallestReadableFont' |
722 | if (QFont *systemFont = kdeFont(fontValue: readKdeSetting(QStringLiteral("font" ), kdeDirs, kdeVersion, kdeSettings))) |
723 | resources.fonts[QPlatformTheme::SystemFont] = systemFont; |
724 | else |
725 | resources.fonts[QPlatformTheme::SystemFont] = new QFont(QLatin1StringView(defaultSystemFontNameC), defaultSystemFontSize); |
726 | |
727 | if (QFont *fixedFont = kdeFont(fontValue: readKdeSetting(QStringLiteral("fixed" ), kdeDirs, kdeVersion, kdeSettings))) { |
728 | resources.fonts[QPlatformTheme::FixedFont] = fixedFont; |
729 | } else { |
730 | fixedFont = new QFont(QLatin1StringView(defaultFixedFontNameC), defaultSystemFontSize); |
731 | fixedFont->setStyleHint(QFont::TypeWriter); |
732 | resources.fonts[QPlatformTheme::FixedFont] = fixedFont; |
733 | } |
734 | |
735 | if (QFont * = kdeFont(fontValue: readKdeSetting(QStringLiteral("menuFont" ), kdeDirs, kdeVersion, kdeSettings))) { |
736 | resources.fonts[QPlatformTheme::MenuFont] = menuFont; |
737 | resources.fonts[QPlatformTheme::MenuBarFont] = new QFont(*menuFont); |
738 | } |
739 | |
740 | if (QFont *toolBarFont = kdeFont(fontValue: readKdeSetting(QStringLiteral("toolBarFont" ), kdeDirs, kdeVersion, kdeSettings))) |
741 | resources.fonts[QPlatformTheme::ToolButtonFont] = toolBarFont; |
742 | |
743 | QWindowSystemInterface::handleThemeChange(); |
744 | |
745 | qCDebug(lcQpaFonts) << "default fonts: system" << resources.fonts[QPlatformTheme::SystemFont] |
746 | << "fixed" << resources.fonts[QPlatformTheme::FixedFont]; |
747 | qDeleteAll(c: kdeSettings); |
748 | } |
749 | |
750 | QVariant QKdeThemePrivate::readKdeSetting(const QString &key, const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings) |
751 | { |
752 | for (const QString &kdeDir : kdeDirs) { |
753 | QSettings *settings = kdeSettings.value(key: kdeDir); |
754 | if (!settings) { |
755 | const QString kdeGlobalsPath = kdeGlobals(kdeDir, kdeVersion); |
756 | if (QFileInfo(kdeGlobalsPath).isReadable()) { |
757 | settings = new QSettings(kdeGlobalsPath, QSettings::IniFormat); |
758 | kdeSettings.insert(key: kdeDir, value: settings); |
759 | } |
760 | } |
761 | if (settings) { |
762 | const QVariant value = settings->value(key); |
763 | if (value.isValid()) |
764 | return value; |
765 | } |
766 | } |
767 | return QVariant(); |
768 | } |
769 | |
770 | // Reads the color from the KDE configuration, and store it in the |
771 | // palette with the given color role if found. |
772 | static inline bool kdeColor(QPalette *pal, QPalette::ColorRole role, const QVariant &value) |
773 | { |
774 | if (!value.isValid()) |
775 | return false; |
776 | const QStringList values = value.toStringList(); |
777 | if (values.size() != 3) |
778 | return false; |
779 | pal->setBrush(acr: role, abrush: QColor(values.at(i: 0).toInt(), values.at(i: 1).toInt(), values.at(i: 2).toInt())); |
780 | return true; |
781 | } |
782 | |
783 | void QKdeThemePrivate::readKdeSystemPalette(const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings, QPalette *pal) |
784 | { |
785 | if (!kdeColor(pal, role: QPalette::Button, value: readKdeSetting(QStringLiteral("Colors:Button/BackgroundNormal" ), kdeDirs, kdeVersion, kdeSettings))) { |
786 | // kcolorscheme.cpp: SetDefaultColors |
787 | const QColor defaultWindowBackground(214, 210, 208); |
788 | const QColor defaultButtonBackground(223, 220, 217); |
789 | *pal = QPalette(defaultButtonBackground, defaultWindowBackground); |
790 | return; |
791 | } |
792 | |
793 | kdeColor(pal, role: QPalette::Window, value: readKdeSetting(QStringLiteral("Colors:Window/BackgroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
794 | kdeColor(pal, role: QPalette::Text, value: readKdeSetting(QStringLiteral("Colors:View/ForegroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
795 | kdeColor(pal, role: QPalette::WindowText, value: readKdeSetting(QStringLiteral("Colors:Window/ForegroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
796 | kdeColor(pal, role: QPalette::Base, value: readKdeSetting(QStringLiteral("Colors:View/BackgroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
797 | kdeColor(pal, role: QPalette::Highlight, value: readKdeSetting(QStringLiteral("Colors:Selection/BackgroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
798 | kdeColor(pal, role: QPalette::HighlightedText, value: readKdeSetting(QStringLiteral("Colors:Selection/ForegroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
799 | kdeColor(pal, role: QPalette::AlternateBase, value: readKdeSetting(QStringLiteral("Colors:View/BackgroundAlternate" ), kdeDirs, kdeVersion, kdeSettings)); |
800 | kdeColor(pal, role: QPalette::ButtonText, value: readKdeSetting(QStringLiteral("Colors:Button/ForegroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
801 | kdeColor(pal, role: QPalette::Link, value: readKdeSetting(QStringLiteral("Colors:View/ForegroundLink" ), kdeDirs, kdeVersion, kdeSettings)); |
802 | kdeColor(pal, role: QPalette::LinkVisited, value: readKdeSetting(QStringLiteral("Colors:View/ForegroundVisited" ), kdeDirs, kdeVersion, kdeSettings)); |
803 | kdeColor(pal, role: QPalette::ToolTipBase, value: readKdeSetting(QStringLiteral("Colors:Tooltip/BackgroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
804 | kdeColor(pal, role: QPalette::ToolTipText, value: readKdeSetting(QStringLiteral("Colors:Tooltip/ForegroundNormal" ), kdeDirs, kdeVersion, kdeSettings)); |
805 | |
806 | // The above code sets _all_ color roles to "normal" colors. In KDE, the disabled |
807 | // color roles are calculated by applying various effects described in kdeglobals. |
808 | // We use a bit simpler approach here, similar logic than in qt_palette_from_color(). |
809 | const QColor button = pal->color(cr: QPalette::Button); |
810 | int h, s, v; |
811 | button.getHsv(h: &h, s: &s, v: &v); |
812 | |
813 | const QBrush whiteBrush = QBrush(Qt::white); |
814 | const QBrush buttonBrush = QBrush(button); |
815 | const QBrush buttonBrushDark = QBrush(button.darker(f: v > 128 ? 200 : 50)); |
816 | const QBrush buttonBrushDark150 = QBrush(button.darker(f: v > 128 ? 150 : 75)); |
817 | const QBrush buttonBrushLight150 = QBrush(button.lighter(f: v > 128 ? 150 : 75)); |
818 | const QBrush buttonBrushLight = QBrush(button.lighter(f: v > 128 ? 200 : 50)); |
819 | |
820 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::WindowText, brush: buttonBrushDark); |
821 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::ButtonText, brush: buttonBrushDark); |
822 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Button, brush: buttonBrush); |
823 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Text, brush: buttonBrushDark); |
824 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::BrightText, brush: whiteBrush); |
825 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Base, brush: buttonBrush); |
826 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Window, brush: buttonBrush); |
827 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Highlight, brush: buttonBrushDark150); |
828 | pal->setBrush(cg: QPalette::Disabled, cr: QPalette::HighlightedText, brush: buttonBrushLight150); |
829 | |
830 | // set calculated colors for all groups |
831 | pal->setBrush(acr: QPalette::Light, abrush: buttonBrushLight); |
832 | pal->setBrush(acr: QPalette::Midlight, abrush: buttonBrushLight150); |
833 | pal->setBrush(acr: QPalette::Mid, abrush: buttonBrushDark150); |
834 | pal->setBrush(acr: QPalette::Dark, abrush: buttonBrushDark); |
835 | } |
836 | |
837 | /*! |
838 | \class QKdeTheme |
839 | \brief QKdeTheme is a theme implementation for the KDE desktop (version 4 or higher). |
840 | \since 5.0 |
841 | \internal |
842 | \ingroup qpa |
843 | */ |
844 | |
845 | const char *QKdeTheme::name = "kde" ; |
846 | |
847 | QKdeTheme::QKdeTheme(const QStringList& kdeDirs, int kdeVersion) |
848 | : QPlatformTheme(new QKdeThemePrivate(kdeDirs,kdeVersion)) |
849 | { |
850 | d_func()->refresh(); |
851 | } |
852 | |
853 | QFont *QKdeThemePrivate::kdeFont(const QVariant &fontValue) |
854 | { |
855 | if (fontValue.isValid()) { |
856 | // Read font value: Might be a QStringList as KDE stores fonts without quotes. |
857 | // Also retrieve the family for the constructor since we cannot use the |
858 | // default constructor of QFont, which accesses QGuiApplication::systemFont() |
859 | // causing recursion. |
860 | QString fontDescription; |
861 | QString fontFamily; |
862 | if (fontValue.userType() == QMetaType::QStringList) { |
863 | const QStringList list = fontValue.toStringList(); |
864 | if (!list.isEmpty()) { |
865 | fontFamily = list.first(); |
866 | fontDescription = list.join(sep: u','); |
867 | } |
868 | } else { |
869 | fontDescription = fontFamily = fontValue.toString(); |
870 | } |
871 | if (!fontDescription.isEmpty()) { |
872 | QFont font(fontFamily); |
873 | if (font.fromString(fontDescription)) |
874 | return new QFont(font); |
875 | } |
876 | } |
877 | return nullptr; |
878 | } |
879 | |
880 | |
881 | QStringList QKdeThemePrivate::kdeIconThemeSearchPaths(const QStringList &kdeDirs) |
882 | { |
883 | QStringList paths = QGenericUnixTheme::xdgIconThemePaths(); |
884 | const QString iconPath = QStringLiteral("/share/icons" ); |
885 | for (const QString &candidate : kdeDirs) { |
886 | const QFileInfo fi(candidate + iconPath); |
887 | if (fi.isDir()) |
888 | paths.append(t: fi.absoluteFilePath()); |
889 | } |
890 | return paths; |
891 | } |
892 | |
893 | QVariant QKdeTheme::themeHint(QPlatformTheme::ThemeHint hint) const |
894 | { |
895 | Q_D(const QKdeTheme); |
896 | switch (hint) { |
897 | case QPlatformTheme::UseFullScreenForPopupMenu: |
898 | return QVariant(true); |
899 | case QPlatformTheme::DialogButtonBoxButtonsHaveIcons: |
900 | return QVariant(d->showIconsOnPushButtons); |
901 | case QPlatformTheme::DialogButtonBoxLayout: |
902 | return QVariant(QPlatformDialogHelper::KdeLayout); |
903 | case QPlatformTheme::ToolButtonStyle: |
904 | return QVariant(d->toolButtonStyle); |
905 | case QPlatformTheme::ToolBarIconSize: |
906 | return QVariant(d->toolBarIconSize); |
907 | case QPlatformTheme::SystemIconThemeName: |
908 | return QVariant(d->iconThemeName); |
909 | case QPlatformTheme::SystemIconFallbackThemeName: |
910 | return QVariant(d->iconFallbackThemeName); |
911 | case QPlatformTheme::IconThemeSearchPaths: |
912 | return QVariant(d->kdeIconThemeSearchPaths(kdeDirs: d->kdeDirs)); |
913 | case QPlatformTheme::IconPixmapSizes: |
914 | return QVariant::fromValue(value: availableXdgFileIconSizes()); |
915 | case QPlatformTheme::StyleNames: |
916 | return QVariant(d->styleNames); |
917 | case QPlatformTheme::KeyboardScheme: |
918 | return QVariant(int(KdeKeyboardScheme)); |
919 | case QPlatformTheme::ItemViewActivateItemOnSingleClick: |
920 | return QVariant(d->singleClick); |
921 | case QPlatformTheme::WheelScrollLines: |
922 | return QVariant(d->wheelScrollLines); |
923 | case QPlatformTheme::MouseDoubleClickInterval: |
924 | return QVariant(d->doubleClickInterval); |
925 | case QPlatformTheme::StartDragTime: |
926 | return QVariant(d->startDragTime); |
927 | case QPlatformTheme::StartDragDistance: |
928 | return QVariant(d->startDragDist); |
929 | case QPlatformTheme::CursorFlashTime: |
930 | return QVariant(d->cursorBlinkRate); |
931 | case QPlatformTheme::UiEffects: |
932 | return QVariant(int(HoverEffect)); |
933 | case QPlatformTheme::MouseCursorTheme: |
934 | return QVariant(mouseCursorTheme()); |
935 | case QPlatformTheme::MouseCursorSize: |
936 | return QVariant(mouseCursorSize()); |
937 | default: |
938 | break; |
939 | } |
940 | return QPlatformTheme::themeHint(hint); |
941 | } |
942 | |
943 | QIcon QKdeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions) const |
944 | { |
945 | #if QT_CONFIG(mimetype) |
946 | return xdgFileIcon(fileInfo); |
947 | #else |
948 | Q_UNUSED(fileInfo); |
949 | return QIcon(); |
950 | #endif |
951 | } |
952 | |
953 | Qt::ColorScheme QKdeTheme::colorScheme() const |
954 | { |
955 | return d_func()->m_colorScheme; |
956 | } |
957 | |
958 | /*! |
959 | \internal |
960 | \brief QKdeTheme::setColorScheme - guess and set appearance for unix themes. |
961 | KDE themes do not have an appearance property. |
962 | The key words "dark" or "light" should be part of the theme name. |
963 | This is, however, not a mandatory convention. |
964 | |
965 | If \param themeName contains a key word, the respective appearance is set. |
966 | If it doesn't, the appearance is heuristically determined by comparing text and base color |
967 | of the system palette. |
968 | */ |
969 | void QKdeThemePrivate::updateColorScheme(const QString &themeName) |
970 | { |
971 | if (themeName.contains(s: QLatin1StringView("light" ), cs: Qt::CaseInsensitive)) { |
972 | m_colorScheme = Qt::ColorScheme::Light; |
973 | return; |
974 | } |
975 | if (themeName.contains(s: QLatin1StringView("dark" ), cs: Qt::CaseInsensitive)) { |
976 | m_colorScheme = Qt::ColorScheme::Dark; |
977 | return; |
978 | } |
979 | |
980 | if (systemPalette) { |
981 | if (systemPalette->text().color().lightness() < systemPalette->base().color().lightness()) { |
982 | m_colorScheme = Qt::ColorScheme::Light; |
983 | return; |
984 | } |
985 | if (systemPalette->text().color().lightness() > systemPalette->base().color().lightness()) { |
986 | m_colorScheme = Qt::ColorScheme::Dark; |
987 | return; |
988 | } |
989 | } |
990 | |
991 | m_colorScheme = Qt::ColorScheme::Unknown; |
992 | } |
993 | |
994 | |
995 | const QPalette *QKdeTheme::palette(Palette type) const |
996 | { |
997 | Q_D(const QKdeTheme); |
998 | return d->resources.palettes[type]; |
999 | } |
1000 | |
1001 | const QFont *QKdeTheme::font(Font type) const |
1002 | { |
1003 | Q_D(const QKdeTheme); |
1004 | return d->resources.fonts[type]; |
1005 | } |
1006 | |
1007 | QPlatformTheme *QKdeTheme::createKdeTheme() |
1008 | { |
1009 | const QByteArray kdeVersionBA = qgetenv(varName: "KDE_SESSION_VERSION" ); |
1010 | const int kdeVersion = kdeVersionBA.toInt(); |
1011 | if (kdeVersion < 4) |
1012 | return nullptr; |
1013 | |
1014 | if (kdeVersion > 4) |
1015 | // Plasma 5 follows XDG spec |
1016 | // but uses the same config file format: |
1017 | return new QKdeTheme(QStandardPaths::standardLocations(type: QStandardPaths::GenericConfigLocation), kdeVersion); |
1018 | |
1019 | // Determine KDE prefixes in the following priority order: |
1020 | // - KDEHOME and KDEDIRS environment variables |
1021 | // - ~/.kde(<version>) |
1022 | // - read prefixes from /etc/kde<version>rc |
1023 | // - fallback to /etc/kde<version> |
1024 | |
1025 | QStringList kdeDirs; |
1026 | const QString kdeHomePathVar = QFile::decodeName(localFileName: qgetenv(varName: "KDEHOME" )); |
1027 | if (!kdeHomePathVar.isEmpty()) |
1028 | kdeDirs += kdeHomePathVar; |
1029 | |
1030 | const QString kdeDirsVar = QFile::decodeName(localFileName: qgetenv(varName: "KDEDIRS" )); |
1031 | if (!kdeDirsVar.isEmpty()) |
1032 | kdeDirs += kdeDirsVar.split(sep: u':', behavior: Qt::SkipEmptyParts); |
1033 | |
1034 | const QString kdeVersionHomePath = QDir::homePath() + "/.kde"_L1 + QLatin1StringView(kdeVersionBA); |
1035 | if (QFileInfo(kdeVersionHomePath).isDir()) |
1036 | kdeDirs += kdeVersionHomePath; |
1037 | |
1038 | const QString kdeHomePath = QDir::homePath() + "/.kde"_L1 ; |
1039 | if (QFileInfo(kdeHomePath).isDir()) |
1040 | kdeDirs += kdeHomePath; |
1041 | |
1042 | const QString kdeRcPath = "/etc/kde"_L1 + QLatin1StringView(kdeVersionBA) + "rc"_L1 ; |
1043 | if (QFileInfo(kdeRcPath).isReadable()) { |
1044 | QSettings kdeSettings(kdeRcPath, QSettings::IniFormat); |
1045 | kdeSettings.beginGroup(QStringLiteral("Directories-default" )); |
1046 | kdeDirs += kdeSettings.value(QStringLiteral("prefixes" )).toStringList(); |
1047 | } |
1048 | |
1049 | const QString kdeVersionPrefix = "/etc/kde"_L1 + QLatin1StringView(kdeVersionBA); |
1050 | if (QFileInfo(kdeVersionPrefix).isDir()) |
1051 | kdeDirs += kdeVersionPrefix; |
1052 | |
1053 | kdeDirs.removeDuplicates(); |
1054 | if (kdeDirs.isEmpty()) { |
1055 | qWarning(msg: "Unable to determine KDE dirs" ); |
1056 | return nullptr; |
1057 | } |
1058 | |
1059 | return new QKdeTheme(kdeDirs, kdeVersion); |
1060 | } |
1061 | |
1062 | #ifndef QT_NO_DBUS |
1063 | QPlatformMenuBar *QKdeTheme::() const |
1064 | { |
1065 | if (isDBusGlobalMenuAvailable()) |
1066 | return new QDBusMenuBar(); |
1067 | return nullptr; |
1068 | } |
1069 | #endif |
1070 | |
1071 | #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) |
1072 | QPlatformSystemTrayIcon *QKdeTheme::createPlatformSystemTrayIcon() const |
1073 | { |
1074 | if (isDBusTrayAvailable()) |
1075 | return new QDBusTrayIcon(); |
1076 | return nullptr; |
1077 | } |
1078 | #endif |
1079 | |
1080 | #endif // settings |
1081 | |
1082 | /*! |
1083 | \class QGnomeTheme |
1084 | \brief QGnomeTheme is a theme implementation for the Gnome desktop. |
1085 | \since 5.0 |
1086 | \internal |
1087 | \ingroup qpa |
1088 | */ |
1089 | |
1090 | const char *QGnomeTheme::name = "gnome" ; |
1091 | |
1092 | class QGnomeThemePrivate : public QPlatformThemePrivate |
1093 | { |
1094 | public: |
1095 | QGnomeThemePrivate(); |
1096 | ~QGnomeThemePrivate(); |
1097 | |
1098 | void configureFonts(const QString >kFontName) const |
1099 | { |
1100 | Q_ASSERT(!systemFont); |
1101 | const int split = gtkFontName.lastIndexOf(c: QChar::Space); |
1102 | float size = QStringView{gtkFontName}.mid(pos: split + 1).toFloat(); |
1103 | QString fontName = gtkFontName.left(n: split); |
1104 | |
1105 | systemFont = new QFont(fontName, size); |
1106 | fixedFont = new QFont(QLatin1StringView(defaultFixedFontNameC), systemFont->pointSize()); |
1107 | fixedFont->setStyleHint(QFont::TypeWriter); |
1108 | qCDebug(lcQpaFonts) << "default fonts: system" << systemFont << "fixed" << fixedFont; |
1109 | } |
1110 | |
1111 | mutable QFont *systemFont = nullptr; |
1112 | mutable QFont *fixedFont = nullptr; |
1113 | |
1114 | #ifndef QT_NO_DBUS |
1115 | Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; |
1116 | private: |
1117 | std::unique_ptr<QGenericUnixThemeDBusListener> dbus; |
1118 | bool initDbus(); |
1119 | void updateColorScheme(const QString &themeName); |
1120 | #endif // QT_NO_DBUS |
1121 | }; |
1122 | |
1123 | QGnomeThemePrivate::QGnomeThemePrivate() |
1124 | { |
1125 | #ifndef QT_NO_DBUS |
1126 | initDbus(); |
1127 | #endif // QT_NO_DBUS |
1128 | } |
1129 | QGnomeThemePrivate::~QGnomeThemePrivate() |
1130 | { |
1131 | if (systemFont) |
1132 | delete systemFont; |
1133 | if (fixedFont) |
1134 | delete fixedFont; |
1135 | } |
1136 | |
1137 | #ifndef QT_NO_DBUS |
1138 | bool QGnomeThemePrivate::initDbus() |
1139 | { |
1140 | dbus.reset(p: new QGenericUnixThemeDBusListener()); |
1141 | Q_ASSERT(dbus); |
1142 | |
1143 | // Wrap slot in a lambda to avoid inheriting QGnomeThemePrivate from QObject |
1144 | auto wrapper = [this](QGenericUnixThemeDBusListener::Provider provider, |
1145 | QGenericUnixThemeDBusListener::Setting setting, |
1146 | const QString &value) { |
1147 | if (provider != QGenericUnixThemeDBusListener::Provider::Gnome |
1148 | && provider != QGenericUnixThemeDBusListener::Provider::Gtk) { |
1149 | return; |
1150 | } |
1151 | |
1152 | if (setting == QGenericUnixThemeDBusListener::Setting::Theme) |
1153 | updateColorScheme(themeName: value); |
1154 | }; |
1155 | |
1156 | return QObject::connect(sender: dbus.get(), signal: &QGenericUnixThemeDBusListener::settingChanged, slot&: wrapper); |
1157 | } |
1158 | |
1159 | void QGnomeThemePrivate::updateColorScheme(const QString &themeName) |
1160 | { |
1161 | const auto oldColorScheme = m_colorScheme; |
1162 | if (themeName.contains(s: QLatin1StringView("light" ), cs: Qt::CaseInsensitive)) { |
1163 | m_colorScheme = Qt::ColorScheme::Light; |
1164 | } else if (themeName.contains(s: QLatin1StringView("dark" ), cs: Qt::CaseInsensitive)) { |
1165 | m_colorScheme = Qt::ColorScheme::Dark; |
1166 | } else { |
1167 | m_colorScheme = Qt::ColorScheme::Unknown; |
1168 | } |
1169 | |
1170 | if (oldColorScheme != m_colorScheme) |
1171 | QWindowSystemInterface::handleThemeChange(); |
1172 | } |
1173 | #endif // QT_NO_DBUS |
1174 | |
1175 | QGnomeTheme::QGnomeTheme() |
1176 | : QPlatformTheme(new QGnomeThemePrivate()) |
1177 | { |
1178 | } |
1179 | |
1180 | QVariant QGnomeTheme::themeHint(QPlatformTheme::ThemeHint hint) const |
1181 | { |
1182 | switch (hint) { |
1183 | case QPlatformTheme::DialogButtonBoxButtonsHaveIcons: |
1184 | return QVariant(true); |
1185 | case QPlatformTheme::DialogButtonBoxLayout: |
1186 | return QVariant(QPlatformDialogHelper::GnomeLayout); |
1187 | case QPlatformTheme::SystemIconThemeName: |
1188 | return QVariant(QStringLiteral("Adwaita" )); |
1189 | case QPlatformTheme::SystemIconFallbackThemeName: |
1190 | return QVariant(QStringLiteral("gnome" )); |
1191 | case QPlatformTheme::IconThemeSearchPaths: |
1192 | return QVariant(QGenericUnixTheme::xdgIconThemePaths()); |
1193 | case QPlatformTheme::IconPixmapSizes: |
1194 | return QVariant::fromValue(value: availableXdgFileIconSizes()); |
1195 | case QPlatformTheme::StyleNames: { |
1196 | QStringList styleNames; |
1197 | styleNames << QStringLiteral("Fusion" ) << QStringLiteral("windows" ); |
1198 | return QVariant(styleNames); |
1199 | } |
1200 | case QPlatformTheme::KeyboardScheme: |
1201 | return QVariant(int(GnomeKeyboardScheme)); |
1202 | case QPlatformTheme::PasswordMaskCharacter: |
1203 | return QVariant(QChar(0x2022)); |
1204 | case QPlatformTheme::UiEffects: |
1205 | return QVariant(int(HoverEffect)); |
1206 | case QPlatformTheme::ButtonPressKeys: |
1207 | return QVariant::fromValue( |
1208 | value: QList<Qt::Key>({ Qt::Key_Space, Qt::Key_Return, Qt::Key_Enter, Qt::Key_Select })); |
1209 | case QPlatformTheme::PreselectFirstFileInDirectory: |
1210 | return true; |
1211 | case QPlatformTheme::MouseCursorTheme: |
1212 | return QVariant(mouseCursorTheme()); |
1213 | case QPlatformTheme::MouseCursorSize: |
1214 | return QVariant(mouseCursorSize()); |
1215 | default: |
1216 | break; |
1217 | } |
1218 | return QPlatformTheme::themeHint(hint); |
1219 | } |
1220 | |
1221 | QIcon QGnomeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions) const |
1222 | { |
1223 | #if QT_CONFIG(mimetype) |
1224 | return xdgFileIcon(fileInfo); |
1225 | #else |
1226 | Q_UNUSED(fileInfo); |
1227 | return QIcon(); |
1228 | #endif |
1229 | } |
1230 | |
1231 | const QFont *QGnomeTheme::font(Font type) const |
1232 | { |
1233 | Q_D(const QGnomeTheme); |
1234 | if (!d->systemFont) |
1235 | d->configureFonts(gtkFontName: gtkFontName()); |
1236 | switch (type) { |
1237 | case QPlatformTheme::SystemFont: |
1238 | return d->systemFont; |
1239 | case QPlatformTheme::FixedFont: |
1240 | return d->fixedFont; |
1241 | default: |
1242 | return nullptr; |
1243 | } |
1244 | } |
1245 | |
1246 | QString QGnomeTheme::gtkFontName() const |
1247 | { |
1248 | return QStringLiteral("%1 %2" ).arg(a: QLatin1StringView(defaultSystemFontNameC)).arg(a: defaultSystemFontSize); |
1249 | } |
1250 | |
1251 | #ifndef QT_NO_DBUS |
1252 | QPlatformMenuBar *QGnomeTheme::() const |
1253 | { |
1254 | if (isDBusGlobalMenuAvailable()) |
1255 | return new QDBusMenuBar(); |
1256 | return nullptr; |
1257 | } |
1258 | |
1259 | Qt::ColorScheme QGnomeTheme::colorScheme() const |
1260 | { |
1261 | return d_func()->m_colorScheme; |
1262 | } |
1263 | |
1264 | #endif |
1265 | |
1266 | #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) |
1267 | QPlatformSystemTrayIcon *QGnomeTheme::createPlatformSystemTrayIcon() const |
1268 | { |
1269 | if (isDBusTrayAvailable()) |
1270 | return new QDBusTrayIcon(); |
1271 | return nullptr; |
1272 | } |
1273 | #endif |
1274 | |
1275 | QString QGnomeTheme::standardButtonText(int button) const |
1276 | { |
1277 | switch (button) { |
1278 | case QPlatformDialogHelper::Ok: |
1279 | return QCoreApplication::translate(context: "QGnomeTheme" , key: "&OK" ); |
1280 | case QPlatformDialogHelper::Save: |
1281 | return QCoreApplication::translate(context: "QGnomeTheme" , key: "&Save" ); |
1282 | case QPlatformDialogHelper::Cancel: |
1283 | return QCoreApplication::translate(context: "QGnomeTheme" , key: "&Cancel" ); |
1284 | case QPlatformDialogHelper::Close: |
1285 | return QCoreApplication::translate(context: "QGnomeTheme" , key: "&Close" ); |
1286 | case QPlatformDialogHelper::Discard: |
1287 | return QCoreApplication::translate(context: "QGnomeTheme" , key: "Close without Saving" ); |
1288 | default: |
1289 | break; |
1290 | } |
1291 | return QPlatformTheme::standardButtonText(button); |
1292 | } |
1293 | |
1294 | /*! |
1295 | \brief Creates a UNIX theme according to the detected desktop environment. |
1296 | */ |
1297 | |
1298 | QPlatformTheme *QGenericUnixTheme::createUnixTheme(const QString &name) |
1299 | { |
1300 | if (name == QLatin1StringView(QGenericUnixTheme::name)) |
1301 | return new QGenericUnixTheme; |
1302 | #if QT_CONFIG(settings) |
1303 | if (name == QLatin1StringView(QKdeTheme::name)) |
1304 | if (QPlatformTheme *kdeTheme = QKdeTheme::createKdeTheme()) |
1305 | return kdeTheme; |
1306 | #endif |
1307 | if (name == QLatin1StringView(QGnomeTheme::name)) |
1308 | return new QGnomeTheme; |
1309 | return nullptr; |
1310 | } |
1311 | |
1312 | QStringList QGenericUnixTheme::themeNames() |
1313 | { |
1314 | QStringList result; |
1315 | if (QGuiApplication::desktopSettingsAware()) { |
1316 | const QByteArray desktopEnvironment = QGuiApplicationPrivate::platformIntegration()->services()->desktopEnvironment(); |
1317 | QList<QByteArray> gtkBasedEnvironments; |
1318 | gtkBasedEnvironments << "GNOME" |
1319 | << "X-CINNAMON" |
1320 | << "UNITY" |
1321 | << "MATE" |
1322 | << "XFCE" |
1323 | << "LXDE" ; |
1324 | const QList<QByteArray> desktopNames = desktopEnvironment.split(sep: ':'); |
1325 | for (const QByteArray &desktopName : desktopNames) { |
1326 | if (desktopEnvironment == "KDE" ) { |
1327 | #if QT_CONFIG(settings) |
1328 | result.push_back(t: QLatin1StringView(QKdeTheme::name)); |
1329 | #endif |
1330 | } else if (gtkBasedEnvironments.contains(t: desktopName)) { |
1331 | // prefer the GTK3 theme implementation with native dialogs etc. |
1332 | result.push_back(QStringLiteral("gtk3" )); |
1333 | // fallback to the generic Gnome theme if loading the GTK3 theme fails |
1334 | result.push_back(t: QLatin1StringView(QGnomeTheme::name)); |
1335 | } else { |
1336 | // unknown, but lowercase the name (our standard practice) and |
1337 | // remove any "x-" prefix |
1338 | QString s = QString::fromLatin1(ba: desktopName.toLower()); |
1339 | result.push_back(t: s.startsWith(s: "x-"_L1 ) ? s.mid(position: 2) : s); |
1340 | } |
1341 | } |
1342 | } // desktopSettingsAware |
1343 | result.append(t: QLatin1StringView(QGenericUnixTheme::name)); |
1344 | return result; |
1345 | } |
1346 | |
1347 | QT_END_NAMESPACE |
1348 | |
1349 | #ifndef QT_NO_DBUS |
1350 | #include "qgenericunixthemes.moc" |
1351 | #endif // QT_NO_DBUS |
1352 | |