From ad8b47bff734d68f5668e39f2aaecb9d001b008d Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Mon, 16 Oct 2023 16:43:17 +0000 Subject: [PATCH] gui: Change Windows title bar based on theme On Windows, the title bar can be set to light or dark mode via DwmSetWindowAttribute (). This adds code to update the main title bar and dialogue title bars based on the current theme. The main title bar uses "prefer-dark-theme", while the dialogue title bars uses the color of the widget background to assume the correct color. --- app/dialogs/about-dialog.c | 32 ++++++---- app/dialogs/about-dialog.h | 3 +- app/dialogs/dialogs-constructors.c | 2 +- app/gimp-update.c | 10 +++- app/gui/gui.c | 6 +- app/gui/themes.c | 31 ++++++++++ app/gui/themes.h | 1 + app/widgets/gimpfiledialog.c | 16 +++++ app/widgets/gimpwidgets-utils.c | 63 ++++++++++++++++++++ app/widgets/gimpwidgets-utils.h | 4 ++ libgimpwidgets/gimpdialog.c | 96 ++++++++++++++++++++++++------ 11 files changed, 230 insertions(+), 34 deletions(-) diff --git a/app/dialogs/about-dialog.c b/app/dialogs/about-dialog.c index 74cba08ae0..e7ff0a7b38 100644 --- a/app/dialogs/about-dialog.c +++ b/app/dialogs/about-dialog.c @@ -30,6 +30,8 @@ #include "config/gimpcoreconfig.h" +#include "widgets/gimpwidgets-utils.h" + #include "about.h" #include "git-version.h" @@ -50,23 +52,25 @@ typedef struct { - GtkWidget *dialog; + GtkWidget *dialog; + + Gimp *gimp; GtkWidget *update_frame; GimpCoreConfig *config; - GtkWidget *anim_area; - PangoLayout *layout; + GtkWidget *anim_area; + PangoLayout *layout; - gint n_authors; - gint shuffle[G_N_ELEMENTS (authors) - 1]; /* NULL terminated */ + gint n_authors; + gint shuffle[G_N_ELEMENTS (authors) - 1]; /* NULL terminated */ - guint timer; + guint timer; - gint index; - gint animstep; - gint state; - gboolean visible; + gint index; + gint animstep; + gint state; + gboolean visible; } GimpAboutDialog; @@ -99,7 +103,8 @@ static void about_dialog_download_clicked const gchar *link); GtkWidget * -about_dialog_create (GimpCoreConfig *config) +about_dialog_create (Gimp *gimp, + GimpCoreConfig *config) { static GimpAboutDialog dialog; @@ -112,6 +117,7 @@ about_dialog_create (GimpCoreConfig *config) gchar *copyright; gchar *version; + dialog.gimp = gimp; dialog.n_authors = G_N_ELEMENTS (authors) - 1; dialog.config = config; @@ -206,6 +212,10 @@ about_dialog_map (GtkWidget *widget, dialog->timer = g_timeout_add (800, about_dialog_timer, dialog); } + +#ifdef G_OS_WIN32 + gimp_window_set_title_bar_theme (dialog->gimp, widget, FALSE); +#endif } static void diff --git a/app/dialogs/about-dialog.h b/app/dialogs/about-dialog.h index 516d7da500..0a929cbe24 100644 --- a/app/dialogs/about-dialog.h +++ b/app/dialogs/about-dialog.h @@ -19,7 +19,8 @@ #define __ABOUT_DIALOG_H__ -GtkWidget * about_dialog_create (GimpCoreConfig *config); +GtkWidget * about_dialog_create (Gimp *gimp, + GimpCoreConfig *config); #endif /* __ABOUT_DIALOG_H__ */ diff --git a/app/dialogs/dialogs-constructors.c b/app/dialogs/dialogs-constructors.c index af9f72719f..99c0ffca29 100644 --- a/app/dialogs/dialogs-constructors.c +++ b/app/dialogs/dialogs-constructors.c @@ -217,7 +217,7 @@ dialogs_about_get (GimpDialogFactory *factory, GimpUIManager *ui_manager, gint view_size) { - return about_dialog_create (context->gimp->edit_config); + return about_dialog_create (context->gimp, context->gimp->edit_config); } GtkWidget * diff --git a/app/gimp-update.c b/app/gimp-update.c index 520b6d1c75..7aa289efe6 100644 --- a/app/gimp-update.c +++ b/app/gimp-update.c @@ -435,14 +435,18 @@ gimp_update_about_dialog (GimpCoreConfig *config, const GParamSpec *pspec, gpointer user_data) { +#ifndef GIMP_CONSOLE_COMPILATION + Gimp *gimp = user_data; +#endif + g_signal_handlers_disconnect_by_func (config, (GCallback) gimp_update_about_dialog, - NULL); + user_data); if (config->last_known_release != NULL) { #ifndef GIMP_CONSOLE_COMPILATION - gtk_widget_show (about_dialog_create (config)); + gtk_widget_show (about_dialog_create (gimp, config)); #else g_printerr (_("A new version of GIMP (%s) was released.\n" "It is recommended to update."), @@ -639,7 +643,7 @@ gimp_update_auto_check (GimpCoreConfig *config, g_signal_connect (config, "notify::last-known-release", (GCallback) gimp_update_about_dialog, - NULL); + gimp); gimp_update_check (config); diff --git a/app/gui/gui.c b/app/gui/gui.c index 5e34b96f41..54c086254e 100644 --- a/app/gui/gui.c +++ b/app/gui/gui.c @@ -617,6 +617,10 @@ gui_restore_after_callback (Gimp *gimp, shell = gimp_display_get_shell (display); +#ifdef G_OS_WIN32 + themes_set_title_bar (gimp); +#endif + if (gui_config->restore_session) session_restore (gimp, initial_monitor); @@ -624,7 +628,7 @@ gui_restore_after_callback (Gimp *gimp, #ifdef G_OS_WIN32 /* Prevents window from reappearing on start-up if the user - * requested it to be minimized via window hints + * requested it to be minimized via window hints */ if (StartupInfo.wShowWindow != SW_SHOWMINIMIZED && StartupInfo.wShowWindow != SW_SHOWMINNOACTIVE && diff --git a/app/gui/themes.c b/app/gui/themes.c index b45e035013..950b27a6dd 100644 --- a/app/gui/themes.c +++ b/app/gui/themes.c @@ -31,6 +31,10 @@ #include "core/gimp.h" +#include "display/gimpimagewindow.h" + +#include "widgets/gimpwidgets-utils.h" + #include "themes.h" #include "gimp-intl.h" @@ -105,6 +109,10 @@ themes_init (Gimp *gimp) gimp); themes_theme_change_notify (config, NULL, gimp); + +#ifdef G_OS_WIN32 + themes_set_title_bar (gimp); +#endif } void @@ -469,6 +477,10 @@ themes_theme_change_notify (GimpGuiConfig *config, g_object_unref (theme_css); gtk_style_context_reset_widgets (gdk_screen_get_default ()); + +#ifdef G_OS_WIN32 + themes_set_title_bar (gimp); +#endif } static void @@ -549,3 +561,22 @@ themes_theme_paths_notify (GimpExtensionManager *manager, g_list_free_full (path, (GDestroyNotify) g_object_unref); } } + +void +themes_set_title_bar (Gimp *gimp) +{ +#ifdef G_OS_WIN32 + GList *windows = gimp_get_image_windows (gimp); + GList *iter; + + for (iter = windows; iter; iter = g_list_next (iter)) + { + GtkWidget *window = GTK_WIDGET (windows->data); + + gimp_window_set_title_bar_theme (gimp, window, TRUE); + } + + if (windows) + g_list_free (windows); +#endif +} diff --git a/app/gui/themes.h b/app/gui/themes.h index 0b7e74fc76..87682ed82f 100644 --- a/app/gui/themes.h +++ b/app/gui/themes.h @@ -30,5 +30,6 @@ GFile * themes_get_theme_file (Gimp *gimp, const gchar *first_component, ...) G_GNUC_NULL_TERMINATED; +void themes_set_title_bar (Gimp *gimp); #endif /* __THEMES_H__ */ diff --git a/app/widgets/gimpfiledialog.c b/app/widgets/gimpfiledialog.c index d01d32a8a3..84cfa2b614 100644 --- a/app/widgets/gimpfiledialog.c +++ b/app/widgets/gimpfiledialog.c @@ -86,6 +86,8 @@ static gboolean gimp_file_dialog_delete_event (GtkWidget *w GdkEventAny *event); static void gimp_file_dialog_response (GtkDialog *dialog, gint response_id); +static void gimp_file_dialog_map (GimpFileDialog *dialog, + gpointer data); static GFile * gimp_file_dialog_real_get_default_folder (GimpFileDialog *dialog); static void gimp_file_dialog_real_save_state (GimpFileDialog *dialog, const gchar *state_name); @@ -225,6 +227,11 @@ gimp_file_dialog_class_init (GimpFileDialogClass *klass) static void gimp_file_dialog_init (GimpFileDialog *dialog) { +#ifdef G_OS_WIN32 + g_signal_connect (dialog, "map", + G_CALLBACK (gimp_file_dialog_map), + NULL); +#endif } static void @@ -420,6 +427,15 @@ gimp_file_dialog_response (GtkDialog *dialog, } } +static void +gimp_file_dialog_map (GimpFileDialog *dialog, + gpointer data) +{ +#ifdef G_OS_WIN32 + gimp_window_set_title_bar_theme (dialog->gimp, GTK_WIDGET (dialog), FALSE); +#endif +} + static GFile * gimp_file_dialog_real_get_default_folder (GimpFileDialog *dialog) { diff --git a/app/widgets/gimpwidgets-utils.c b/app/widgets/gimpwidgets-utils.c index dd587e2f22..e7298eaf97 100644 --- a/app/widgets/gimpwidgets-utils.c +++ b/app/widgets/gimpwidgets-utils.c @@ -26,7 +26,12 @@ #include #ifdef GDK_WINDOWING_WIN32 +#include #include + +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif #endif #ifdef GDK_WINDOWING_X11 @@ -51,6 +56,8 @@ #include "gegl/gimp-babl.h" +#include "config/gimpguiconfig.h" + #include "core/gimp.h" #include "core/gimpprogress.h" #include "core/gimptoolinfo.h" @@ -2616,3 +2623,59 @@ gimp_window_set_transient_cb (GtkWidget *window, } #endif } + +void +gimp_window_set_title_bar_theme (Gimp *gimp, + GtkWidget *dialog, + gboolean is_main_window) +{ +#ifdef G_OS_WIN32 + HWND hwnd; + GdkWindow *window = NULL; + gboolean use_dark_mode = FALSE; + + window = gtk_widget_get_window (GTK_WIDGET (dialog)); + if (window) + { + if (gimp) + { + GimpGuiConfig *config; + + config = GIMP_GUI_CONFIG (gimp->config); + use_dark_mode = config->prefer_dark_theme; + } + else + { + GtkStyleContext *style; + GdkRGBA *color = NULL; + + /* Workaround if we don't have access to GimpGuiConfig. + * If the background color is below the threshold, then we're + * likely in dark mode. + */ + style = gtk_widget_get_style_context (dialog); + gtk_style_context_get (style, gtk_style_context_get_state (style), + GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &color, + NULL); + if (color) + { + if (color->red < 0.5 && color->green < 0.5 && color->blue < 0.5) + use_dark_mode = TRUE; + + gdk_rgba_free (color); + } + } + + hwnd = (HWND) gdk_win32_window_get_handle (window); + DwmSetWindowAttribute (hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark_mode, sizeof (use_dark_mode)); + + if (! is_main_window) + { + /* Toggle the window's visibility so the title bar change appears */ + gdk_window_hide (window); + gdk_window_show (window); + } + } +#endif +} diff --git a/app/widgets/gimpwidgets-utils.h b/app/widgets/gimpwidgets-utils.h index 114bb11146..eb1bd3d3d6 100644 --- a/app/widgets/gimpwidgets-utils.h +++ b/app/widgets/gimpwidgets-utils.h @@ -165,5 +165,9 @@ gboolean gimp_utils_are_menu_path_identical (const gchar *path1 gchar **mnemonic_path1, gchar **path1_section_name); +void gimp_window_set_title_bar_theme (Gimp *gimp, + GtkWidget *dialog, + gboolean is_main_window); + #endif /* __APP_GIMP_WIDGETS_UTILS_H__ */ diff --git a/libgimpwidgets/gimpdialog.c b/libgimpwidgets/gimpdialog.c index 77f4199cf2..3f2d8eb290 100644 --- a/libgimpwidgets/gimpdialog.c +++ b/libgimpwidgets/gimpdialog.c @@ -34,6 +34,14 @@ #include "libgimp/libgimp-intl.h" +#ifdef G_OS_WIN32 +#include +#include + +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +#endif /** * SECTION: gimpdialog @@ -67,27 +75,28 @@ struct _GimpDialogPrivate #define GET_PRIVATE(obj) (((GimpDialog *) (obj))->priv) -static void gimp_dialog_constructed (GObject *object); -static void gimp_dialog_dispose (GObject *object); -static void gimp_dialog_finalize (GObject *object); -static void gimp_dialog_set_property (GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec); -static void gimp_dialog_get_property (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec); +static void gimp_dialog_constructed (GObject *object); +static void gimp_dialog_dispose (GObject *object); +static void gimp_dialog_finalize (GObject *object); +static void gimp_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); +static void gimp_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); -static void gimp_dialog_hide (GtkWidget *widget); -static gboolean gimp_dialog_delete_event (GtkWidget *widget, - GdkEventAny *event); +static void gimp_dialog_hide (GtkWidget *widget); +static gboolean gimp_dialog_delete_event (GtkWidget *widget, + GdkEventAny *event); -static void gimp_dialog_close (GtkDialog *dialog); +static void gimp_dialog_close (GtkDialog *dialog); -static void gimp_dialog_response (GtkDialog *dialog, - gint response_id); +static void gimp_dialog_response (GtkDialog *dialog, + gint response_id); +static void gimp_dialog_set_title_bar_theme (GtkWidget *dialog); G_DEFINE_TYPE_WITH_PRIVATE (GimpDialog, gimp_dialog, GTK_TYPE_DIALOG) @@ -161,6 +170,12 @@ gimp_dialog_init (GimpDialog *dialog) g_signal_connect (dialog, "response", G_CALLBACK (gimp_dialog_response), NULL); + +#ifdef G_OS_WIN32 + g_signal_connect (GTK_WIDGET (dialog), "map", + G_CALLBACK (gimp_dialog_set_title_bar_theme), + NULL); +#endif } static void @@ -656,6 +671,10 @@ gimp_dialog_run (GimpDialog *dialog) gtk_window_present (GTK_WINDOW (dialog)); +#ifdef G_OS_WIN32 + gimp_dialog_set_title_bar_theme (GTK_WIDGET (dialog)); +#endif + response_handler = g_signal_connect (dialog, "response", G_CALLBACK (run_response_handler), &ri); @@ -750,3 +769,46 @@ gimp_dialogs_show_help_button (gboolean show) { show_help_button = show ? TRUE : FALSE; } + +void +gimp_dialog_set_title_bar_theme (GtkWidget *dialog) +{ +#ifdef G_OS_WIN32 + HWND hwnd; + gboolean use_dark_mode = FALSE; + GdkWindow *window = NULL; + + window = gtk_widget_get_window (GTK_WIDGET (dialog)); + if (window) + { + GtkStyleContext *style; + GdkRGBA *color = NULL; + + hwnd = (HWND) gdk_win32_window_get_handle (window); + /* Workaround since we don't have access to GimpGuiConfig. + * If the background color is below the threshold, then we're + * likely in dark mode. + */ + style = gtk_widget_get_style_context (GTK_WIDGET (dialog)); + gtk_style_context_get (style, gtk_style_context_get_state (style), + GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &color, + NULL); + if (color) + { + if (color->red < 0.5 && color->green < 0.5 && color->blue < 0.5) + use_dark_mode = TRUE; + + gdk_rgba_free (color); + } + + DwmSetWindowAttribute (hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark_mode, sizeof (use_dark_mode)); + UpdateWindow (hwnd); + ShowWindow (hwnd, 5); + + /* Toggle the window's visibility so the title bar change appears */ + gdk_window_hide (window); + gdk_window_show (window); + } +#endif +}