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

source code of qtbase/src/gui/platform/unix/qgenericunixthemes.cpp