From a767e8eacc5874ba0aa98d2ecf69658cd295a23f Mon Sep 17 00:00:00 2001 From: deepend-tildeclub Date: Tue, 7 Jul 2026 14:00:25 -0600 Subject: [PATCH] theming: respect system theme by default --- src/common/gtk3-theme-service.c | 164 ++++++-- src/common/tests/test-gtk3-theme-service.c | 28 +- src/fe-gtk/plugin-tray.c | 5 + .../test-theme-application-input-style.c | 18 +- .../tests/test-theme-manager-auto-refresh.c | 56 ++- .../test-theme-manager-dispatch-routing.c | 10 + .../theme/tests/test-theme-manager-policy.c | 46 ++- .../test-theme-preferences-gtk3-populate.c | 25 +- .../tests/test-theme-runtime-persistence.c | 4 +- src/fe-gtk/theme/theme-access.c | 77 +++- src/fe-gtk/theme/theme-application.c | 11 + src/fe-gtk/theme/theme-css.c | 64 +-- src/fe-gtk/theme/theme-manager.c | 108 +++++- src/fe-gtk/theme/theme-policy.c | 367 +++++++++++++++++- src/fe-gtk/theme/theme-policy.h | 4 + src/fe-gtk/theme/theme-preferences.c | 47 ++- 16 files changed, 870 insertions(+), 164 deletions(-) diff --git a/src/common/gtk3-theme-service.c b/src/common/gtk3-theme-service.c index 26bfe5cd..202eb298 100644 --- a/src/common/gtk3-theme-service.c +++ b/src/common/gtk3-theme-service.c @@ -315,6 +315,59 @@ resolve_parent_theme_root (const char *child_theme_root, const char *parent_name return home_local; g_free (home_local); + { + const char *xdg_data_home = g_getenv ("XDG_DATA_HOME"); + + if (xdg_data_home && xdg_data_home[0]) + { + candidate = g_build_filename (xdg_data_home, "themes", parent_name, NULL); + if (path_exists_as_dir (candidate)) + return candidate; + g_free (candidate); + } + } + + candidate = g_build_filename (g_get_user_data_dir (), "themes", parent_name, NULL); + if (path_exists_as_dir (candidate)) + return candidate; + g_free (candidate); + + { + const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS"); + guint i; + + if (xdg_data_dirs && xdg_data_dirs[0]) + { + char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1); + + for (i = 0; data_dirs && data_dirs[i]; i++) + { + if (!data_dirs[i][0]) + continue; + candidate = g_build_filename (data_dirs[i], "themes", parent_name, NULL); + if (path_exists_as_dir (candidate)) + { + g_strfreev (data_dirs); + return candidate; + } + g_free (candidate); + } + g_strfreev (data_dirs); + } + else + { + const char *const *system_data_dirs = g_get_system_data_dirs (); + + for (i = 0; system_data_dirs && system_data_dirs[i]; i++) + { + candidate = g_build_filename (system_data_dirs[i], "themes", parent_name, NULL); + if (path_exists_as_dir (candidate)) + return candidate; + g_free (candidate); + } + } + } + user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir (); candidate = g_build_filename (user_dir, parent_name, NULL); g_free (user_dir); @@ -632,18 +685,50 @@ zoitechat_gtk3_theme_service_discover (void) g_mkdir_with_parents (user_dir, 0700); - user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL); + /* Prefer the live XDG environment over GLib's process-cached copies so + * theme lookup matches what GTK resolves for the running session. */ + { + const char *xdg_data_home = g_getenv ("XDG_DATA_HOME"); + + if (xdg_data_home && xdg_data_home[0]) + user_data_themes = g_build_filename (xdg_data_home, "themes", NULL); + else + user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL); + } add_theme_root (user_roots, seen_user_roots, user_data_themes); g_free (user_data_themes); add_theme_root (user_roots, seen_user_roots, home_themes); add_theme_root (user_roots, seen_user_roots, user_dir); - system_data_dirs = g_get_system_data_dirs (); - for (i = 0; system_data_dirs && system_data_dirs[i]; i++) { - char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL); - add_theme_root (system_roots, seen_system_roots, system_themes); - g_free (system_themes); + const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS"); + + if (xdg_data_dirs && xdg_data_dirs[0]) + { + char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1); + + for (i = 0; data_dirs && data_dirs[i]; i++) + { + char *system_themes; + + if (!data_dirs[i][0]) + continue; + system_themes = g_build_filename (data_dirs[i], "themes", NULL); + add_theme_root (system_roots, seen_system_roots, system_themes); + g_free (system_themes); + } + g_strfreev (data_dirs); + } + else + { + system_data_dirs = g_get_system_data_dirs (); + for (i = 0; system_data_dirs && system_data_dirs[i]; i++) + { + char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL); + add_theme_root (system_roots, seen_system_roots, system_themes); + g_free (system_themes); + } + } } for (i = 0; i < system_roots->len; i++) @@ -922,7 +1007,7 @@ static gboolean validate_theme_root_for_import (const char *theme_root, GError **error) { char *index_theme; - GKeyFile *keyfile; + GKeyFile *keyfile = NULL; char *css_dir; char *css_path; char *raw_inherits; @@ -930,36 +1015,33 @@ validate_theme_root_for_import (const char *theme_root, GError **error) guint i; GError *load_error = NULL; + /* index.theme is optional for GTK3 CSS themes (GTK only needs + * gtk-3.x/gtk.css), but when it is present it must be well formed so + * broken archives are rejected up front. */ index_theme = g_build_filename (theme_root, "index.theme", NULL); - if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR)) + if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR)) { - g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, - "Invalid GTK3 theme at '%s': missing required index.theme at '%s'.", - theme_root, index_theme); - g_free (index_theme); - return FALSE; - } + keyfile = g_key_file_new (); + if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error)) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.", + theme_root, index_theme, load_error->message); + g_error_free (load_error); + g_key_file_unref (keyfile); + g_free (index_theme); + return FALSE; + } - keyfile = g_key_file_new (); - if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error)) - { - g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, - "Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.", - theme_root, index_theme, load_error->message); - g_error_free (load_error); - g_key_file_unref (keyfile); - g_free (index_theme); - return FALSE; - } - - if (!g_key_file_has_group (keyfile, "Desktop Entry")) - { - g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, - "Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.", - theme_root, index_theme); - g_key_file_unref (keyfile); - g_free (index_theme); - return FALSE; + if (!g_key_file_has_group (keyfile, "Desktop Entry")) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, + "Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.", + theme_root, index_theme); + g_key_file_unref (keyfile); + g_free (index_theme); + return FALSE; + } } css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root); @@ -968,7 +1050,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).", theme_root); - g_key_file_unref (keyfile); + if (keyfile) + g_key_file_unref (keyfile); g_free (index_theme); return FALSE; } @@ -981,15 +1064,20 @@ validate_theme_root_for_import (const char *theme_root, GError **error) theme_root, css_path); g_free (css_path); g_free (css_dir); - g_key_file_unref (keyfile); + if (keyfile) + g_key_file_unref (keyfile); g_free (index_theme); return FALSE; } g_free (css_path); g_free (css_dir); - raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL); - g_key_file_unref (keyfile); + raw_inherits = NULL; + if (keyfile) + { + raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL); + g_key_file_unref (keyfile); + } g_free (index_theme); if (!raw_inherits) return TRUE; diff --git a/src/common/tests/test-gtk3-theme-service.c b/src/common/tests/test-gtk3-theme-service.c index 887ba9a4..807e409c 100644 --- a/src/common/tests/test-gtk3-theme-service.c +++ b/src/common/tests/test-gtk3-theme-service.c @@ -381,9 +381,10 @@ test_archive_without_theme_reports_css_error (void) } archive_path = g_build_filename (tmp_root, "invalid-theme.zip", NULL); - command = g_strdup_printf ("cd %s && zip -qr %s .", archive_root, archive_path); + command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", archive_root, archive_path); g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL)); g_free (command); + g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR)); g_assert_false (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, &error)); g_assert_null (imported_id); @@ -397,25 +398,31 @@ test_archive_without_theme_reports_css_error (void) } static void -test_import_rejects_theme_missing_index_theme (void) +test_import_accepts_theme_missing_index_theme (void) { char *tmp_root; char *src_root; char *theme_root; char *imported_id = NULL; GError *error = NULL; + ZoitechatGtk3Theme *found; setup_test_xdir (&tmp_root); src_root = g_build_filename (tmp_root, "src", NULL); g_mkdir_with_parents (src_root, 0700); theme_root = make_theme_dir (src_root, "NoIndex", FALSE, FALSE); - g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error)); - g_assert_null (imported_id); - g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); - g_assert_nonnull (g_strstr_len (error->message, -1, "missing required index.theme")); - g_assert_nonnull (g_strstr_len (error->message, -1, "NoIndex")); - g_error_free (error); + /* index.theme is optional for GTK3 CSS themes: gtk-3.x/gtk.css alone + * makes a loadable theme. */ + g_assert_true (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error)); + g_assert_no_error (error); + g_assert_nonnull (imported_id); + + found = zoitechat_gtk3_theme_find_by_id (imported_id); + g_assert_nonnull (found); + g_assert_true (g_str_has_suffix (found->path, "NoIndex")); + zoitechat_gtk3_theme_free (found); + g_free (imported_id); g_free (theme_root); g_free (src_root); @@ -663,9 +670,10 @@ test_zip_import_nested_root (void) theme = make_theme_dir (nested, "Juno-ocean", TRUE, FALSE); archive_path = g_build_filename (tmp_root, "themes.zip", NULL); - command = g_strdup_printf ("cd %s && zip -qr %s .", zip_root, archive_path); + command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", zip_root, archive_path); g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL)); g_free (command); + g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR)); g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL)); found = zoitechat_gtk3_theme_find_by_id (imported_id); @@ -697,7 +705,7 @@ main (int argc, char **argv) g_test_add_func ("/gtk3-theme-service/zip-import-nested-root", test_zip_import_nested_root); g_test_add_func ("/gtk3-theme-service/invalid-archive-extract-error", test_invalid_archive_reports_extract_error); g_test_add_func ("/gtk3-theme-service/archive-without-theme-css-error", test_archive_without_theme_reports_css_error); - g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_rejects_theme_missing_index_theme); + g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_accepts_theme_missing_index_theme); g_test_add_func ("/gtk3-theme-service/import-missing-desktop-entry", test_import_rejects_index_without_desktop_entry); g_test_add_func ("/gtk3-theme-service/import-unresolved-inherits", test_import_rejects_unresolved_inherits); return g_test_run (); diff --git a/src/fe-gtk/plugin-tray.c b/src/fe-gtk/plugin-tray.c index 700fb39d..04fe0f06 100644 --- a/src/fe-gtk/plugin-tray.c +++ b/src/fe-gtk/plugin-tray.c @@ -1539,6 +1539,11 @@ tray_cleanup (void) void tray_apply_setup (void) { + /* Theme/setup events can be dispatched before the tray plugin has + * been registered; there is nothing to (de)activate yet. */ + if (!ph) + return; + if (tray_backend_active) { if (!prefs.hex_gui_tray) diff --git a/src/fe-gtk/theme/tests/test-theme-application-input-style.c b/src/fe-gtk/theme/tests/test-theme-application-input-style.c index 3c9b3dd4..91068d18 100644 --- a/src/fe-gtk/theme/tests/test-theme-application-input-style.c +++ b/src/fe-gtk/theme/tests/test-theme-application-input-style.c @@ -21,6 +21,7 @@ #include "../../fe-gtk.h" #include "../theme-application.h" +#include "../theme-policy.h" #include "../../maingui.h" #include "../../../common/zoitechat.h" #include "../../../common/zoitechatc.h" @@ -54,6 +55,19 @@ theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font css_reload_calls++; } +gboolean +theme_policy_system_prefers_dark (void) +{ + return FALSE; +} + +gboolean +theme_policy_is_dark_mode_active (unsigned int mode) +{ + (void) mode; + return FALSE; +} + void theme_runtime_load (void) { @@ -104,7 +118,9 @@ test_invalid_font_falls_back_to_sans_11 (void) g_assert_cmpint (pango_font_description_get_size (style->font_desc), ==, 11 * PANGO_SCALE); g_assert_cmpint (message_calls, ==, 1); g_assert_cmpint (css_reload_calls, ==, 1); - g_assert_true (css_enabled); + /* Input boxes are themed per-widget; the app-global input CSS stays + * off so entries render natively under the active GTK3 theme. */ + g_assert_false (css_enabled); g_assert_nonnull (css_font_desc); if (style->font_desc) diff --git a/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c b/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c index 9e53e544..0ccd2e08 100644 --- a/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c +++ b/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c @@ -22,6 +22,7 @@ #include "../theme-manager.h" #include "../theme-gtk3.h" +#include "../theme-policy.h" #include "../../../common/zoitechat.h" #include "../../../common/zoitechatc.h" @@ -84,6 +85,15 @@ gboolean theme_policy_system_prefers_dark (void) return stub_system_prefers_dark; } +void theme_policy_init (void) +{ +} + +void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback) +{ + (void) callback; +} + gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) { (void) mode; @@ -203,6 +213,7 @@ reset_state (void) idle_add_calls = 0; next_idle_source_id = 33; prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM; + prefs.hex_gui_gtk3_theme[0] = '\0'; test_theme_gtk3_stub_reset (); } @@ -235,12 +246,14 @@ test_auto_refresh_dispatches_mode_palette_and_style_reasons (void) } static void -test_auto_refresh_ignores_non_auto_mode (void) +test_auto_refresh_ignores_locked_in_app_theme (void) { guint listener_id; reset_state (); prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + g_strlcpy (prefs.hex_gui_gtk3_theme, "locked-theme", sizeof (prefs.hex_gui_gtk3_theme)); + prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT; stub_apply_mode_palette_changed = TRUE; listener_id = theme_listener_register ("auto.nonauto", auto_listener, NULL); theme_manager_set_idle_add_func (immediate_idle_add); @@ -254,6 +267,7 @@ test_auto_refresh_ignores_non_auto_mode (void) theme_manager_set_idle_add_func (NULL); theme_listener_unregister (listener_id); + prefs.hex_gui_gtk3_theme[0] = '\0'; } static void @@ -263,6 +277,7 @@ test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void) reset_state (); prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + g_strlcpy (prefs.hex_gui_gtk3_theme, "system-follower", sizeof (prefs.hex_gui_gtk3_theme)); prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM; listener_id = theme_listener_register ("auto.gtk3", auto_listener, NULL); theme_manager_set_idle_add_func (immediate_idle_add); @@ -274,6 +289,39 @@ test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void) g_assert_cmpint (listener_calls, ==, 0); g_assert_cmpint (test_theme_gtk3_stub_apply_current_calls (), ==, 1); + theme_manager_set_idle_add_func (NULL); + theme_listener_unregister (listener_id); + prefs.hex_gui_gtk3_theme[0] = '\0'; +} + +static void +test_auto_refresh_follows_system_theme_without_auto_mode (void) +{ + guint listener_id; + + reset_state (); + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT; + listener_id = theme_listener_register ("auto.system", auto_listener, NULL); + theme_manager_set_idle_add_func (immediate_idle_add); + + theme_manager_refresh_auto_mode (); + + /* No in-app theme is selected, so a system theme change must + * re-resolve the mapped palette even outside auto dark mode. */ + g_assert_cmpint (idle_add_calls, ==, 1); + g_assert_cmpint (auto_state_calls, ==, 0); + g_assert_cmpint (listener_calls, ==, 1); + g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_PALETTE)); + g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_WIDGET_STYLE)); + g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_USERLIST)); + g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_MODE)); + + /* Unchanged system state must not dispatch again. */ + theme_manager_refresh_auto_mode (); + g_assert_cmpint (idle_add_calls, ==, 2); + g_assert_cmpint (listener_calls, ==, 1); + theme_manager_set_idle_add_func (NULL); theme_listener_unregister (listener_id); } @@ -284,9 +332,11 @@ main (int argc, char **argv) g_test_init (&argc, &argv, NULL); g_test_add_func ("/theme/manager/auto_refresh_dispatches_mode_palette_and_style_reasons", test_auto_refresh_dispatches_mode_palette_and_style_reasons); - g_test_add_func ("/theme/manager/auto_refresh_ignores_non_auto_mode", - test_auto_refresh_ignores_non_auto_mode); + g_test_add_func ("/theme/manager/auto_refresh_ignores_locked_in_app_theme", + test_auto_refresh_ignores_locked_in_app_theme); g_test_add_func ("/theme/manager/auto_refresh_reapplies_gtk3_for_follow_system_variant", test_auto_refresh_reapplies_gtk3_for_follow_system_variant); + g_test_add_func ("/theme/manager/auto_refresh_follows_system_theme_without_auto_mode", + test_auto_refresh_follows_system_theme_without_auto_mode); return g_test_run (); } diff --git a/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c b/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c index 4abce690..483c6227 100644 --- a/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c +++ b/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c @@ -23,6 +23,7 @@ #include #include "../theme-manager.h" +#include "../theme-policy.h" #include "../../../common/zoitechat.h" #include "../../../common/zoitechatc.h" @@ -77,6 +78,15 @@ gboolean theme_policy_system_prefers_dark (void) return FALSE; } +void theme_policy_init (void) +{ +} + +void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback) +{ + (void) callback; +} + gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) { (void) mode; diff --git a/src/fe-gtk/theme/tests/test-theme-manager-policy.c b/src/fe-gtk/theme/tests/test-theme-manager-policy.c index c35185cf..f70d23c8 100644 --- a/src/fe-gtk/theme/tests/test-theme-manager-policy.c +++ b/src/fe-gtk/theme/tests/test-theme-manager-policy.c @@ -22,6 +22,7 @@ #include "../theme-palette.h" #include "../theme-manager.h" +#include "../theme-policy.h" #include "../../../common/zoitechat.h" #include "../../../common/zoitechatc.h" @@ -30,6 +31,7 @@ struct session *current_tab; struct session *lastact_sess; struct zoitechatprefs prefs; +static gboolean gtk_available; static gboolean stub_policy_dark; static unsigned int stub_policy_mode; static gboolean stub_apply_mode_result; @@ -79,7 +81,18 @@ void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callbac gboolean theme_policy_is_dark_mode_active (unsigned int mode) { stub_policy_mode = mode; - return stub_policy_dark; + if (mode == ZOITECHAT_DARK_MODE_AUTO) + return stub_policy_dark; + return fe_dark_mode_is_enabled_for (mode); +} + +void theme_policy_init (void) +{ +} + +void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback) +{ + (void) callback; } gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) @@ -109,11 +122,12 @@ void theme_runtime_reset_mode_colors (gboolean dark_mode) (void) dark_mode; } -gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active) +gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed) { (void) mode; - (void) dark_active; stub_apply_mode_calls++; + if (palette_changed) + *palette_changed = TRUE; return TRUE; } @@ -249,10 +263,11 @@ test_manager_set_token_color_routes_by_mode (void) GdkRGBA color = { 0.1, 0.2, 0.3, 1.0 }; gboolean palette_changed = FALSE; + /* All edits target the single user palette; the mode argument is + * accepted for compatibility but no longer selects a dark palette. */ reset_manager_stubs (); stub_policy_dark = FALSE; theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, THEME_TOKEN_MIRC_2, &color, &palette_changed); - g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_LIGHT); g_assert_cmpint (stub_user_set_calls, ==, 1); g_assert_cmpint (stub_dark_set_calls, ==, 0); g_assert_cmpint (stub_apply_mode_calls, ==, 1); @@ -262,16 +277,14 @@ test_manager_set_token_color_routes_by_mode (void) reset_manager_stubs (); stub_policy_dark = TRUE; theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, THEME_TOKEN_MIRC_2, &color, &palette_changed); - g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_DARK); - g_assert_cmpint (stub_user_set_calls, ==, 0); - g_assert_cmpint (stub_dark_set_calls, ==, 1); + g_assert_cmpint (stub_user_set_calls, ==, 1); + g_assert_cmpint (stub_dark_set_calls, ==, 0); reset_manager_stubs (); stub_policy_dark = TRUE; theme_manager_set_token_color (ZOITECHAT_DARK_MODE_AUTO, THEME_TOKEN_MIRC_2, &color, &palette_changed); - g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_AUTO); - g_assert_cmpint (stub_user_set_calls, ==, 0); - g_assert_cmpint (stub_dark_set_calls, ==, 1); + g_assert_cmpint (stub_user_set_calls, ==, 1); + g_assert_cmpint (stub_dark_set_calls, ==, 0); } @@ -300,9 +313,9 @@ test_manager_set_token_color_routes_setup_indexes (void) stub_policy_dark = TRUE; palette_changed = FALSE; theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, def->token, &color, &palette_changed); - g_assert_cmpint (stub_dark_set_calls, ==, 1); - g_assert_cmpint (stub_last_dark_token, ==, def->token); - g_assert_cmpint (stub_user_set_calls, ==, 0); + g_assert_cmpint (stub_user_set_calls, ==, 1); + g_assert_cmpint (stub_last_user_token, ==, def->token); + g_assert_cmpint (stub_dark_set_calls, ==, 0); g_assert_true (palette_changed); } } @@ -345,6 +358,12 @@ test_manager_window_attach_detach_idempotence (void) gulong *second_handler_ptr; gulong second_handler_id; + if (!gtk_available) + { + g_test_message ("GTK display not available"); + return; + } + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); g_assert_nonnull (window); @@ -395,5 +414,6 @@ main (int argc, char **argv) test_manager_listener_registration_dispatch_and_unregister); g_test_add_func ("/theme/manager/window_attach_detach_idempotence", test_manager_window_attach_detach_idempotence); + gtk_available = gtk_init_check (&argc, &argv); return g_test_run (); } diff --git a/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c b/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c index 78f94e3d..16d1d259 100644 --- a/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c +++ b/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c @@ -300,7 +300,7 @@ theme_gtk3_is_active (void) #include "../theme-preferences.c" static void -test_removed_selected_theme_commits_fallback_and_applies (void) +test_removed_selected_theme_falls_back_to_system_default (void) { GtkWidget *page; theme_preferences_ui *ui; @@ -317,23 +317,26 @@ test_removed_selected_theme_commits_fallback_and_applies (void) g_strlcpy (prefs.hex_gui_gtk3_theme, "removed-theme", sizeof (prefs.hex_gui_gtk3_theme)); prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_DARK; removed_selected = FALSE; - apply_current_calls = 0; - applied_theme_id[0] = '\0'; page = theme_preferences_create_page (NULL, &setup_prefs, NULL); ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui"); g_assert_nonnull (ui); + apply_current_calls = 0; + applied_theme_id[0] = '\0'; + g_assert_nonnull (ui->gtk3_remove); gtk_button_clicked (GTK_BUTTON (ui->gtk3_remove)); - g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme"); - g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "fallback-theme"); - g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT); - g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT); + /* Removing the active in-app theme returns to the system GTK3 + * theme ("None") instead of forcing another theme. */ + g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, ""); + g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, ""); + g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM); + g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM); g_assert_cmpint (apply_current_calls, ==, 1); - g_assert_cmpstr (applied_theme_id, ==, "fallback-theme"); - g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT); + g_assert_cmpstr (applied_theme_id, ==, ""); + g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM); gtk_widget_destroy (page); } @@ -412,8 +415,8 @@ main (int argc, char **argv) { g_test_init (&argc, &argv, NULL); gtk_available = gtk_init_check (&argc, &argv); - g_test_add_func ("/theme/preferences/gtk3_removed_selection_applies_fallback", - test_removed_selected_theme_commits_fallback_and_applies); + g_test_add_func ("/theme/preferences/gtk3_removed_selection_falls_back_to_system", + test_removed_selected_theme_falls_back_to_system_default); g_test_add_func ("/theme/preferences/gtk3_unset_keeps_system_default", test_unset_theme_keeps_system_default_without_apply); g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme", diff --git a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c index 4feffb7f..3050e5fb 100644 --- a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c +++ b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c @@ -290,12 +290,12 @@ test_ui_edits_persist_without_legacy_array_mutation (void) theme_runtime_save (); theme_runtime_load (); - theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL); + theme_runtime_apply_dark_mode (FALSE); g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &light_loaded)); g_assert_true (gdk_rgba_parse (&light_expected, "#224466")); g_assert_true (colors_equal (&light_loaded, &light_expected)); - theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_DARK, NULL); + theme_runtime_apply_dark_mode (TRUE); g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &dark_loaded)); g_assert_true (gdk_rgba_parse (&dark_expected, "#88aacc")); g_assert_true (colors_equal (&dark_loaded, &dark_expected)); diff --git a/src/fe-gtk/theme/theme-access.c b/src/fe-gtk/theme/theme-access.c index d1aca1a9..156dd3be 100644 --- a/src/fe-gtk/theme/theme-access.c +++ b/src/fe-gtk/theme/theme-access.c @@ -21,7 +21,6 @@ #include "theme-access.h" #include "theme-runtime.h" -#include "theme-gtk3.h" @@ -32,7 +31,8 @@ enum THEME_XTEXT_MARK_BG_INDEX = 100, THEME_XTEXT_FG_INDEX = 101, THEME_XTEXT_BG_INDEX = 102, - THEME_XTEXT_MARKER_INDEX = 103 + THEME_XTEXT_MARKER_INDEX = 103, + THEME_XTEXT_MARKER_LEGACY_INDEX = 36 }; static const guint8 theme_default_99_mirc_colors[THEME_XTEXT_MIRC_COLS][3] = { @@ -111,14 +111,49 @@ theme_access_context_get_background_color (GtkStyleContext *context, GtkStateFla gtk_style_context_restore (context); } +static gboolean +theme_access_lookup_named_color (GtkStyleContext *context, const char *const *names, GdkRGBA *out_color) +{ + size_t i; + + for (i = 0; names[i] != NULL; i++) + { + if (gtk_style_context_lookup_color (context, names[i], out_color)) + return TRUE; + } + + return FALSE; +} + +static void +theme_access_resolve_map_background (GtkStyleContext *context, GtkStateFlags state, + const char *const *named_fallbacks, GdkRGBA *out_color) +{ + GdkRGBA named; + + theme_access_context_get_background_color (context, state, out_color); + if (out_color->alpha > 0.0) + return; + + /* Unstyled CSS nodes (e.g. the custom chat text widget) report a fully + * transparent background; fall back to the theme's named colors so the + * mapped palette still follows the active GTK3 theme. */ + if (theme_access_lookup_named_color (context, named_fallbacks, &named)) + *out_color = named; +} + static gboolean theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map) { + static const char *const base_fallbacks[] = { "theme_base_color", "theme_bg_color", NULL }; + static const char *const selected_bg_fallbacks[] = { "theme_selected_bg_color", NULL }; + static const char *const selected_fg_fallbacks[] = { "theme_selected_fg_color", NULL }; GtkStyleContext *context; + GdkRGBA named; GdkRGBA accent; g_return_val_if_fail (out_map != NULL, FALSE); - if (!theme_gtk3_is_active () || widget == NULL || !GTK_IS_WIDGET (widget)) + if (widget == NULL || !GTK_IS_WIDGET (widget)) return FALSE; context = gtk_widget_get_style_context (widget); @@ -126,9 +161,14 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map return FALSE; theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground); - theme_access_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_background); + theme_access_resolve_map_background (context, GTK_STATE_FLAG_NORMAL, base_fallbacks, + &out_map->text_background); theme_access_context_get_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_foreground); - theme_access_context_get_background_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_background); + theme_access_resolve_map_background (context, GTK_STATE_FLAG_SELECTED, selected_bg_fallbacks, + &out_map->selection_background); + if (gdk_rgba_equal (&out_map->selection_foreground, &out_map->text_foreground) && + theme_access_lookup_named_color (context, selected_fg_fallbacks, &named)) + out_map->selection_foreground = named; theme_access_context_get_color (context, GTK_STATE_FLAG_LINK, &accent); if (accent.alpha <= 0.0) accent = out_map->selection_background; @@ -223,14 +263,35 @@ theme_get_xtext_colors (XTextColor *palette, size_t palette_len) void theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len) { + ThemeGtkPaletteMap gtk_map = { 0 }; ThemeWidgetStyleValues style_values; GdkRGBA marker_color; + gboolean have_marker = FALSE; if (!palette) return; - theme_get_widget_style_values_for_widget (widget, &style_values); - theme_runtime_get_xtext_colors (palette, palette_len); + if (theme_access_get_gtk_palette_map (widget, >k_map)) + { + theme_runtime_get_widget_style_values_mapped (>k_map, &style_values); + theme_runtime_get_xtext_colors_mapped (>k_map, palette, palette_len); + } + else + { + theme_runtime_get_widget_style_values (&style_values); + theme_runtime_get_xtext_colors (palette, palette_len); + } + if (palette_len > THEME_XTEXT_MARKER_LEGACY_INDEX) + { + /* The marker token lives at its legacy slot before the extended + * mIRC palette overwrites it; keep the (possibly theme-mapped) + * value for the dedicated marker index below. */ + marker_color.red = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].red; + marker_color.green = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].green; + marker_color.blue = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].blue; + marker_color.alpha = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].alpha; + have_marker = TRUE; + } if (palette_len >= THEME_XTEXT_MIRC_COLS) theme_access_apply_default_99_palette (palette, palette_len); if (palette_len > THEME_XTEXT_MARK_FG_INDEX) @@ -249,7 +310,7 @@ theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_ } if (palette_len > THEME_XTEXT_MARKER_INDEX) { - if (!theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color)) + if (!have_marker && !theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color)) marker_color = style_values.selection_background; palette[THEME_XTEXT_MARKER_INDEX].red = marker_color.red; palette[THEME_XTEXT_MARKER_INDEX].green = marker_color.green; diff --git a/src/fe-gtk/theme/theme-application.c b/src/fe-gtk/theme/theme-application.c index 6a08c5a9..333d46b3 100644 --- a/src/fe-gtk/theme/theme-application.c +++ b/src/fe-gtk/theme/theme-application.c @@ -25,6 +25,7 @@ #include "theme-css.h" #include "theme-runtime.h" #include "theme-gtk3.h" +#include "theme-policy.h" #include "../maingui.h" #include @@ -59,6 +60,16 @@ theme_application_apply_toplevel_theme (gboolean dark) prefer_dark = TRUE; else if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_LIGHT) prefer_dark = FALSE; + else + prefer_dark = theme_policy_system_prefers_dark (); + } + else + { + /* Following the system GTK3 theme: only request the dark + * variant when the dark-mode preference (or the system, in + * auto mode) asks for it, instead of clobbering the value + * that came from the user's GTK configuration. */ + prefer_dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode); } if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), diff --git a/src/fe-gtk/theme/theme-css.c b/src/fe-gtk/theme/theme-css.c index 276b65c4..d82e7f87 100644 --- a/src/fe-gtk/theme/theme-css.c +++ b/src/fe-gtk/theme/theme-css.c @@ -29,8 +29,6 @@ static const char *theme_css_selector_input = "#zoitechat-inputbox"; static const char *theme_css_selector_input_text = "#zoitechat-inputbox text"; static const char *theme_css_selector_palette_class = "zoitechat-palette"; -static const char *theme_css_selector_dark_class = "zoitechat-dark"; -static const char *theme_css_selector_light_class = "zoitechat-light"; static const char *theme_css_palette_provider_key = "zoitechat-palette-provider"; static const guint theme_css_provider_priority = GTK_STYLE_PROVIDER_PRIORITY_USER; static const char *theme_css_palette_scope_selectors = @@ -362,61 +360,9 @@ theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkR char * theme_css_build_toplevel_classes (void) { - return g_strdup_printf ( - "window.%s, window.%s:backdrop, .%s {" - "background-color: #202020;" - "color: #f0f0f0;" - "border-color: #202020;" - "}" - "window.%s menubar, window.%s menubar:backdrop, window.%s menubar box, window.%s menubar box:backdrop, window.%s menuitem, window.%s menuitem:backdrop {" - "background-color: @theme_bg_color;" - "background-image: none;" - "color: @theme_fg_color;" - "border-color: @theme_bg_color;" - "}" - "window.%s menuitem:hover, window.%s menuitem:selected {" - "background-color: @theme_selected_bg_color;" - "background-image: none;" - "color: @theme_selected_fg_color;" - "border-color: @theme_selected_bg_color;" - "}" - "window.%s, window.%s:backdrop, .%s {" - "background-color: #f6f6f6;" - "color: #101010;" - "border-color: #f6f6f6;" - "}" - "window.%s menubar, window.%s menubar:backdrop, window.%s menubar box, window.%s menubar box:backdrop, window.%s menuitem, window.%s menuitem:backdrop {" - "background-color: @theme_bg_color;" - "background-image: none;" - "color: @theme_fg_color;" - "border-color: @theme_bg_color;" - "}" - "window.%s menuitem:hover, window.%s menuitem:selected {" - "background-color: @theme_selected_bg_color;" - "background-image: none;" - "color: @theme_selected_fg_color;" - "border-color: @theme_selected_bg_color;" - "}", - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_dark_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class, - theme_css_selector_light_class); + /* Windows, dialogs and menus must render with the colors of the active + * GTK3 theme (system or in-app selected). zoitechat-dark and + * zoitechat-light are marker classes only; painting fixed colors here + * overrides every theme (issue #334). */ + return g_strdup (""); } diff --git a/src/fe-gtk/theme/theme-manager.c b/src/fe-gtk/theme/theme-manager.c index 16cd3079..b62b1386 100644 --- a/src/fe-gtk/theme/theme-manager.c +++ b/src/fe-gtk/theme/theme-manager.c @@ -58,10 +58,52 @@ typedef struct gboolean resolved_dark_preference; char gtk3_theme_id[sizeof prefs.hex_gui_gtk3_theme]; int gtk3_variant; + char *gtk_theme_name; } ThemeManagerAutoRefreshCache; static ThemeManagerAutoRefreshCache theme_manager_auto_refresh_cache; +static void +theme_manager_auto_refresh_cache_reset (void) +{ + g_free (theme_manager_auto_refresh_cache.gtk_theme_name); + memset (&theme_manager_auto_refresh_cache, 0, sizeof (theme_manager_auto_refresh_cache)); +} + +static void +theme_manager_auto_refresh_cache_store (gboolean resolved_dark_preference, char *gtk_theme_name) +{ + theme_manager_auto_refresh_cache.initialized = TRUE; + theme_manager_auto_refresh_cache.resolved_dark_preference = resolved_dark_preference; + theme_manager_auto_refresh_cache.gtk3_variant = prefs.hex_gui_gtk3_variant; + g_strlcpy (theme_manager_auto_refresh_cache.gtk3_theme_id, + prefs.hex_gui_gtk3_theme, + sizeof (theme_manager_auto_refresh_cache.gtk3_theme_id)); + g_free (theme_manager_auto_refresh_cache.gtk_theme_name); + theme_manager_auto_refresh_cache.gtk_theme_name = gtk_theme_name; +} + +static char * +theme_manager_dup_gtk_theme_name (void) +{ + GtkSettings *settings = gtk_settings_get_default (); + char *gtk_theme_name = NULL; + + if (settings) + g_object_get (settings, "gtk-theme-name", >k_theme_name, NULL); + return gtk_theme_name; +} + +/* Record the state the application started up with so that the first queued + * auto refresh only reacts to real changes; startup already applied the + * current theme, and consumers such as the tray are not ready yet. */ +static void +theme_manager_auto_refresh_cache_prime (void) +{ + theme_manager_auto_refresh_cache_store (theme_policy_system_prefers_dark (), + theme_manager_dup_gtk_theme_name ()); +} + static void theme_manager_apply_platform_window_theme (GtkWidget *window); static void @@ -145,7 +187,9 @@ theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gboolean color_change = FALSE; gboolean should_refresh_gtk3; gboolean gtk3_refresh; + gboolean follows_system_theme; gboolean resolved_dark_preference; + char *gtk_theme_name = NULL; static gboolean in_handler = FALSE; (void) settings; @@ -154,39 +198,47 @@ theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, resolved_dark_preference = theme_policy_system_prefers_dark (); gtk3_refresh = theme_manager_should_refresh_gtk3 (); + follows_system_theme = prefs.hex_gui_gtk3_theme[0] == '\0'; should_refresh_gtk3 = gtk3_refresh || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO; + gtk_theme_name = theme_manager_dup_gtk_theme_name (); + if (theme_manager_auto_refresh_cache.initialized && theme_manager_auto_refresh_cache.resolved_dark_preference == resolved_dark_preference && theme_manager_auto_refresh_cache.gtk3_variant == prefs.hex_gui_gtk3_variant && - g_strcmp0 (theme_manager_auto_refresh_cache.gtk3_theme_id, prefs.hex_gui_gtk3_theme) == 0) + g_strcmp0 (theme_manager_auto_refresh_cache.gtk3_theme_id, prefs.hex_gui_gtk3_theme) == 0 && + g_strcmp0 (theme_manager_auto_refresh_cache.gtk_theme_name, gtk_theme_name) == 0) + { + g_free (gtk_theme_name); return; + } - theme_manager_auto_refresh_cache.initialized = TRUE; - theme_manager_auto_refresh_cache.resolved_dark_preference = resolved_dark_preference; - theme_manager_auto_refresh_cache.gtk3_variant = prefs.hex_gui_gtk3_variant; - g_strlcpy (theme_manager_auto_refresh_cache.gtk3_theme_id, - prefs.hex_gui_gtk3_theme, - sizeof (theme_manager_auto_refresh_cache.gtk3_theme_id)); + theme_manager_auto_refresh_cache_store (resolved_dark_preference, gtk_theme_name); - if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh) + if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh && !follows_system_theme) return; if (in_handler) return; in_handler = TRUE; - if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO) + if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO || follows_system_theme) { - fe_set_auto_dark_mode_state (resolved_dark_preference); theme_manager_commit_preferences (prefs.hex_gui_dark_mode, &color_change); - if (color_change) - theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE); + if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO) + fe_set_auto_dark_mode_state (resolved_dark_preference); } if (should_refresh_gtk3) theme_gtk3_apply_current (NULL); + if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO || follows_system_theme) + { + /* The system theme feeds the mapped palette (chat area, user + * list, channel tree), so consumers must re-resolve colors. */ + theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE); + } + in_handler = FALSE; } @@ -204,18 +256,28 @@ theme_manager_run_auto_refresh (gpointer data) static void theme_manager_queue_auto_refresh (GtkSettings *settings, GParamSpec *pspec, gpointer data) { + guint source_id; + (void) settings; (void) pspec; if (theme_manager_auto_refresh_source != 0) return; - theme_manager_auto_refresh_source = theme_manager_idle_add_func (theme_manager_run_auto_refresh, data); + theme_manager_auto_refresh_source = 1; + source_id = theme_manager_idle_add_func (theme_manager_run_auto_refresh, data); + /* An injected idle-add may run the refresh synchronously and already + * have cleared the pending marker; only keep the id while pending. */ + if (theme_manager_auto_refresh_source != 0) + theme_manager_auto_refresh_source = source_id; } void theme_manager_init (void) { + static gboolean settings_signals_connected = FALSE; + GtkSettings *settings; + if (!theme_manager_listeners) theme_manager_listeners = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, theme_listener_free); @@ -223,10 +285,25 @@ theme_manager_init (void) if (!theme_manager_setup_listener_id) theme_manager_setup_listener_id = theme_listener_register ("setup.apply", theme_manager_setup_apply_listener, NULL); - fe_set_auto_dark_mode_state (FALSE); + theme_policy_init (); + theme_policy_set_appearance_changed_callback (theme_manager_refresh_auto_mode); + + settings = gtk_settings_get_default (); + if (settings && !settings_signals_connected) + { + g_signal_connect (settings, "notify::gtk-theme-name", + G_CALLBACK (theme_manager_queue_auto_refresh), NULL); + g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme", + G_CALLBACK (theme_manager_queue_auto_refresh), NULL); + settings_signals_connected = TRUE; + } + + fe_set_auto_dark_mode_state (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO ? + theme_policy_system_prefers_dark () : FALSE); theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL); theme_gtk3_init (); zoitechat_set_theme_post_apply_callback (theme_manager_handle_theme_applied); + theme_manager_auto_refresh_cache_prime (); } gboolean @@ -519,7 +596,7 @@ theme_manager_apply_platform_window_theme (GtkWidget *window) dark = theme_policy_system_prefers_dark (); } else - dark = theme_runtime_is_dark_active (); + dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode); if (context) { gtk_style_context_remove_class (context, "zoitechat-dark"); @@ -712,4 +789,5 @@ theme_manager_set_idle_add_func (ThemeManagerIdleAddFunc idle_add_func) { theme_manager_idle_add_func = idle_add_func ? idle_add_func : g_idle_add; theme_manager_auto_refresh_source = 0; + theme_manager_auto_refresh_cache_reset (); } diff --git a/src/fe-gtk/theme/theme-policy.c b/src/fe-gtk/theme/theme-policy.c index 8bf31863..aa473787 100644 --- a/src/fe-gtk/theme/theme-policy.c +++ b/src/fe-gtk/theme/theme-policy.c @@ -20,14 +20,379 @@ #include "theme-policy.h" +#include + +#include +#include + +#include "theme-gtk3.h" #include "../fe-gtk.h" #include "../../common/zoitechat.h" #include "../../common/zoitechatc.h" +/* org.freedesktop.appearance color-scheme values (XDG desktop portal) */ +enum +{ + THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE = 0, + THEME_POLICY_COLOR_SCHEME_PREFER_DARK = 1, + THEME_POLICY_COLOR_SCHEME_PREFER_LIGHT = 2 +}; + +static ThemePolicyAppearanceChangedFunc policy_appearance_changed_cb; + +/* GtkSettings state captured before ZoiteChat writes to it, so that values + * coming from the user's own GTK configuration are not mistaken for values + * this application set. */ +static gboolean policy_initial_gtk_state_captured; +static gboolean policy_initial_prefer_dark; +static char *policy_initial_theme_name; + +#ifndef G_OS_WIN32 +static gboolean policy_portal_watch_started; +static gboolean policy_portal_value_valid; +static guint32 policy_portal_color_scheme; + +static gboolean policy_gsettings_checked; +static GSettings *policy_gsettings_interface; +#endif + +static void +policy_capture_initial_gtk_state (void) +{ + GtkSettings *settings; + + if (policy_initial_gtk_state_captured) + return; + + settings = gtk_settings_get_default (); + if (!settings) + return; + + g_object_get (settings, + "gtk-application-prefer-dark-theme", &policy_initial_prefer_dark, + "gtk-theme-name", &policy_initial_theme_name, + NULL); + policy_initial_gtk_state_captured = TRUE; +} + +static gboolean +policy_theme_name_is_dark (const char *name) +{ + char *lower; + gboolean dark; + + if (!name || !name[0]) + return FALSE; + + lower = g_ascii_strdown (name, -1); + dark = strstr (lower, "dark") != NULL || strstr (lower, "noir") != NULL || + strstr (lower, "black") != NULL; + g_free (lower); + return dark; +} + +#ifndef G_OS_WIN32 + +static void +policy_notify_appearance_changed (void) +{ + if (policy_appearance_changed_cb) + policy_appearance_changed_cb (); +} + +static gboolean +policy_color_scheme_from_variant (GVariant *value, guint32 *color_scheme) +{ + GVariant *inner; + gboolean ok = FALSE; + + if (!value || !color_scheme) + return FALSE; + + inner = g_variant_ref (value); + while (g_variant_is_of_type (inner, G_VARIANT_TYPE_VARIANT)) + { + GVariant *unwrapped = g_variant_get_variant (inner); + + g_variant_unref (inner); + inner = unwrapped; + } + + if (g_variant_is_of_type (inner, G_VARIANT_TYPE_UINT32)) + { + *color_scheme = g_variant_get_uint32 (inner); + ok = TRUE; + } + + g_variant_unref (inner); + return ok; +} + +static void +policy_portal_store_color_scheme (guint32 color_scheme) +{ + gboolean changed; + + changed = !policy_portal_value_valid || policy_portal_color_scheme != color_scheme; + policy_portal_value_valid = TRUE; + policy_portal_color_scheme = color_scheme; + + if (changed) + policy_notify_appearance_changed (); +} + +static void +policy_portal_setting_changed_cb (GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, + gpointer user_data) +{ + const char *setting_namespace = NULL; + const char *setting_key = NULL; + GVariant *value = NULL; + guint32 color_scheme = THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE; + + (void) connection; + (void) sender_name; + (void) object_path; + (void) interface_name; + (void) signal_name; + (void) user_data; + + if (!parameters || !g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ssv)"))) + return; + + g_variant_get (parameters, "(&s&sv)", &setting_namespace, &setting_key, &value); + if (g_strcmp0 (setting_namespace, "org.freedesktop.appearance") == 0 && + g_strcmp0 (setting_key, "color-scheme") == 0 && + policy_color_scheme_from_variant (value, &color_scheme)) + policy_portal_store_color_scheme (color_scheme); + + if (value) + g_variant_unref (value); +} + +static void +policy_portal_read_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data) +{ + GVariant *reply; + GVariant *value = NULL; + guint32 color_scheme = THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE; + + (void) user_data; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, NULL); + if (!reply) + return; + + if (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(v)"))) + { + g_variant_get (reply, "(v)", &value); + if (policy_color_scheme_from_variant (value, &color_scheme)) + policy_portal_store_color_scheme (color_scheme); + if (value) + g_variant_unref (value); + } + + g_variant_unref (reply); +} + +static void +policy_portal_bus_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data) +{ + GDBusConnection *connection; + + (void) source; + (void) user_data; + + connection = g_bus_get_finish (result, NULL); + if (!connection) + return; + + g_dbus_connection_signal_subscribe (connection, + "org.freedesktop.portal.Desktop", + "org.freedesktop.portal.Settings", + "SettingChanged", + "/org/freedesktop/portal/desktop", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + policy_portal_setting_changed_cb, + NULL, + NULL); + + g_dbus_connection_call (connection, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "Read", + g_variant_new ("(ss)", "org.freedesktop.appearance", "color-scheme"), + G_VARIANT_TYPE ("(v)"), + G_DBUS_CALL_FLAGS_NONE, + 3000, + NULL, + policy_portal_read_ready_cb, + NULL); + g_object_unref (connection); +} + +static void +policy_portal_watch_start (void) +{ + if (policy_portal_watch_started) + return; + + policy_portal_watch_started = TRUE; + g_bus_get (G_BUS_TYPE_SESSION, NULL, policy_portal_bus_ready_cb, NULL); +} + +static void +policy_gsettings_changed_cb (GSettings *settings, const char *key, gpointer user_data) +{ + (void) settings; + (void) key; + (void) user_data; + + policy_notify_appearance_changed (); +} + +static GSettings * +policy_get_interface_gsettings (void) +{ + GSettingsSchemaSource *source; + GSettingsSchema *schema; + gboolean has_key; + + if (policy_gsettings_checked) + return policy_gsettings_interface; + + policy_gsettings_checked = TRUE; + + source = g_settings_schema_source_get_default (); + if (!source) + return NULL; + + schema = g_settings_schema_source_lookup (source, "org.gnome.desktop.interface", TRUE); + if (!schema) + return NULL; + + /* Requires GLib 2.40, always satisfied by the GTK 3.22 dependency. */ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + has_key = g_settings_schema_has_key (schema, "color-scheme"); + G_GNUC_END_IGNORE_DEPRECATIONS + g_settings_schema_unref (schema); + if (!has_key) + return NULL; + + policy_gsettings_interface = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect (policy_gsettings_interface, "changed::color-scheme", + G_CALLBACK (policy_gsettings_changed_cb), NULL); + return policy_gsettings_interface; +} + +static gboolean +policy_gsettings_color_scheme (gboolean *prefers_dark) +{ + GSettings *settings; + char *scheme; + gboolean explicit_preference = FALSE; + + settings = policy_get_interface_gsettings (); + if (!settings) + return FALSE; + + scheme = g_settings_get_string (settings, "color-scheme"); + if (g_strcmp0 (scheme, "prefer-dark") == 0) + { + *prefers_dark = TRUE; + explicit_preference = TRUE; + } + else if (g_strcmp0 (scheme, "prefer-light") == 0) + { + *prefers_dark = FALSE; + explicit_preference = TRUE; + } + g_free (scheme); + return explicit_preference; +} + +static gboolean +policy_gtk_heuristic_prefers_dark (void) +{ + GtkSettings *settings; + char *theme_name = NULL; + gboolean dark; + + policy_capture_initial_gtk_state (); + + /* gtk-application-prefer-dark-theme is overwritten by ZoiteChat at + * runtime, so only its captured startup value (settings.ini, XSETTINGS) + * reflects the user's own configuration. */ + if (policy_initial_gtk_state_captured && policy_initial_prefer_dark) + return TRUE; + + if (theme_gtk3_is_active ()) + { + /* gtk-theme-name currently holds the in-app theme override; + * fall back to the captured system theme name. */ + theme_name = g_strdup (policy_initial_theme_name); + } + else + { + settings = gtk_settings_get_default (); + if (settings) + g_object_get (settings, "gtk-theme-name", &theme_name, NULL); + } + + dark = policy_theme_name_is_dark (theme_name); + g_free (theme_name); + return dark; +} + +#endif /* !G_OS_WIN32 */ + +void +theme_policy_init (void) +{ + policy_capture_initial_gtk_state (); +#ifndef G_OS_WIN32 + policy_portal_watch_start (); +#endif +} + +void +theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback) +{ + policy_appearance_changed_cb = callback; +} + gboolean theme_policy_system_prefers_dark (void) { - return FALSE; +#ifdef G_OS_WIN32 + gboolean prefer_dark = FALSE; + + policy_capture_initial_gtk_state (); + + if (fe_win32_high_contrast_is_enabled ()) + return FALSE; + if (fe_win32_try_get_system_dark (&prefer_dark)) + return prefer_dark; + return policy_theme_name_is_dark (policy_initial_theme_name); +#else + gboolean prefers_dark = FALSE; + + policy_capture_initial_gtk_state (); + policy_portal_watch_start (); + + if (policy_portal_value_valid && + policy_portal_color_scheme != THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE) + return policy_portal_color_scheme == THEME_POLICY_COLOR_SCHEME_PREFER_DARK; + + if (policy_gsettings_color_scheme (&prefers_dark)) + return prefers_dark; + + return policy_gtk_heuristic_prefers_dark (); +#endif } gboolean diff --git a/src/fe-gtk/theme/theme-policy.h b/src/fe-gtk/theme/theme-policy.h index 2d9ca9a0..be201cad 100644 --- a/src/fe-gtk/theme/theme-policy.h +++ b/src/fe-gtk/theme/theme-policy.h @@ -23,6 +23,10 @@ #include "../fe-gtk.h" +typedef void (*ThemePolicyAppearanceChangedFunc) (void); + +void theme_policy_init (void); +void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback); gboolean theme_policy_system_prefers_dark (void); gboolean theme_policy_is_dark_mode_active (unsigned int mode); gboolean theme_policy_is_app_dark_mode_active (void); diff --git a/src/fe-gtk/theme/theme-preferences.c b/src/fe-gtk/theme/theme-preferences.c index efcc9975..ca1ced22 100644 --- a/src/fe-gtk/theme/theme-preferences.c +++ b/src/fe-gtk/theme/theme-preferences.c @@ -100,6 +100,9 @@ typedef struct gboolean changed; gboolean snapshot_valid[THEME_TOKEN_COUNT]; gboolean staged_valid[THEME_TOKEN_COUNT]; + /* Only explicitly edited tokens are written back to the runtime + * palette; untouched tokens keep following the GTK3 theme. */ + gboolean staged_dirty[THEME_TOKEN_COUNT]; GdkRGBA snapshot[THEME_TOKEN_COUNT]; GdkRGBA staged[THEME_TOKEN_COUNT]; } theme_preferences_stage_state; @@ -146,7 +149,8 @@ theme_preferences_stage_sync_runtime_to_snapshot (void) for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) { - if (theme_preferences_stage.snapshot_valid[token]) + if (theme_preferences_stage.snapshot_valid[token] && + theme_preferences_stage.staged_dirty[token]) theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, &theme_preferences_stage.snapshot[token], NULL); } @@ -159,7 +163,8 @@ theme_preferences_stage_sync_runtime_to_staged (void) for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) { - if (theme_preferences_stage.staged_valid[token]) + if (theme_preferences_stage.staged_valid[token] && + theme_preferences_stage.staged_dirty[token]) theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, &theme_preferences_stage.staged[token], NULL); } @@ -178,6 +183,7 @@ theme_preferences_staged_set_color (ThemeSemanticToken token, const GdkRGBA *rgb { theme_preferences_stage.staged[token] = *rgba; theme_preferences_stage.staged_valid[token] = TRUE; + theme_preferences_stage.staged_dirty[token] = TRUE; theme_preferences_stage_recompute_changed (); if (color_change_flag) *color_change_flag = theme_preferences_stage.changed; @@ -728,6 +734,10 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i ThemeSemanticToken token; ThemeWidgetStyleValues style_values; + /* The runtime palette was just reset; drop any + * pending edits so the reset is not re-pinned. */ + memset (theme_preferences_stage.staged_dirty, 0, + sizeof (theme_preferences_stage.staged_dirty)); for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) { GdkRGBA rgba; @@ -742,7 +752,6 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_FOREGROUND] = TRUE; theme_preferences_stage.staged[THEME_TOKEN_TEXT_BACKGROUND] = style_values.background; theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_BACKGROUND] = TRUE; - theme_preferences_stage_sync_runtime_to_staged (); theme_preferences_stage_recompute_changed (); if (ui->color_change_flag) *ui->color_change_flag = theme_preferences_stage.changed; @@ -1418,11 +1427,31 @@ theme_preferences_gtk3_sync_remove_state (theme_preferences_ui *ui) gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER); } +static void +theme_preferences_stage_rebase_color (ThemeSemanticToken token, const GdkRGBA *rgba) +{ + if (token < 0 || token >= THEME_TOKEN_COUNT || !rgba) + return; + if (!theme_preferences_stage.active) + return; + + /* The theme changed underneath the dialog: shift the baseline so the + * new theme-provided value is neither treated as a user edit nor + * pinned into the runtime palette. */ + theme_preferences_stage.snapshot[token] = *rgba; + theme_preferences_stage.staged[token] = *rgba; + theme_preferences_stage.snapshot_valid[token] = TRUE; + theme_preferences_stage.staged_valid[token] = TRUE; + theme_preferences_stage.staged_dirty[token] = FALSE; + theme_preferences_stage_recompute_changed (); +} + static void theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui) { ThemeWidgetStyleValues style_values; GtkWidget *style_source = NULL; + gboolean follows_system_theme = prefs.hex_gui_gtk3_theme[0] == '\0'; if (ui && ui->parent) style_source = GTK_WIDGET (ui->parent); @@ -1433,6 +1462,18 @@ theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui) theme_get_widget_style_values_for_widget (style_source, &style_values); + if (follows_system_theme) + { + /* Following the system GTK3 theme: leave the mapped tokens + * unpinned so they keep tracking the theme, and only refresh + * what the dialog shows. */ + theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground); + theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_BACKGROUND, &style_values.background); + theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_FOREGROUND, &style_values.selection_foreground); + theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_BACKGROUND, &style_values.selection_background); + return; + } + theme_preferences_staged_set_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground, NULL,