1// Copyright (C) 2016 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 "qgtk3theme.h"
5#include "qgtk3dialoghelpers.h"
6#include <QVariant>
7#include <QGuiApplication>
8#include <qpa/qwindowsysteminterface.h>
9
10#undef signals
11#include <gtk/gtk.h>
12
13#if QT_CONFIG(xcb_xlib)
14#include <X11/Xlib.h>
15#endif
16
17QT_BEGIN_NAMESPACE
18
19using namespace Qt::StringLiterals;
20
21const char *QGtk3Theme::name = "gtk3";
22
23template <typename T>
24static T gtkSetting(const gchar *propertyName)
25{
26 GtkSettings *settings = gtk_settings_get_default();
27 T value;
28 g_object_get(settings, propertyName, &value, NULL);
29 return value;
30}
31
32static QString gtkSetting(const gchar *propertyName)
33{
34 gchararray value = gtkSetting<gchararray>(propertyName);
35 QString str = QString::fromUtf8(utf8: value);
36 g_free(mem: value);
37 return str;
38}
39
40void gtkMessageHandler(const gchar *log_domain,
41 GLogLevelFlags log_level,
42 const gchar *message,
43 gpointer unused_data) {
44 /* Silence false-positive Gtk warnings (we are using Xlib to set
45 * the WM_TRANSIENT_FOR hint).
46 */
47 if (g_strcmp0(str1: message, str2: "GtkDialog mapped without a transient parent. "
48 "This is discouraged.") != 0) {
49 /* For other messages, call the default handler. */
50 g_log_default_handler(log_domain, log_level, message, unused_data);
51 }
52}
53
54QGtk3Theme::QGtk3Theme()
55{
56 // Ensure gtk uses the same windowing system, but let it
57 // fallback in case GDK_BACKEND environment variable
58 // filters the preferred one out
59 if (QGuiApplication::platformName().startsWith(s: "wayland"_L1))
60 gdk_set_allowed_backends(backends: "wayland,x11");
61 else if (QGuiApplication::platformName() == "xcb"_L1)
62 gdk_set_allowed_backends(backends: "x11,wayland");
63
64#if QT_CONFIG(xcb_xlib)
65 // gtk_init will reset the Xlib error handler, and that causes
66 // Qt applications to quit on X errors. Therefore, we need to manually restore it.
67 int (*oldErrorHandler)(Display *, XErrorEvent *) = XSetErrorHandler(nullptr);
68#endif
69
70 gtk_init(argc: nullptr, argv: nullptr);
71
72#if QT_CONFIG(xcb_xlib)
73 XSetErrorHandler(oldErrorHandler);
74#endif
75
76 /* Initialize some types here so that Gtk+ does not crash when reading
77 * the treemodel for GtkFontChooser.
78 */
79 g_type_ensure(PANGO_TYPE_FONT_FAMILY);
80 g_type_ensure(PANGO_TYPE_FONT_FACE);
81
82 /* Use our custom log handler. */
83 g_log_set_handler(log_domain: "Gtk", log_levels: G_LOG_LEVEL_MESSAGE, log_func: gtkMessageHandler, user_data: nullptr);
84
85#define SETTING_CONNECT(setting) g_signal_connect(settings, "notify::" setting, G_CALLBACK(notifyThemeChanged), nullptr)
86 auto notifyThemeChanged = [] {
87 QWindowSystemInterface::handleThemeChange();
88 };
89
90 GtkSettings *settings = gtk_settings_get_default();
91 SETTING_CONNECT("gtk-cursor-blink");
92 SETTING_CONNECT("gtk-cursor-blink-time");
93 SETTING_CONNECT("gtk-double-click-distance");
94 SETTING_CONNECT("gtk-double-click-time");
95 SETTING_CONNECT("gtk-long-press-time");
96 SETTING_CONNECT("gtk-entry-password-hint-timeout");
97 SETTING_CONNECT("gtk-dnd-drag-threshold");
98 SETTING_CONNECT("gtk-icon-theme-name");
99 SETTING_CONNECT("gtk-fallback-icon-theme");
100 SETTING_CONNECT("gtk-font-name");
101 SETTING_CONNECT("gtk-application-prefer-dark-theme");
102 SETTING_CONNECT("gtk-theme-name");
103 SETTING_CONNECT("gtk-cursor-theme-name");
104 SETTING_CONNECT("gtk-cursor-theme-size");
105#undef SETTING_CONNECT
106
107 m_storage.reset(p: new QGtk3Storage);
108}
109
110static inline QVariant gtkGetLongPressTime()
111{
112 const char *gtk_long_press_time = "gtk-long-press-time";
113 static bool found = g_object_class_find_property(G_OBJECT_GET_CLASS(gtk_settings_get_default()), property_name: gtk_long_press_time);
114 if (!found)
115 return QVariant();
116 return QVariant(gtkSetting<guint>(propertyName: gtk_long_press_time)); // Since 3.14, apparently we support >= 3.6
117}
118
119QVariant QGtk3Theme::themeHint(QPlatformTheme::ThemeHint hint) const
120{
121 switch (hint) {
122 case QPlatformTheme::CursorFlashTime:
123 if (gtkSetting<gboolean>(propertyName: "gtk-cursor-blink"))
124 return QVariant(gtkSetting<gint>(propertyName: "gtk-cursor-blink-time"));
125 else
126 return 0;
127 case QPlatformTheme::MouseDoubleClickDistance:
128 return QVariant(gtkSetting<gint>(propertyName: "gtk-double-click-distance"));
129 case QPlatformTheme::MouseDoubleClickInterval:
130 return QVariant(gtkSetting<gint>(propertyName: "gtk-double-click-time"));
131 case QPlatformTheme::MousePressAndHoldInterval: {
132 QVariant v = gtkGetLongPressTime();
133 if (!v.isValid())
134 v = QGnomeTheme::themeHint(hint);
135 return v;
136 }
137 case QPlatformTheme::PasswordMaskDelay:
138 return QVariant(gtkSetting<guint>(propertyName: "gtk-entry-password-hint-timeout"));
139 case QPlatformTheme::StartDragDistance:
140 return QVariant(gtkSetting<gint>(propertyName: "gtk-dnd-drag-threshold"));
141 case QPlatformTheme::SystemIconThemeName:
142 return QVariant(gtkSetting(propertyName: "gtk-icon-theme-name"));
143 case QPlatformTheme::SystemIconFallbackThemeName:
144 return QVariant(gtkSetting(propertyName: "gtk-fallback-icon-theme"));
145 case QPlatformTheme::MouseCursorTheme:
146 return QVariant(gtkSetting(propertyName: "gtk-cursor-theme-name"));
147 case QPlatformTheme::MouseCursorSize: {
148 int s = gtkSetting<gint>(propertyName: "gtk-cursor-theme-size");
149 if (s > 0)
150 return QVariant(QSize(s, s));
151 return QGnomeTheme::themeHint(hint);
152 }
153 default:
154 return QGnomeTheme::themeHint(hint);
155 }
156}
157
158QString QGtk3Theme::gtkFontName() const
159{
160 QString cfgFontName = gtkSetting(propertyName: "gtk-font-name");
161 if (!cfgFontName.isEmpty())
162 return cfgFontName;
163 return QGnomeTheme::gtkFontName();
164}
165
166Qt::ColorScheme QGtk3Theme::colorScheme() const
167{
168 Q_ASSERT(m_storage);
169
170 Q_D(const QGnomeTheme);
171 const Qt::ColorScheme colorScheme = d->colorScheme();
172 const bool hasRequestedColorScheme = d->hasRequestedColorScheme();
173
174#ifdef QT_DEBUG
175 if (hasRequestedColorScheme && colorScheme != m_storage->colorScheme()) {
176 qCDebug(lcQGtk3Interface) << "Requested color scheme" << colorScheme
177 << "differs from theme color scheme" << m_storage->colorScheme();
178 }
179#endif
180
181 return hasRequestedColorScheme ? colorScheme : m_storage->colorScheme();
182}
183
184void QGtk3Theme::requestColorScheme(Qt::ColorScheme scheme)
185{
186 const Qt::ColorScheme oldColorScheme = colorScheme();
187 QGnomeTheme::requestColorScheme(scheme);
188 if (oldColorScheme == colorScheme())
189 return;
190 qCDebug(lcQGtk3Interface) << scheme << "has been requested. Theme supports color scheme:"
191 << m_storage->colorScheme();
192 m_storage->handleThemeChange();
193 QWindowSystemInterface::sendWindowSystemEvents(flags: QEventLoop::AllEvents);
194}
195
196bool QGtk3Theme::usePlatformNativeDialog(DialogType type) const
197{
198 switch (type) {
199 case ColorDialog:
200 return true;
201 case FileDialog:
202 return useNativeFileDialog();
203 case FontDialog:
204 return true;
205 default:
206 return false;
207 }
208}
209
210QPlatformDialogHelper *QGtk3Theme::createPlatformDialogHelper(DialogType type) const
211{
212 switch (type) {
213 case ColorDialog:
214 return new QGtk3ColorDialogHelper;
215 case FileDialog:
216 if (!useNativeFileDialog())
217 return nullptr;
218 return new QGtk3FileDialogHelper;
219 case FontDialog:
220 return new QGtk3FontDialogHelper;
221 default:
222 return nullptr;
223 }
224}
225
226bool QGtk3Theme::useNativeFileDialog()
227{
228 /* Require GTK3 >= 3.15.5 to avoid running into this bug:
229 * https://bugzilla.gnome.org/show_bug.cgi?id=725164
230 *
231 * While this bug only occurs when using widget-based file dialogs
232 * (native GTK3 dialogs are fine) we have to disable platform file
233 * dialogs entirely since we can't avoid creation of a platform
234 * dialog helper.
235 */
236 return gtk_check_version(required_major: 3, required_minor: 15, required_micro: 5) == nullptr;
237}
238
239const QPalette *QGtk3Theme::palette(Palette type) const
240{
241 Q_ASSERT(m_storage);
242
243 Q_D(const QGnomeTheme);
244 const Qt::ColorScheme colorScheme = d->colorScheme();
245 const bool hasRequestedColorScheme = d->hasRequestedColorScheme();
246
247#ifdef QT_DEBUG
248 if (hasRequestedColorScheme && colorScheme != m_storage->colorScheme()) {
249 qCDebug(lcQGtk3Interface) << "Current KDE theme doesn't support requested color scheme"
250 << colorScheme << "Falling back to fusion palette.";
251 return QPlatformTheme::palette(type);
252 }
253#endif
254
255 return (hasRequestedColorScheme && colorScheme != m_storage->colorScheme())
256 ? QPlatformTheme::palette(type)
257 : m_storage->palette(type);
258}
259
260QPixmap QGtk3Theme::standardPixmap(StandardPixmap sp, const QSizeF &size) const
261{
262 Q_ASSERT(m_storage);
263 return m_storage->standardPixmap(standardPixmap: sp, size);
264}
265
266const QFont *QGtk3Theme::font(Font type) const
267{
268 Q_ASSERT(m_storage);
269 return m_storage->font(type);
270}
271
272QIcon QGtk3Theme::fileIcon(const QFileInfo &fileInfo,
273 QPlatformTheme::IconOptions iconOptions) const
274{
275 Q_UNUSED(iconOptions);
276 Q_ASSERT(m_storage);
277 return m_storage->fileIcon(fileInfo);
278}
279
280#if QT_CONFIG(dbus)
281void QGtk3Theme::updateColorScheme(Qt::ColorScheme newColorScheme)
282{
283 if (newColorScheme == colorScheme())
284 QGnomeTheme::updateColorScheme(newColorScheme);
285 else
286 m_storage->handleThemeChange();
287}
288#endif // QT_CONFIG(dbus)
289
290QT_END_NAMESPACE
291

source code of qtbase/src/plugins/platformthemes/gtk3/qgtk3theme.cpp