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 Q_UNREACHABLE_RETURN(QLatin1StringView());
698}
699
700static constexpr QKdeThemePrivate::KdeSettingType settingsType(QKdeThemePrivate::KdeSetting setting)
701{
702#define CASE(s, type) case QKdeThemePrivate::KdeSetting::s:\
703 return QKdeThemePrivate::KdeSettingType::type
704
705 switch (setting) {
706 CASE(WidgetStyle, Root);
707 CASE(ColorScheme, Root);
708 CASE(SingleClick, KDE);
709 CASE(ShowIconsOnPushButtons, KDE);
710 CASE(IconTheme, Icons);
711 CASE(ToolBarIconSize, ToolBarIcons);
712 CASE(ToolButtonStyle, ToolBarStyle);
713 CASE(WheelScrollLines, KDE);
714 CASE(DoubleClickInterval, KDE);
715 CASE(StartDragDistance, KDE);
716 CASE(StartDragTime, KDE);
717 CASE(CursorBlinkRate, KDE);
718 CASE(Font, Root);
719 CASE(Fixed, Root);
720 CASE(MenuFont, Root);
721 CASE(ToolBarFont, Root);
722 CASE(ButtonBackground, Colors);
723 CASE(WindowBackground, Colors);
724 CASE(ViewForeground, Colors);
725 CASE(WindowForeground, Colors);
726 CASE(ViewBackground, Colors);
727 CASE(SelectionBackground, Colors);
728 CASE(SelectionForeground, Colors);
729 CASE(ViewBackgroundAlternate, Colors);
730 CASE(ButtonForeground, Colors);
731 CASE(ViewForegroundLink, Colors);
732 CASE(ViewForegroundVisited, Colors);
733 CASE(TooltipBackground, Colors);
734 CASE(TooltipForeground, Colors);
735 };
736 Q_UNREACHABLE_RETURN(QKdeThemePrivate::KdeSettingType::Root);
737}
738#undef CASE
739
740static constexpr QLatin1StringView settingsKey(QKdeThemePrivate::KdeSetting setting)
741{
742 switch (setting) {
743 case QKdeThemePrivate::KdeSetting::WidgetStyle:
744 return QLatin1StringView("widgetStyle");
745 case QKdeThemePrivate::KdeSetting::ColorScheme:
746 return QLatin1StringView("ColorScheme");
747 case QKdeThemePrivate::KdeSetting::SingleClick:
748 return QLatin1StringView("SingleClick");
749 case QKdeThemePrivate::KdeSetting::ShowIconsOnPushButtons:
750 return QLatin1StringView("ShowIconsOnPushButtons");
751 case QKdeThemePrivate::KdeSetting::IconTheme:
752 return QLatin1StringView("Theme");
753 case QKdeThemePrivate::KdeSetting::ToolBarIconSize:
754 return QLatin1StringView("Size");
755 case QKdeThemePrivate::KdeSetting::ToolButtonStyle:
756 return QLatin1StringView("ToolButtonStyle");
757 case QKdeThemePrivate::KdeSetting::WheelScrollLines:
758 return QLatin1StringView("WheelScrollLines");
759 case QKdeThemePrivate::KdeSetting::DoubleClickInterval:
760 return QLatin1StringView("DoubleClickInterval");
761 case QKdeThemePrivate::KdeSetting::StartDragDistance:
762 return QLatin1StringView("StartDragDist");
763 case QKdeThemePrivate::KdeSetting::StartDragTime:
764 return QLatin1StringView("StartDragTime");
765 case QKdeThemePrivate::KdeSetting::CursorBlinkRate:
766 return QLatin1StringView("CursorBlinkRate");
767 case QKdeThemePrivate::KdeSetting::Font:
768 return QLatin1StringView("font");
769 case QKdeThemePrivate::KdeSetting::Fixed:
770 return QLatin1StringView("fixed");
771 case QKdeThemePrivate::KdeSetting::MenuFont:
772 return QLatin1StringView("menuFont");
773 case QKdeThemePrivate::KdeSetting::ToolBarFont:
774 return QLatin1StringView("toolBarFont");
775 case QKdeThemePrivate::KdeSetting::ButtonBackground:
776 return QLatin1StringView("Button/BackgroundNormal");
777 case QKdeThemePrivate::KdeSetting::WindowBackground:
778 return QLatin1StringView("Window/BackgroundNormal");
779 case QKdeThemePrivate::KdeSetting::ViewForeground:
780 return QLatin1StringView("View/ForegroundNormal");
781 case QKdeThemePrivate::KdeSetting::WindowForeground:
782 return QLatin1StringView("Window/ForegroundNormal");
783 case QKdeThemePrivate::KdeSetting::ViewBackground:
784 return QLatin1StringView("View/BackgroundNormal");
785 case QKdeThemePrivate::KdeSetting::SelectionBackground:
786 return QLatin1StringView("Selection/BackgroundNormal");
787 case QKdeThemePrivate::KdeSetting::SelectionForeground:
788 return QLatin1StringView("Selection/ForegroundNormal");
789 case QKdeThemePrivate::KdeSetting::ViewBackgroundAlternate:
790 return QLatin1StringView("View/BackgroundAlternate");
791 case QKdeThemePrivate::KdeSetting::ButtonForeground:
792 return QLatin1StringView("Button/ForegroundNormal");
793 case QKdeThemePrivate::KdeSetting::ViewForegroundLink:
794 return QLatin1StringView("View/ForegroundLink");
795 case QKdeThemePrivate::KdeSetting::ViewForegroundVisited:
796 return QLatin1StringView("View/ForegroundVisited");
797 case QKdeThemePrivate::KdeSetting::TooltipBackground:
798 return QLatin1StringView("Tooltip/BackgroundNormal");
799 case QKdeThemePrivate::KdeSetting::TooltipForeground:
800 return QLatin1StringView("Tooltip/ForegroundNormal");
801 };
802 Q_UNREACHABLE_RETURN(QLatin1StringView());
803}
804
805void QKdeThemePrivate::refresh()
806{
807 resources.clear();
808 clearKdeSettings();
809
810 toolButtonStyle = Qt::ToolButtonTextBesideIcon;
811 toolBarIconSize = 0;
812 styleNames.clear();
813 if (kdeVersion >= 5)
814 styleNames << QStringLiteral("breeze");
815 styleNames << QStringLiteral("Oxygen") << QStringLiteral("Fusion") << QStringLiteral("windows");
816 if (kdeVersion >= 5)
817 iconFallbackThemeName = iconThemeName = QStringLiteral("breeze");
818 else
819 iconFallbackThemeName = iconThemeName = QStringLiteral("oxygen");
820
821 QPalette systemPalette = QPalette();
822 readKdeSystemPalette(kdeDirs, kdeVersion, kdeSettings, pal: &systemPalette);
823 resources.palettes[QPlatformTheme::SystemPalette] = new QPalette(systemPalette);
824 //## TODO tooltip color
825
826 const QVariant styleValue = readKdeSetting(s: KdeSetting::WidgetStyle);
827 if (styleValue.isValid()) {
828 const QString style = styleValue.toString();
829 if (style != styleNames.front())
830 styleNames.push_front(t: style);
831 }
832
833 const QVariant colorScheme = readKdeSetting(s: KdeSetting::ColorScheme);
834
835 updateColorScheme(themeName: colorScheme.toString());
836
837 const QVariant singleClickValue = readKdeSetting(s: KdeSetting::SingleClick);
838 if (singleClickValue.isValid())
839 singleClick = singleClickValue.toBool();
840 else if (kdeVersion >= 6) // Plasma 6 defaults to double-click
841 singleClick = false;
842 else // earlier version to single-click
843 singleClick = true;
844
845 const QVariant showIconsOnPushButtonsValue = readKdeSetting(s: KdeSetting::ShowIconsOnPushButtons);
846 if (showIconsOnPushButtonsValue.isValid())
847 showIconsOnPushButtons = showIconsOnPushButtonsValue.toBool();
848
849 const QVariant themeValue = readKdeSetting(s: KdeSetting::IconTheme);
850 if (themeValue.isValid())
851 iconThemeName = themeValue.toString();
852
853 const QVariant toolBarIconSizeValue = readKdeSetting(s: KdeSetting::ToolBarIconSize);
854 if (toolBarIconSizeValue.isValid())
855 toolBarIconSize = toolBarIconSizeValue.toInt();
856
857 const QVariant toolbarStyleValue = readKdeSetting(s: KdeSetting::ToolButtonStyle);
858 if (toolbarStyleValue.isValid()) {
859 const QString toolBarStyle = toolbarStyleValue.toString();
860 if (toolBarStyle == "TextBesideIcon"_L1)
861 toolButtonStyle = Qt::ToolButtonTextBesideIcon;
862 else if (toolBarStyle == "TextOnly"_L1)
863 toolButtonStyle = Qt::ToolButtonTextOnly;
864 else if (toolBarStyle == "TextUnderIcon"_L1)
865 toolButtonStyle = Qt::ToolButtonTextUnderIcon;
866 }
867
868 const QVariant wheelScrollLinesValue = readKdeSetting(s: KdeSetting::WheelScrollLines);
869 if (wheelScrollLinesValue.isValid())
870 wheelScrollLines = wheelScrollLinesValue.toInt();
871
872 const QVariant doubleClickIntervalValue = readKdeSetting(s: KdeSetting::DoubleClickInterval);
873 if (doubleClickIntervalValue.isValid())
874 doubleClickInterval = doubleClickIntervalValue.toInt();
875
876 const QVariant startDragDistValue = readKdeSetting(s: KdeSetting::StartDragDistance);
877 if (startDragDistValue.isValid())
878 startDragDist = startDragDistValue.toInt();
879
880 const QVariant startDragTimeValue = readKdeSetting(s: KdeSetting::StartDragTime);
881 if (startDragTimeValue.isValid())
882 startDragTime = startDragTimeValue.toInt();
883
884 const QVariant cursorBlinkRateValue = readKdeSetting(s: KdeSetting::CursorBlinkRate);
885 if (cursorBlinkRateValue.isValid()) {
886 cursorBlinkRate = cursorBlinkRateValue.toInt();
887 cursorBlinkRate = cursorBlinkRate > 0 ? qBound(min: 200, val: cursorBlinkRate, max: 2000) : 0;
888 }
889
890 // Read system font, ignore 'smallestReadableFont'
891 if (QFont *systemFont = kdeFont(fontValue: readKdeSetting(s: KdeSetting::Font)))
892 resources.fonts[QPlatformTheme::SystemFont] = systemFont;
893 else
894 resources.fonts[QPlatformTheme::SystemFont] = new QFont(QLatin1StringView(defaultSystemFontNameC), defaultSystemFontSize);
895
896 if (QFont *fixedFont = kdeFont(fontValue: readKdeSetting(s: KdeSetting::Fixed))) {
897 resources.fonts[QPlatformTheme::FixedFont] = fixedFont;
898 } else {
899 fixedFont = new QFont(QLatin1StringView(defaultFixedFontNameC), defaultSystemFontSize);
900 fixedFont->setStyleHint(QFont::TypeWriter);
901 resources.fonts[QPlatformTheme::FixedFont] = fixedFont;
902 }
903
904 if (QFont *menuFont = kdeFont(fontValue: readKdeSetting(s: KdeSetting::MenuFont))) {
905 resources.fonts[QPlatformTheme::MenuFont] = menuFont;
906 resources.fonts[QPlatformTheme::MenuBarFont] = new QFont(*menuFont);
907 }
908
909 if (QFont *toolBarFont = kdeFont(fontValue: readKdeSetting(s: KdeSetting::ToolBarFont)))
910 resources.fonts[QPlatformTheme::ToolButtonFont] = toolBarFont;
911
912 QWindowSystemInterface::handleThemeChange();
913
914 qCDebug(lcQpaFonts) << "default fonts: system" << resources.fonts[QPlatformTheme::SystemFont]
915 << "fixed" << resources.fonts[QPlatformTheme::FixedFont];
916 qDeleteAll(c: kdeSettings);
917}
918
919QVariant QKdeThemePrivate::readKdeSetting(KdeSetting s, const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings)
920{
921 for (const QString &kdeDir : kdeDirs) {
922 QSettings *settings = kdeSettings.value(key: kdeDir);
923 if (!settings) {
924 const QString kdeGlobalsPath = kdeGlobals(kdeDir, kdeVersion);
925 if (QFileInfo(kdeGlobalsPath).isReadable()) {
926 settings = new QSettings(kdeGlobalsPath, QSettings::IniFormat);
927 kdeSettings.insert(key: kdeDir, value: settings);
928 }
929 }
930 if (settings) {
931 const QString key = settingsPrefix(settingsType(s)) + settingsKey(s);
932 const QVariant value = settings->value(key);
933 if (value.isValid())
934 return value;
935 }
936 }
937 return QVariant();
938}
939
940QVariant QKdeThemePrivate::readKdeSetting(KdeSetting s) const
941{
942 return readKdeSetting(s, kdeDirs, kdeVersion, kdeSettings);
943}
944
945void QKdeThemePrivate::clearKdeSettings() const
946{
947 kdeSettings.clear();
948}
949
950// Reads the color from the KDE configuration, and store it in the
951// palette with the given color role if found.
952static inline bool kdeColor(QPalette *pal, QPalette::ColorRole role, const QVariant &value)
953{
954 if (!value.isValid())
955 return false;
956 const QStringList values = value.toStringList();
957 if (values.size() != 3)
958 return false;
959 pal->setBrush(acr: role, abrush: QColor(values.at(i: 0).toInt(), values.at(i: 1).toInt(), values.at(i: 2).toInt()));
960 return true;
961}
962
963void QKdeThemePrivate::readKdeSystemPalette(const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings, QPalette *pal)
964{
965 if (!kdeColor(pal, role: QPalette::Button, value: readKdeSetting(s: KdeSetting::ButtonBackground, kdeDirs, kdeVersion, kdeSettings))) {
966 // kcolorscheme.cpp: SetDefaultColors
967 const QColor defaultWindowBackground(214, 210, 208);
968 const QColor defaultButtonBackground(223, 220, 217);
969 *pal = QPalette(defaultButtonBackground, defaultWindowBackground);
970 return;
971 }
972
973 kdeColor(pal, role: QPalette::Window, value: readKdeSetting(s: KdeSetting::WindowBackground, kdeDirs, kdeVersion, kdeSettings));
974 kdeColor(pal, role: QPalette::Text, value: readKdeSetting(s: KdeSetting::ViewForeground, kdeDirs, kdeVersion, kdeSettings));
975 kdeColor(pal, role: QPalette::WindowText, value: readKdeSetting(s: KdeSetting::WindowForeground, kdeDirs, kdeVersion, kdeSettings));
976 kdeColor(pal, role: QPalette::Base, value: readKdeSetting(s: KdeSetting::ViewBackground, kdeDirs, kdeVersion, kdeSettings));
977 kdeColor(pal, role: QPalette::Highlight, value: readKdeSetting(s: KdeSetting::SelectionBackground, kdeDirs, kdeVersion, kdeSettings));
978 kdeColor(pal, role: QPalette::HighlightedText, value: readKdeSetting(s: KdeSetting::SelectionForeground, kdeDirs, kdeVersion, kdeSettings));
979 kdeColor(pal, role: QPalette::AlternateBase, value: readKdeSetting(s: KdeSetting::ViewBackgroundAlternate, kdeDirs, kdeVersion, kdeSettings));
980 kdeColor(pal, role: QPalette::ButtonText, value: readKdeSetting(s: KdeSetting::ButtonForeground, kdeDirs, kdeVersion, kdeSettings));
981 kdeColor(pal, role: QPalette::Link, value: readKdeSetting(s: KdeSetting::ViewForegroundLink, kdeDirs, kdeVersion, kdeSettings));
982 kdeColor(pal, role: QPalette::LinkVisited, value: readKdeSetting(s: KdeSetting::ViewForegroundVisited, kdeDirs, kdeVersion, kdeSettings));
983 kdeColor(pal, role: QPalette::ToolTipBase, value: readKdeSetting(s: KdeSetting::TooltipBackground, kdeDirs, kdeVersion, kdeSettings));
984 kdeColor(pal, role: QPalette::ToolTipText, value: readKdeSetting(s: KdeSetting::TooltipForeground, kdeDirs, kdeVersion, kdeSettings));
985
986 // The above code sets _all_ color roles to "normal" colors. In KDE, the disabled
987 // color roles are calculated by applying various effects described in kdeglobals.
988 // We use a bit simpler approach here, similar logic than in qt_palette_from_color().
989 const QColor button = pal->color(cr: QPalette::Button);
990 int h, s, v;
991 button.getHsv(h: &h, s: &s, v: &v);
992
993 const QBrush whiteBrush = QBrush(Qt::white);
994 const QBrush buttonBrush = QBrush(button);
995 const QBrush buttonBrushDark = QBrush(button.darker(f: v > 128 ? 200 : 50));
996 const QBrush buttonBrushDark150 = QBrush(button.darker(f: v > 128 ? 150 : 75));
997 const QBrush buttonBrushLight150 = QBrush(button.lighter(f: v > 128 ? 150 : 75));
998 const QBrush buttonBrushLight = QBrush(button.lighter(f: v > 128 ? 200 : 50));
999
1000 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::WindowText, brush: buttonBrushDark);
1001 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::ButtonText, brush: buttonBrushDark);
1002 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Button, brush: buttonBrush);
1003 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Text, brush: buttonBrushDark);
1004 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::BrightText, brush: whiteBrush);
1005 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Base, brush: buttonBrush);
1006 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Window, brush: buttonBrush);
1007 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::Highlight, brush: buttonBrushDark150);
1008 pal->setBrush(cg: QPalette::Disabled, cr: QPalette::HighlightedText, brush: buttonBrushLight150);
1009
1010 // set calculated colors for all groups
1011 pal->setBrush(acr: QPalette::Light, abrush: buttonBrushLight);
1012 pal->setBrush(acr: QPalette::Midlight, abrush: buttonBrushLight150);
1013 pal->setBrush(acr: QPalette::Mid, abrush: buttonBrushDark150);
1014 pal->setBrush(acr: QPalette::Dark, abrush: buttonBrushDark);
1015}
1016
1017/*!
1018 \class QKdeTheme
1019 \brief QKdeTheme is a theme implementation for the KDE desktop (version 4 or higher).
1020 \since 5.0
1021 \internal
1022 \ingroup qpa
1023*/
1024
1025const char *QKdeTheme::name = "kde";
1026
1027QKdeTheme::QKdeTheme(const QStringList& kdeDirs, int kdeVersion)
1028 : QPlatformTheme(new QKdeThemePrivate(kdeDirs,kdeVersion))
1029{
1030 d_func()->refresh();
1031}
1032
1033QFont *QKdeThemePrivate::kdeFont(const QVariant &fontValue)
1034{
1035 if (fontValue.isValid()) {
1036 // Read font value: Might be a QStringList as KDE stores fonts without quotes.
1037 // Also retrieve the family for the constructor since we cannot use the
1038 // default constructor of QFont, which accesses QGuiApplication::systemFont()
1039 // causing recursion.
1040 QString fontDescription;
1041 QString fontFamily;
1042 if (fontValue.userType() == QMetaType::QStringList) {
1043 const QStringList list = fontValue.toStringList();
1044 if (!list.isEmpty()) {
1045 fontFamily = list.first();
1046 fontDescription = list.join(sep: u',');
1047 }
1048 } else {
1049 fontDescription = fontFamily = fontValue.toString();
1050 }
1051 if (!fontDescription.isEmpty()) {
1052 QFont font(fontFamily);
1053 if (font.fromString(fontDescription))
1054 return new QFont(font);
1055 }
1056 }
1057 return nullptr;
1058}
1059
1060
1061QStringList QKdeThemePrivate::kdeIconThemeSearchPaths(const QStringList &kdeDirs)
1062{
1063 QStringList paths = QGenericUnixTheme::xdgIconThemePaths();
1064 const QString iconPath = QStringLiteral("/share/icons");
1065 for (const QString &candidate : kdeDirs) {
1066 const QFileInfo fi(candidate + iconPath);
1067 if (fi.isDir())
1068 paths.append(t: fi.absoluteFilePath());
1069 }
1070 return paths;
1071}
1072
1073QVariant QKdeTheme::themeHint(QPlatformTheme::ThemeHint hint) const
1074{
1075 Q_D(const QKdeTheme);
1076 switch (hint) {
1077 case QPlatformTheme::UseFullScreenForPopupMenu:
1078 return QVariant(true);
1079 case QPlatformTheme::DialogButtonBoxButtonsHaveIcons:
1080 return QVariant(d->showIconsOnPushButtons);
1081 case QPlatformTheme::DialogButtonBoxLayout:
1082 return QVariant(QPlatformDialogHelper::KdeLayout);
1083 case QPlatformTheme::ToolButtonStyle:
1084 return QVariant(d->toolButtonStyle);
1085 case QPlatformTheme::ToolBarIconSize:
1086 return QVariant(d->toolBarIconSize);
1087 case QPlatformTheme::SystemIconThemeName:
1088 return QVariant(d->iconThemeName);
1089 case QPlatformTheme::SystemIconFallbackThemeName:
1090 return QVariant(d->iconFallbackThemeName);
1091 case QPlatformTheme::IconThemeSearchPaths:
1092 return QVariant(d->kdeIconThemeSearchPaths(kdeDirs: d->kdeDirs));
1093 case QPlatformTheme::IconPixmapSizes:
1094 return QVariant::fromValue(value: availableXdgFileIconSizes());
1095 case QPlatformTheme::StyleNames:
1096 return QVariant(d->styleNames);
1097 case QPlatformTheme::KeyboardScheme:
1098 return QVariant(int(KdeKeyboardScheme));
1099 case QPlatformTheme::ItemViewActivateItemOnSingleClick:
1100 return QVariant(d->singleClick);
1101 case QPlatformTheme::WheelScrollLines:
1102 return QVariant(d->wheelScrollLines);
1103 case QPlatformTheme::MouseDoubleClickInterval:
1104 return QVariant(d->doubleClickInterval);
1105 case QPlatformTheme::StartDragTime:
1106 return QVariant(d->startDragTime);
1107 case QPlatformTheme::StartDragDistance:
1108 return QVariant(d->startDragDist);
1109 case QPlatformTheme::CursorFlashTime:
1110 return QVariant(d->cursorBlinkRate);
1111 case QPlatformTheme::UiEffects:
1112 return QVariant(int(HoverEffect));
1113 case QPlatformTheme::MouseCursorTheme:
1114 return QVariant(mouseCursorTheme());
1115 case QPlatformTheme::MouseCursorSize:
1116 return QVariant(mouseCursorSize());
1117 case QPlatformTheme::PreferFileIconFromTheme:
1118 return true;
1119 default:
1120 break;
1121 }
1122 return QPlatformTheme::themeHint(hint);
1123}
1124
1125QIcon QKdeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions) const
1126{
1127#if QT_CONFIG(mimetype)
1128 return xdgFileIcon(fileInfo);
1129#else
1130 Q_UNUSED(fileInfo);
1131 return QIcon();
1132#endif
1133}
1134
1135Qt::ColorScheme QKdeTheme::colorScheme() const
1136{
1137 return d_func()->m_colorScheme;
1138}
1139
1140/*!
1141 \internal
1142 \brief QKdeTheme::updateColorScheme - guess and set a color scheme for unix themes.
1143 KDE themes do not have a color scheme property.
1144 The key words "dark" or "light" are usually part of the theme name.
1145 This is, however, not a mandatory convention.
1146
1147 If \param themeName contains a valid key word, the respective color scheme is set.
1148 If it doesn't, the color scheme is heuristically determined by comparing text and base color
1149 of the system palette.
1150 */
1151void QKdeThemePrivate::updateColorScheme(const QString &themeName)
1152{
1153 if (themeName.contains(s: QLatin1StringView("light"), cs: Qt::CaseInsensitive)) {
1154 m_colorScheme = Qt::ColorScheme::Light;
1155 return;
1156 }
1157 if (themeName.contains(s: QLatin1StringView("dark"), cs: Qt::CaseInsensitive)) {
1158 m_colorScheme = Qt::ColorScheme::Dark;
1159 return;
1160 }
1161
1162 if (systemPalette) {
1163 if (systemPalette->text().color().lightness() < systemPalette->base().color().lightness()) {
1164 m_colorScheme = Qt::ColorScheme::Light;
1165 return;
1166 }
1167 if (systemPalette->text().color().lightness() > systemPalette->base().color().lightness()) {
1168 m_colorScheme = Qt::ColorScheme::Dark;
1169 return;
1170 }
1171 }
1172
1173 m_colorScheme = Qt::ColorScheme::Unknown;
1174}
1175
1176const QPalette *QKdeTheme::palette(Palette type) const
1177{
1178 Q_D(const QKdeTheme);
1179 return d->resources.palettes[type];
1180}
1181
1182const QFont *QKdeTheme::font(Font type) const
1183{
1184 Q_D(const QKdeTheme);
1185 return d->resources.fonts[type];
1186}
1187
1188QPlatformTheme *QKdeTheme::createKdeTheme()
1189{
1190 const QByteArray kdeVersionBA = qgetenv(varName: "KDE_SESSION_VERSION");
1191 const int kdeVersion = kdeVersionBA.toInt();
1192 if (kdeVersion < 4)
1193 return nullptr;
1194
1195 if (kdeVersion > 4)
1196 // Plasma 5 follows XDG spec
1197 // but uses the same config file format:
1198 return new QKdeTheme(QStandardPaths::standardLocations(type: QStandardPaths::GenericConfigLocation), kdeVersion);
1199
1200 // Determine KDE prefixes in the following priority order:
1201 // - KDEHOME and KDEDIRS environment variables
1202 // - ~/.kde(<version>)
1203 // - read prefixes from /etc/kde<version>rc
1204 // - fallback to /etc/kde<version>
1205
1206 QStringList kdeDirs;
1207 const QString kdeHomePathVar = QFile::decodeName(localFileName: qgetenv(varName: "KDEHOME"));
1208 if (!kdeHomePathVar.isEmpty())
1209 kdeDirs += kdeHomePathVar;
1210
1211 const QString kdeDirsVar = QFile::decodeName(localFileName: qgetenv(varName: "KDEDIRS"));
1212 if (!kdeDirsVar.isEmpty())
1213 kdeDirs += kdeDirsVar.split(sep: u':', behavior: Qt::SkipEmptyParts);
1214
1215 const QString kdeVersionHomePath = QDir::homePath() + "/.kde"_L1 + QLatin1StringView(kdeVersionBA);
1216 if (QFileInfo(kdeVersionHomePath).isDir())
1217 kdeDirs += kdeVersionHomePath;
1218
1219 const QString kdeHomePath = QDir::homePath() + "/.kde"_L1;
1220 if (QFileInfo(kdeHomePath).isDir())
1221 kdeDirs += kdeHomePath;
1222
1223 const QString kdeRcPath = "/etc/kde"_L1 + QLatin1StringView(kdeVersionBA) + "rc"_L1;
1224 if (QFileInfo(kdeRcPath).isReadable()) {
1225 QSettings kdeSettings(kdeRcPath, QSettings::IniFormat);
1226 kdeSettings.beginGroup(QStringLiteral("Directories-default"));
1227 kdeDirs += kdeSettings.value(QStringLiteral("prefixes")).toStringList();
1228 }
1229
1230 const QString kdeVersionPrefix = "/etc/kde"_L1 + QLatin1StringView(kdeVersionBA);
1231 if (QFileInfo(kdeVersionPrefix).isDir())
1232 kdeDirs += kdeVersionPrefix;
1233
1234 kdeDirs.removeDuplicates();
1235 if (kdeDirs.isEmpty()) {
1236 qWarning(msg: "Unable to determine KDE dirs");
1237 return nullptr;
1238 }
1239
1240 return new QKdeTheme(kdeDirs, kdeVersion);
1241}
1242
1243#ifndef QT_NO_DBUS
1244QPlatformMenuBar *QKdeTheme::createPlatformMenuBar() const
1245{
1246 if (isDBusGlobalMenuAvailable())
1247 return new QDBusMenuBar();
1248 return nullptr;
1249}
1250#endif
1251
1252#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON)
1253QPlatformSystemTrayIcon *QKdeTheme::createPlatformSystemTrayIcon() const
1254{
1255 if (shouldUseDBusTray())
1256 return new QDBusTrayIcon();
1257 return nullptr;
1258}
1259#endif
1260
1261#endif // settings
1262
1263/*!
1264 \class QGnomeTheme
1265 \brief QGnomeTheme is a theme implementation for the Gnome desktop.
1266 \since 5.0
1267 \internal
1268 \ingroup qpa
1269*/
1270
1271const char *QGnomeTheme::name = "gnome";
1272
1273class QGnomeThemePrivate : public QPlatformThemePrivate
1274{
1275public:
1276 QGnomeThemePrivate();
1277 ~QGnomeThemePrivate();
1278
1279 void configureFonts(const QString &gtkFontName) const
1280 {
1281 Q_ASSERT(!systemFont);
1282 const int split = gtkFontName.lastIndexOf(c: QChar::Space);
1283 float size = QStringView{gtkFontName}.mid(pos: split + 1).toFloat();
1284 QString fontName = gtkFontName.left(n: split);
1285
1286 systemFont = new QFont(fontName, size);
1287 fixedFont = new QFont(QLatin1StringView(defaultFixedFontNameC), systemFont->pointSize());
1288 fixedFont->setStyleHint(QFont::TypeWriter);
1289 qCDebug(lcQpaFonts) << "default fonts: system" << systemFont << "fixed" << fixedFont;
1290 }
1291
1292 mutable QFont *systemFont = nullptr;
1293 mutable QFont *fixedFont = nullptr;
1294
1295#ifndef QT_NO_DBUS
1296 Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown;
1297private:
1298 std::unique_ptr<QGenericUnixThemeDBusListener> dbus;
1299 bool initDbus();
1300 void updateColorScheme(const QString &themeName);
1301#endif // QT_NO_DBUS
1302};
1303
1304QGnomeThemePrivate::QGnomeThemePrivate()
1305{
1306#ifndef QT_NO_DBUS
1307 initDbus();
1308#endif // QT_NO_DBUS
1309}
1310QGnomeThemePrivate::~QGnomeThemePrivate()
1311{
1312 if (systemFont)
1313 delete systemFont;
1314 if (fixedFont)
1315 delete fixedFont;
1316}
1317
1318#ifndef QT_NO_DBUS
1319bool QGnomeThemePrivate::initDbus()
1320{
1321 dbus.reset(p: new QGenericUnixThemeDBusListener());
1322 Q_ASSERT(dbus);
1323
1324 // Wrap slot in a lambda to avoid inheriting QGnomeThemePrivate from QObject
1325 auto wrapper = [this](QGenericUnixThemeDBusListener::Provider provider,
1326 QGenericUnixThemeDBusListener::Setting setting,
1327 const QString &value) {
1328 if (provider != QGenericUnixThemeDBusListener::Provider::Gnome
1329 && provider != QGenericUnixThemeDBusListener::Provider::Gtk) {
1330 return;
1331 }
1332
1333 if (setting == QGenericUnixThemeDBusListener::Setting::Theme)
1334 updateColorScheme(themeName: value);
1335 };
1336
1337 return QObject::connect(sender: dbus.get(), signal: &QGenericUnixThemeDBusListener::settingChanged, context: dbus.get(), slot&: wrapper);
1338}
1339
1340void QGnomeThemePrivate::updateColorScheme(const QString &themeName)
1341{
1342 const auto oldColorScheme = m_colorScheme;
1343 if (themeName.contains(s: QLatin1StringView("light"), cs: Qt::CaseInsensitive)) {
1344 m_colorScheme = Qt::ColorScheme::Light;
1345 } else if (themeName.contains(s: QLatin1StringView("dark"), cs: Qt::CaseInsensitive)) {
1346 m_colorScheme = Qt::ColorScheme::Dark;
1347 } else {
1348 m_colorScheme = Qt::ColorScheme::Unknown;
1349 }
1350
1351 if (oldColorScheme != m_colorScheme)
1352 QWindowSystemInterface::handleThemeChange();
1353}
1354#endif // QT_NO_DBUS
1355
1356QGnomeTheme::QGnomeTheme()
1357 : QPlatformTheme(new QGnomeThemePrivate())
1358{
1359}
1360
1361QVariant QGnomeTheme::themeHint(QPlatformTheme::ThemeHint hint) const
1362{
1363 switch (hint) {
1364 case QPlatformTheme::DialogButtonBoxButtonsHaveIcons:
1365 return QVariant(true);
1366 case QPlatformTheme::DialogButtonBoxLayout:
1367 return QVariant(QPlatformDialogHelper::GnomeLayout);
1368 case QPlatformTheme::SystemIconThemeName:
1369 return QVariant(QStringLiteral("Adwaita"));
1370 case QPlatformTheme::SystemIconFallbackThemeName:
1371 return QVariant(QStringLiteral("gnome"));
1372 case QPlatformTheme::IconThemeSearchPaths:
1373 return QVariant(QGenericUnixTheme::xdgIconThemePaths());
1374 case QPlatformTheme::IconPixmapSizes:
1375 return QVariant::fromValue(value: availableXdgFileIconSizes());
1376 case QPlatformTheme::StyleNames: {
1377 QStringList styleNames;
1378 styleNames << QStringLiteral("Fusion") << QStringLiteral("windows");
1379 return QVariant(styleNames);
1380 }
1381 case QPlatformTheme::KeyboardScheme:
1382 return QVariant(int(GnomeKeyboardScheme));
1383 case QPlatformTheme::PasswordMaskCharacter:
1384 return QVariant(QChar(0x2022));
1385 case QPlatformTheme::UiEffects:
1386 return QVariant(int(HoverEffect));
1387 case QPlatformTheme::ButtonPressKeys:
1388 return QVariant::fromValue(
1389 value: QList<Qt::Key>({ Qt::Key_Space, Qt::Key_Return, Qt::Key_Enter, Qt::Key_Select }));
1390 case QPlatformTheme::PreselectFirstFileInDirectory:
1391 return true;
1392 case QPlatformTheme::MouseCursorTheme:
1393 return QVariant(mouseCursorTheme());
1394 case QPlatformTheme::MouseCursorSize:
1395 return QVariant(mouseCursorSize());
1396 case QPlatformTheme::PreferFileIconFromTheme:
1397 return true;
1398 default:
1399 break;
1400 }
1401 return QPlatformTheme::themeHint(hint);
1402}
1403
1404QIcon QGnomeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions) const
1405{
1406#if QT_CONFIG(mimetype)
1407 return xdgFileIcon(fileInfo);
1408#else
1409 Q_UNUSED(fileInfo);
1410 return QIcon();
1411#endif
1412}
1413
1414const QFont *QGnomeTheme::font(Font type) const
1415{
1416 Q_D(const QGnomeTheme);
1417 if (!d->systemFont)
1418 d->configureFonts(gtkFontName: gtkFontName());
1419 switch (type) {
1420 case QPlatformTheme::SystemFont:
1421 return d->systemFont;
1422 case QPlatformTheme::FixedFont:
1423 return d->fixedFont;
1424 default:
1425 return nullptr;
1426 }
1427}
1428
1429QString QGnomeTheme::gtkFontName() const
1430{
1431 return QStringLiteral("%1 %2").arg(a: QLatin1StringView(defaultSystemFontNameC)).arg(a: defaultSystemFontSize);
1432}
1433
1434#ifndef QT_NO_DBUS
1435QPlatformMenuBar *QGnomeTheme::createPlatformMenuBar() const
1436{
1437 if (isDBusGlobalMenuAvailable())
1438 return new QDBusMenuBar();
1439 return nullptr;
1440}
1441
1442Qt::ColorScheme QGnomeTheme::colorScheme() const
1443{
1444 return d_func()->m_colorScheme;
1445}
1446
1447#endif
1448
1449#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON)
1450QPlatformSystemTrayIcon *QGnomeTheme::createPlatformSystemTrayIcon() const
1451{
1452 if (shouldUseDBusTray())
1453 return new QDBusTrayIcon();
1454 return nullptr;
1455}
1456#endif
1457
1458QString QGnomeTheme::standardButtonText(int button) const
1459{
1460 switch (button) {
1461 case QPlatformDialogHelper::Ok:
1462 return QCoreApplication::translate(context: "QGnomeTheme", key: "&OK");
1463 case QPlatformDialogHelper::Save:
1464 return QCoreApplication::translate(context: "QGnomeTheme", key: "&Save");
1465 case QPlatformDialogHelper::Cancel:
1466 return QCoreApplication::translate(context: "QGnomeTheme", key: "&Cancel");
1467 case QPlatformDialogHelper::Close:
1468 return QCoreApplication::translate(context: "QGnomeTheme", key: "&Close");
1469 case QPlatformDialogHelper::Discard:
1470 return QCoreApplication::translate(context: "QGnomeTheme", key: "Close without Saving");
1471 default:
1472 break;
1473 }
1474 return QPlatformTheme::standardButtonText(button);
1475}
1476
1477/*!
1478 \brief Creates a UNIX theme according to the detected desktop environment.
1479*/
1480
1481QPlatformTheme *QGenericUnixTheme::createUnixTheme(const QString &name)
1482{
1483 if (name == QLatin1StringView(QGenericUnixTheme::name))
1484 return new QGenericUnixTheme;
1485#if QT_CONFIG(settings)
1486 if (name == QLatin1StringView(QKdeTheme::name))
1487 if (QPlatformTheme *kdeTheme = QKdeTheme::createKdeTheme())
1488 return kdeTheme;
1489#endif
1490 if (name == QLatin1StringView(QGnomeTheme::name))
1491 return new QGnomeTheme;
1492 return nullptr;
1493}
1494
1495QStringList QGenericUnixTheme::themeNames()
1496{
1497 QStringList result;
1498 if (QGuiApplication::desktopSettingsAware()) {
1499 const QByteArray desktopEnvironment = QGuiApplicationPrivate::platformIntegration()->services()->desktopEnvironment();
1500 QList<QByteArray> gtkBasedEnvironments;
1501 gtkBasedEnvironments << "GNOME"
1502 << "X-CINNAMON"
1503 << "PANTHEON"
1504 << "UNITY"
1505 << "MATE"
1506 << "XFCE"
1507 << "LXDE";
1508 const QList<QByteArray> desktopNames = desktopEnvironment.split(sep: ':');
1509 for (const QByteArray &desktopName : desktopNames) {
1510 if (desktopEnvironment == "KDE") {
1511#if QT_CONFIG(settings)
1512 result.push_back(t: QLatin1StringView(QKdeTheme::name));
1513#endif
1514 } else if (gtkBasedEnvironments.contains(t: desktopName)) {
1515 // prefer the GTK3 theme implementation with native dialogs etc.
1516 result.push_back(QStringLiteral("gtk3"));
1517 // fallback to the generic Gnome theme if loading the GTK3 theme fails
1518 result.push_back(t: QLatin1StringView(QGnomeTheme::name));
1519 } else {
1520 // unknown, but lowercase the name (our standard practice) and
1521 // remove any "x-" prefix
1522 QString s = QString::fromLatin1(ba: desktopName.toLower());
1523 result.push_back(t: s.startsWith(s: "x-"_L1) ? s.mid(position: 2) : s);
1524 }
1525 }
1526 } // desktopSettingsAware
1527 result.append(t: QLatin1StringView(QGenericUnixTheme::name));
1528 return result;
1529}
1530
1531QT_END_NAMESPACE
1532
1533#ifndef QT_NO_DBUS
1534#include "qgenericunixthemes.moc"
1535#endif // QT_NO_DBUS
1536

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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