| 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 | |
| 17 | QT_BEGIN_NAMESPACE |
| 18 | |
| 19 | using namespace Qt::StringLiterals; |
| 20 | |
| 21 | const char *QGtk3Theme::name = "gtk3" ; |
| 22 | |
| 23 | template <typename T> |
| 24 | static 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 | |
| 32 | static 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 | |
| 40 | void 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 | |
| 54 | QGtk3Theme::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-time" ); |
| 92 | SETTING_CONNECT("gtk-double-click-distance" ); |
| 93 | SETTING_CONNECT("gtk-double-click-time" ); |
| 94 | SETTING_CONNECT("gtk-long-press-time" ); |
| 95 | SETTING_CONNECT("gtk-entry-password-hint-timeout" ); |
| 96 | SETTING_CONNECT("gtk-dnd-drag-threshold" ); |
| 97 | SETTING_CONNECT("gtk-icon-theme-name" ); |
| 98 | SETTING_CONNECT("gtk-fallback-icon-theme" ); |
| 99 | SETTING_CONNECT("gtk-font-name" ); |
| 100 | SETTING_CONNECT("gtk-application-prefer-dark-theme" ); |
| 101 | SETTING_CONNECT("gtk-theme-name" ); |
| 102 | SETTING_CONNECT("gtk-cursor-theme-name" ); |
| 103 | SETTING_CONNECT("gtk-cursor-theme-size" ); |
| 104 | #undef SETTING_CONNECT |
| 105 | |
| 106 | m_storage.reset(p: new QGtk3Storage); |
| 107 | } |
| 108 | |
| 109 | static inline QVariant gtkGetLongPressTime() |
| 110 | { |
| 111 | const char *gtk_long_press_time = "gtk-long-press-time" ; |
| 112 | static bool found = g_object_class_find_property(G_OBJECT_GET_CLASS(gtk_settings_get_default()), property_name: gtk_long_press_time); |
| 113 | if (!found) |
| 114 | return QVariant(); |
| 115 | return QVariant(gtkSetting<guint>(propertyName: gtk_long_press_time)); // Since 3.14, apparently we support >= 3.6 |
| 116 | } |
| 117 | |
| 118 | QVariant QGtk3Theme::themeHint(QPlatformTheme::ThemeHint hint) const |
| 119 | { |
| 120 | switch (hint) { |
| 121 | case QPlatformTheme::CursorFlashTime: |
| 122 | return QVariant(gtkSetting<gint>(propertyName: "gtk-cursor-blink-time" )); |
| 123 | case QPlatformTheme::MouseDoubleClickDistance: |
| 124 | return QVariant(gtkSetting<gint>(propertyName: "gtk-double-click-distance" )); |
| 125 | case QPlatformTheme::MouseDoubleClickInterval: |
| 126 | return QVariant(gtkSetting<gint>(propertyName: "gtk-double-click-time" )); |
| 127 | case QPlatformTheme::MousePressAndHoldInterval: { |
| 128 | QVariant v = gtkGetLongPressTime(); |
| 129 | if (!v.isValid()) |
| 130 | v = QGnomeTheme::themeHint(hint); |
| 131 | return v; |
| 132 | } |
| 133 | case QPlatformTheme::PasswordMaskDelay: |
| 134 | return QVariant(gtkSetting<guint>(propertyName: "gtk-entry-password-hint-timeout" )); |
| 135 | case QPlatformTheme::StartDragDistance: |
| 136 | return QVariant(gtkSetting<gint>(propertyName: "gtk-dnd-drag-threshold" )); |
| 137 | case QPlatformTheme::SystemIconThemeName: |
| 138 | return QVariant(gtkSetting(propertyName: "gtk-icon-theme-name" )); |
| 139 | case QPlatformTheme::SystemIconFallbackThemeName: |
| 140 | return QVariant(gtkSetting(propertyName: "gtk-fallback-icon-theme" )); |
| 141 | case QPlatformTheme::MouseCursorTheme: |
| 142 | return QVariant(gtkSetting(propertyName: "gtk-cursor-theme-name" )); |
| 143 | case QPlatformTheme::MouseCursorSize: { |
| 144 | int s = gtkSetting<gint>(propertyName: "gtk-cursor-theme-size" ); |
| 145 | if (s > 0) |
| 146 | return QVariant(QSize(s, s)); |
| 147 | return QGnomeTheme::themeHint(hint); |
| 148 | } |
| 149 | default: |
| 150 | return QGnomeTheme::themeHint(hint); |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | QString QGtk3Theme::gtkFontName() const |
| 155 | { |
| 156 | QString cfgFontName = gtkSetting(propertyName: "gtk-font-name" ); |
| 157 | if (!cfgFontName.isEmpty()) |
| 158 | return cfgFontName; |
| 159 | return QGnomeTheme::gtkFontName(); |
| 160 | } |
| 161 | |
| 162 | Qt::ColorScheme QGtk3Theme::colorScheme() const |
| 163 | { |
| 164 | Q_ASSERT(m_storage); |
| 165 | return m_storage->colorScheme(); |
| 166 | } |
| 167 | |
| 168 | bool QGtk3Theme::usePlatformNativeDialog(DialogType type) const |
| 169 | { |
| 170 | switch (type) { |
| 171 | case ColorDialog: |
| 172 | return true; |
| 173 | case FileDialog: |
| 174 | return useNativeFileDialog(); |
| 175 | case FontDialog: |
| 176 | return true; |
| 177 | default: |
| 178 | return false; |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | QPlatformDialogHelper *QGtk3Theme::createPlatformDialogHelper(DialogType type) const |
| 183 | { |
| 184 | switch (type) { |
| 185 | case ColorDialog: |
| 186 | return new QGtk3ColorDialogHelper; |
| 187 | case FileDialog: |
| 188 | if (!useNativeFileDialog()) |
| 189 | return nullptr; |
| 190 | return new QGtk3FileDialogHelper; |
| 191 | case FontDialog: |
| 192 | return new QGtk3FontDialogHelper; |
| 193 | default: |
| 194 | return nullptr; |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | bool QGtk3Theme::useNativeFileDialog() |
| 199 | { |
| 200 | /* Require GTK3 >= 3.15.5 to avoid running into this bug: |
| 201 | * https://bugzilla.gnome.org/show_bug.cgi?id=725164 |
| 202 | * |
| 203 | * While this bug only occurs when using widget-based file dialogs |
| 204 | * (native GTK3 dialogs are fine) we have to disable platform file |
| 205 | * dialogs entirely since we can't avoid creation of a platform |
| 206 | * dialog helper. |
| 207 | */ |
| 208 | return gtk_check_version(required_major: 3, required_minor: 15, required_micro: 5) == nullptr; |
| 209 | } |
| 210 | |
| 211 | const QPalette *QGtk3Theme::palette(Palette type) const |
| 212 | { |
| 213 | Q_ASSERT(m_storage); |
| 214 | return m_storage->palette(type); |
| 215 | } |
| 216 | |
| 217 | QPixmap QGtk3Theme::standardPixmap(StandardPixmap sp, const QSizeF &size) const |
| 218 | { |
| 219 | Q_ASSERT(m_storage); |
| 220 | return m_storage->standardPixmap(standardPixmap: sp, size); |
| 221 | } |
| 222 | |
| 223 | const QFont *QGtk3Theme::font(Font type) const |
| 224 | { |
| 225 | Q_ASSERT(m_storage); |
| 226 | return m_storage->font(type); |
| 227 | } |
| 228 | |
| 229 | QIcon QGtk3Theme::fileIcon(const QFileInfo &fileInfo, |
| 230 | QPlatformTheme::IconOptions iconOptions) const |
| 231 | { |
| 232 | Q_UNUSED(iconOptions); |
| 233 | Q_ASSERT(m_storage); |
| 234 | return m_storage->fileIcon(fileInfo); |
| 235 | } |
| 236 | |
| 237 | QT_END_NAMESPACE |
| 238 | |