From d9be0a7b1c33e2feb9bd3c93fa7b8500ec59c4e4 Mon Sep 17 00:00:00 2001 From: deepend Date: Mon, 2 Mar 2026 19:42:48 -0700 Subject: [PATCH] feat: centralize theming in theme-manager (palette/tokens, CSS, dark-mode, setup UI), add tests + win32/meson wiring --- src/common/cfgfiles.c | 5 +- src/common/common.vcxproj | 2 + src/common/common.vcxproj.filters | 6 + src/common/meson.build | 1 + src/common/theme-service.c | 176 +++++ src/common/theme-service.h | 12 + src/common/zoitechat.c | 86 +-- src/common/zoitechat.h | 3 + src/fe-gtk/chanview-tabs.c | 151 ++++- src/fe-gtk/chanview-tree.c | 15 +- src/fe-gtk/chanview.c | 93 ++- src/fe-gtk/dccgui.c | 23 +- src/fe-gtk/fe-gtk.c | 365 +--------- src/fe-gtk/fe-gtk.h | 12 +- src/fe-gtk/fe-gtk.vcxproj | 24 +- src/fe-gtk/fe-gtk.vcxproj.filters | 72 +- src/fe-gtk/fkeys.c | 60 +- src/fe-gtk/gtkutil.c | 68 +- src/fe-gtk/gtkutil.h | 2 +- src/fe-gtk/icon-resolver.c | 25 +- src/fe-gtk/maingui.c | 153 ++++- src/fe-gtk/menu.c | 2 +- src/fe-gtk/meson.build | 107 ++- src/fe-gtk/notifygui.c | 24 +- src/fe-gtk/palette.c | 453 ------------- src/fe-gtk/palette.h | 57 +- src/fe-gtk/rawlog.c | 56 +- src/fe-gtk/setup.c | 623 +----------------- src/fe-gtk/setup.h | 4 +- src/fe-gtk/sexy-spell-entry.c | 47 +- src/fe-gtk/textgui.c | 56 +- .../theme/tests/test-theme-access-routing.c | 236 +++++++ .../test-theme-application-input-style.c | 122 ++++ .../tests/test-theme-manager-auto-refresh.c | 229 +++++++ .../test-theme-manager-dispatch-routing.c | 273 ++++++++ .../theme/tests/test-theme-manager-policy.c | 368 +++++++++++ .../tests/test-theme-runtime-persistence.c | 281 ++++++++ src/fe-gtk/theme/theme-access.c | 73 ++ src/fe-gtk/theme/theme-access.h | 20 + src/fe-gtk/theme/theme-application.c | 83 +++ src/fe-gtk/theme/theme-application.h | 11 + src/fe-gtk/theme/theme-css.c | 298 +++++++++ src/fe-gtk/theme/theme-css.h | 21 + src/fe-gtk/theme/theme-gtk.h | 9 + src/fe-gtk/theme/theme-manager.c | 497 ++++++++++++++ src/fe-gtk/theme/theme-manager.h | 74 +++ src/fe-gtk/theme/theme-palette.c | 286 ++++++++ src/fe-gtk/theme/theme-palette.h | 143 ++++ src/fe-gtk/theme/theme-policy.c | 59 ++ src/fe-gtk/theme/theme-policy.h | 10 + src/fe-gtk/theme/theme-preferences.c | 580 ++++++++++++++++ src/fe-gtk/theme/theme-preferences.h | 16 + src/fe-gtk/theme/theme-runtime.c | 466 +++++++++++++ src/fe-gtk/theme/theme-runtime.h | 21 + src/fe-gtk/userlistgui.c | 223 +++++-- src/fe-gtk/xtext.c | 197 +++--- troubleshooting.md | 13 + 57 files changed, 5476 insertions(+), 1916 deletions(-) create mode 100644 src/common/theme-service.c create mode 100644 src/common/theme-service.h delete mode 100644 src/fe-gtk/palette.c create mode 100644 src/fe-gtk/theme/tests/test-theme-access-routing.c create mode 100644 src/fe-gtk/theme/tests/test-theme-application-input-style.c create mode 100644 src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c create mode 100644 src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c create mode 100644 src/fe-gtk/theme/tests/test-theme-manager-policy.c create mode 100644 src/fe-gtk/theme/tests/test-theme-runtime-persistence.c create mode 100644 src/fe-gtk/theme/theme-access.c create mode 100644 src/fe-gtk/theme/theme-access.h create mode 100644 src/fe-gtk/theme/theme-application.c create mode 100644 src/fe-gtk/theme/theme-application.h create mode 100644 src/fe-gtk/theme/theme-css.c create mode 100644 src/fe-gtk/theme/theme-css.h create mode 100644 src/fe-gtk/theme/theme-gtk.h create mode 100644 src/fe-gtk/theme/theme-manager.c create mode 100644 src/fe-gtk/theme/theme-manager.h create mode 100644 src/fe-gtk/theme/theme-palette.c create mode 100644 src/fe-gtk/theme/theme-palette.h create mode 100644 src/fe-gtk/theme/theme-policy.c create mode 100644 src/fe-gtk/theme/theme-policy.h create mode 100644 src/fe-gtk/theme/theme-preferences.c create mode 100644 src/fe-gtk/theme/theme-preferences.h create mode 100644 src/fe-gtk/theme/theme-runtime.c create mode 100644 src/fe-gtk/theme/theme-runtime.h diff --git a/src/common/cfgfiles.c b/src/common/cfgfiles.c index 4dc10533..f6c91574 100644 --- a/src/common/cfgfiles.c +++ b/src/common/cfgfiles.c @@ -258,12 +258,13 @@ int cfg_get_color (char *cfg, char *var, guint16 *r, guint16 *g, guint16 *b) { char str[128]; + int matched; if (!cfg_get_str (cfg, var, str, sizeof (str))) return 0; - sscanf (str, "%04hx %04hx %04hx", r, g, b); - return 1; + matched = sscanf (str, "%04hx %04hx %04hx", r, g, b); + return matched == 3; } int diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 025ef20a..c67f589a 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -45,6 +45,7 @@ + @@ -75,6 +76,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index f3b3b822..f220d9d3 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -119,6 +119,9 @@ Source Files\sysinfo + + Header Files + @@ -202,6 +205,9 @@ Source Files\sysinfo\win32 + + Source Files + diff --git a/src/common/meson.build b/src/common/meson.build index 0d097490..50e2422e 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -3,6 +3,7 @@ common_sources = [ 'chanopt.c', 'ctcp.c', 'dcc.c', + 'theme-service.c', 'zoitechat.c', 'history.c', 'ignore.c', diff --git a/src/common/theme-service.c b/src/common/theme-service.c new file mode 100644 index 00000000..f0e5aa5f --- /dev/null +++ b/src/common/theme-service.c @@ -0,0 +1,176 @@ +#include + +#include +#include + +#include "cfgfiles.h" +#include "zoitechat.h" +#include "theme-service.h" + +static zoitechat_theme_post_apply_callback zoitechat_theme_post_apply_cb; + +static gboolean +zoitechat_theme_service_copy_file (const char *src, const char *dest, GError **error) +{ + char *data = NULL; + gsize len = 0; + + if (!g_file_get_contents (src, &data, &len, error)) + return FALSE; + + if (!g_file_set_contents (dest, data, len, error)) + { + g_free (data); + return FALSE; + } + + g_free (data); + return TRUE; +} + +char * +zoitechat_theme_service_get_themes_dir (void) +{ + return g_build_filename (get_xdir (), "themes", NULL); +} + +static gboolean +zoitechat_theme_service_validate (const char *theme_name, + char **colors_src, + char **events_src, + GError **error) +{ + char *themes_dir; + char *theme_dir; + + if (!theme_name || !*theme_name) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("No theme name specified.")); + return FALSE; + } + + themes_dir = zoitechat_theme_service_get_themes_dir (); + theme_dir = g_build_filename (themes_dir, theme_name, NULL); + g_free (themes_dir); + + *colors_src = g_build_filename (theme_dir, "colors.conf", NULL); + *events_src = g_build_filename (theme_dir, "pevents.conf", NULL); + + if (!g_file_test (*colors_src, G_FILE_TEST_IS_REGULAR)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("This theme is missing a colors.conf file.")); + g_free (*events_src); + g_free (*colors_src); + *events_src = NULL; + *colors_src = NULL; + g_free (theme_dir); + return FALSE; + } + + g_free (theme_dir); + return TRUE; +} + +gboolean +zoitechat_theme_service_apply (const char *theme_name, GError **error) +{ + char *colors_src = NULL; + char *colors_dest = NULL; + char *events_src = NULL; + char *events_dest = NULL; + gboolean ok = FALSE; + + if (!zoitechat_theme_service_validate (theme_name, &colors_src, &events_src, error)) + return FALSE; + + colors_dest = g_build_filename (get_xdir (), "colors.conf", NULL); + events_dest = g_build_filename (get_xdir (), "pevents.conf", NULL); + + if (!zoitechat_theme_service_copy_file (colors_src, colors_dest, error)) + goto cleanup; + + if (g_file_test (events_src, G_FILE_TEST_IS_REGULAR)) + { + if (!zoitechat_theme_service_copy_file (events_src, events_dest, error)) + goto cleanup; + } + else if (g_file_test (events_dest, G_FILE_TEST_EXISTS)) + { + if (g_unlink (events_dest) != 0) + { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + _("Failed to remove existing event settings.")); + goto cleanup; + } + } + + zoitechat_theme_service_run_post_apply_callback (); + ok = TRUE; + +cleanup: + g_free (events_dest); + g_free (events_src); + g_free (colors_dest); + g_free (colors_src); + return ok; +} + +GStrv +zoitechat_theme_service_discover_themes (void) +{ + char *themes_dir; + GDir *dir; + const char *name; + GPtrArray *themes; + GStrv result; + + themes_dir = zoitechat_theme_service_get_themes_dir (); + if (!g_file_test (themes_dir, G_FILE_TEST_IS_DIR)) + g_mkdir_with_parents (themes_dir, 0700); + + themes = g_ptr_array_new_with_free_func (g_free); + dir = g_dir_open (themes_dir, 0, NULL); + if (dir) + { + while ((name = g_dir_read_name (dir))) + { + char *theme_dir = g_build_filename (themes_dir, name, NULL); + char *colors_path; + + if (!g_file_test (theme_dir, G_FILE_TEST_IS_DIR)) + { + g_free (theme_dir); + continue; + } + + colors_path = g_build_filename (theme_dir, "colors.conf", NULL); + if (g_file_test (colors_path, G_FILE_TEST_IS_REGULAR)) + g_ptr_array_add (themes, g_strdup (name)); + + g_free (colors_path); + g_free (theme_dir); + } + g_dir_close (dir); + } + + g_ptr_array_sort (themes, (GCompareFunc) g_strcmp0); + g_ptr_array_add (themes, NULL); + result = (GStrv) g_ptr_array_free (themes, FALSE); + g_free (themes_dir); + return result; +} + +void +zoitechat_theme_service_set_post_apply_callback (zoitechat_theme_post_apply_callback callback) +{ + zoitechat_theme_post_apply_cb = callback; +} + +void +zoitechat_theme_service_run_post_apply_callback (void) +{ + if (zoitechat_theme_post_apply_cb) + zoitechat_theme_post_apply_cb (); +} diff --git a/src/common/theme-service.h b/src/common/theme-service.h new file mode 100644 index 00000000..21208cf2 --- /dev/null +++ b/src/common/theme-service.h @@ -0,0 +1,12 @@ +#ifndef ZOITECHAT_THEME_SERVICE_H +#define ZOITECHAT_THEME_SERVICE_H + +#include + +char *zoitechat_theme_service_get_themes_dir (void); +GStrv zoitechat_theme_service_discover_themes (void); +gboolean zoitechat_theme_service_apply (const char *theme_name, GError **error); +void zoitechat_theme_service_set_post_apply_callback (void (*callback) (void)); +void zoitechat_theme_service_run_post_apply_callback (void); + +#endif diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index 6c97bada..e9a0fe74 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -54,6 +54,7 @@ #include "text.h" #include "url.h" #include "zoitechatc.h" +#include "theme-service.h" #if ! GLIB_CHECK_VERSION (2, 36, 0) #include /* for g_type_init() */ @@ -251,83 +252,22 @@ zoitechat_remote_win32 (void) } #endif - -static gboolean -zoitechat_copy_theme_file (const char *src, const char *dest, GError **error) +void +zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback) { - char *data = NULL; - gsize len = 0; + zoitechat_theme_service_set_post_apply_callback (callback); +} - if (!g_file_get_contents (src, &data, &len, error)) - return FALSE; - - if (!g_file_set_contents (dest, data, len, error)) - { - g_free (data); - return FALSE; - } - - g_free (data); - return TRUE; +void +zoitechat_run_theme_post_apply_callback (void) +{ + zoitechat_theme_service_run_post_apply_callback (); } gboolean zoitechat_apply_theme (const char *theme_name, GError **error) { - char *theme_dir; - char *colors_src; - char *colors_dest; - char *events_src; - char *events_dest; - gboolean ok = FALSE; - - if (!theme_name || !*theme_name) - { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, - _("No theme name specified.")); - return FALSE; - } - - theme_dir = g_build_filename (get_xdir (), "themes", theme_name, NULL); - colors_src = g_build_filename (theme_dir, "colors.conf", NULL); - colors_dest = g_build_filename (get_xdir (), "colors.conf", NULL); - events_src = g_build_filename (theme_dir, "pevents.conf", NULL); - events_dest = g_build_filename (get_xdir (), "pevents.conf", NULL); - - if (!g_file_test (colors_src, G_FILE_TEST_IS_REGULAR)) - { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, - _("This theme is missing a colors.conf file.")); - goto cleanup; - } - - if (!zoitechat_copy_theme_file (colors_src, colors_dest, error)) - goto cleanup; - - if (g_file_test (events_src, G_FILE_TEST_IS_REGULAR)) - { - if (!zoitechat_copy_theme_file (events_src, events_dest, error)) - goto cleanup; - } - else if (g_file_test (events_dest, G_FILE_TEST_EXISTS)) - { - if (g_unlink (events_dest) != 0) - { - g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), - _("Failed to remove existing event settings.")); - goto cleanup; - } - } - - ok = TRUE; - -cleanup: - g_free (events_dest); - g_free (events_src); - g_free (colors_dest); - g_free (colors_src); - g_free (theme_dir); - return ok; + return zoitechat_theme_service_apply (theme_name, error); } gboolean @@ -352,7 +292,7 @@ zoitechat_import_theme (const char *path, GError **error) return FALSE; } - themes_dir = g_build_filename (get_xdir (), "themes", NULL); + themes_dir = zoitechat_theme_service_get_themes_dir (); basename = g_path_get_basename (path); if (!basename || basename[0] == '\0') { @@ -825,7 +765,6 @@ irc_init (session *sess) { message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); fe_message (message, FE_MSG_INFO); - handle_command (sess, "gui apply", FALSE); g_free (message); } else @@ -877,8 +816,7 @@ irc_init (session *sess) { message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename); fe_message (message, FE_MSG_INFO); - handle_command (sess, "gui apply", FALSE); - g_free (message); + g_free (message); } else { diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 4c0f11fe..882476ce 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -32,6 +32,9 @@ gboolean zoitechat_theme_path_from_arg (const char *arg, char **path_out); gboolean zoitechat_import_theme (const char *path, GError **error); gboolean zoitechat_apply_theme (const char *theme_name, GError **error); +typedef void (*zoitechat_theme_post_apply_callback) (void); +void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback); +void zoitechat_run_theme_post_apply_callback (void); #ifdef USE_OPENSSL #ifdef __APPLE__ diff --git a/src/fe-gtk/chanview-tabs.c b/src/fe-gtk/chanview-tabs.c index 60643cef..b8e7cd0e 100644 --- a/src/fe-gtk/chanview-tabs.c +++ b/src/fe-gtk/chanview-tabs.c @@ -36,6 +36,21 @@ static int ignore_toggle = FALSE; static int tab_left_is_moving = 0; static int tab_right_is_moving = 0; +typedef struct +{ + GtkAdjustment *adj; + gdouble current_value; + gdouble target_value; + gint direction; + gdouble step_size; + guint source_id; + int *moving_flag; + gboolean is_left; +} tab_scroll_animation; + +static tab_scroll_animation *tab_left_animation; +static tab_scroll_animation *tab_right_animation; + /* userdata for gobjects used here: * * tab (togglebuttons inside boxes): @@ -149,6 +164,92 @@ tab_search_offset (GtkWidget *inner, gint start_offset, return 0; } +static gboolean +tab_scroll_animation_tick (gpointer userdata) +{ + tab_scroll_animation *animation = userdata; + gboolean reached_target; + + animation->current_value += animation->step_size * animation->direction; + + if (animation->direction < 0) + reached_target = animation->current_value <= animation->target_value; + else + reached_target = animation->current_value >= animation->target_value; + + if (reached_target) + animation->current_value = animation->target_value; + + gtk_adjustment_set_value (animation->adj, animation->current_value); + + if (!reached_target) + return G_SOURCE_CONTINUE; + + *animation->moving_flag = 0; + animation->source_id = 0; + + if (animation->is_left) + tab_left_animation = NULL; + else + tab_right_animation = NULL; + + g_object_unref (animation->adj); + g_free (animation); + + return G_SOURCE_REMOVE; +} + +static void +tab_scroll_animation_cancel (tab_scroll_animation **animation) +{ + if (*animation == NULL) + return; + + if ((*animation)->source_id != 0) + g_source_remove ((*animation)->source_id); + + *(*animation)->moving_flag = 0; + g_object_unref ((*animation)->adj); + g_free (*animation); + *animation = NULL; +} + +static void +tab_scroll_animation_start (tab_scroll_animation **slot, GtkAdjustment *adj, + gdouble current_value, gdouble target_value, + gint direction, int *moving_flag, + gboolean is_left) +{ + tab_scroll_animation *animation; + gdouble distance; + gdouble frames; + + distance = target_value - current_value; + if (distance < 0.0) + distance = -distance; + + if (distance <= 0.0) + { + gtk_adjustment_set_value (adj, target_value); + *moving_flag = 0; + return; + } + + animation = g_new0 (tab_scroll_animation, 1); + animation->adj = g_object_ref (adj); + animation->current_value = current_value; + animation->target_value = target_value; + animation->direction = direction; + frames = 12.0; + animation->step_size = distance / MAX (1.0, frames); + animation->moving_flag = moving_flag; + animation->is_left = is_left; + + *moving_flag = 1; + animation->source_id = g_timeout_add (16, tab_scroll_animation_tick, animation); + *slot = animation; +} + static void tab_scroll_left_up_clicked (GtkWidget *widget, chanview *cv) { @@ -157,7 +258,6 @@ tab_scroll_left_up_clicked (GtkWidget *widget, chanview *cv) gfloat new_value; GtkWidget *inner; GdkWindow *parent_win; - gdouble i; inner = ((tabview *)cv)->inner; parent_win = gtk_widget_get_window (gtk_widget_get_parent (inner)); @@ -177,25 +277,15 @@ tab_scroll_left_up_clicked (GtkWidget *widget, chanview *cv) if (new_value + viewport_size > gtk_adjustment_get_upper (adj)) new_value = gtk_adjustment_get_upper (adj) - viewport_size; - if (!tab_left_is_moving) + if (tab_left_is_moving) { - tab_left_is_moving = 1; - - for (i = gtk_adjustment_get_value (adj); ((i > new_value) && (tab_left_is_moving)); i -= 0.1) - { - gtk_adjustment_set_value (adj, i); - while (g_main_context_pending (NULL)) - g_main_context_iteration (NULL, TRUE); - } - - gtk_adjustment_set_value (adj, new_value); - - tab_left_is_moving = 0; - } - else - { - tab_left_is_moving = 0; + tab_scroll_animation_cancel (&tab_left_animation); + return; } + + tab_scroll_animation_start (&tab_left_animation, adj, + gtk_adjustment_get_value (adj), new_value, + -1, &tab_left_is_moving, TRUE); } static void @@ -206,7 +296,6 @@ tab_scroll_right_down_clicked (GtkWidget *widget, chanview *cv) gfloat new_value; GtkWidget *inner; GdkWindow *parent_win; - gdouble i; inner = ((tabview *)cv)->inner; parent_win = gtk_widget_get_window (gtk_widget_get_parent (inner)); @@ -226,25 +315,15 @@ tab_scroll_right_down_clicked (GtkWidget *widget, chanview *cv) if (new_value == 0 || new_value + viewport_size > gtk_adjustment_get_upper (adj)) new_value = gtk_adjustment_get_upper (adj) - viewport_size; - if (!tab_right_is_moving) + if (tab_right_is_moving) { - tab_right_is_moving = 1; - - for (i = gtk_adjustment_get_value (adj); ((i < new_value) && (tab_right_is_moving)); i += 0.1) - { - gtk_adjustment_set_value (adj, i); - while (g_main_context_pending (NULL)) - g_main_context_iteration (NULL, TRUE); - } - - gtk_adjustment_set_value (adj, new_value); - - tab_right_is_moving = 0; - } - else - { - tab_right_is_moving = 0; + tab_scroll_animation_cancel (&tab_right_animation); + return; } + + tab_scroll_animation_start (&tab_right_animation, adj, + gtk_adjustment_get_value (adj), new_value, + 1, &tab_right_is_moving, FALSE); } static gboolean diff --git a/src/fe-gtk/chanview-tree.c b/src/fe-gtk/chanview-tree.c index 971af542..4b45cc7e 100644 --- a/src/fe-gtk/chanview-tree.c +++ b/src/fe-gtk/chanview-tree.c @@ -27,6 +27,8 @@ typedef struct #include +#include "theme/theme-access.h" + static void /* row-activated, when a row is double clicked */ cv_tree_activated_cb (GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer data) @@ -140,10 +142,19 @@ cv_tree_init (chanview *cv) cv->font_desc ) { - gtkutil_apply_palette (view, &colors[COL_BG], &colors[COL_FG], + GdkRGBA bg; + GdkRGBA fg; + const GdkRGBA *bg_color = NULL; + const GdkRGBA *fg_color = NULL; + + if (theme_get_color (THEME_TOKEN_TEXT_BACKGROUND, &bg)) + bg_color = &bg; + if (theme_get_color (THEME_TOKEN_TEXT_FOREGROUND, &fg)) + fg_color = &fg; + gtkutil_apply_palette (view, bg_color, fg_color, cv->font_desc); } - /*gtk_widget_modify_base (view, GTK_STATE_NORMAL, &colors[COL_BG]);*/ + /*gtk_widget_modify_base (view, GTK_STATE_NORMAL, &colors[THEME_LEGACY_TEXT_BACKGROUND]);*/ gtk_widget_set_can_focus (view, FALSE); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (view), FALSE); diff --git a/src/fe-gtk/chanview.c b/src/fe-gtk/chanview.c index 3edb3ebe..ae789f94 100644 --- a/src/fe-gtk/chanview.c +++ b/src/fe-gtk/chanview.c @@ -28,7 +28,7 @@ #include "maingui.h" #include "gtkutil.h" #include "chanview.h" -#include "palette.h" +#include "theme/theme-manager.h" /* treeStore columns */ #define COL_NAME 0 /* (char *) */ @@ -75,6 +75,7 @@ struct _chanview unsigned int sorted:1; unsigned int vertical:1; unsigned int use_icons:1; + guint theme_listener_id; }; struct _chan @@ -91,6 +92,7 @@ struct _chan static chan *cv_find_chan_by_number (chanview *cv, int num); static int cv_find_number_of_chan (chanview *cv, chan *find_ch); +static void cv_find_neighbors_for_removal (chanview *cv, chan *find_ch, chan **left_ch, chan **first_ch); /* ======= TABS ======= */ @@ -127,15 +129,17 @@ chanview_apply_theme (chanview *cv) if (input_style) font = input_style->font_desc; - if (fe_dark_mode_is_enabled () || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_LIGHT) - { - gtkutil_apply_palette (w, &colors[COL_BG], &colors[COL_FG], font); - } - else - { - /* Keep list font in sync while reverting colors to theme defaults. */ - gtkutil_apply_palette (w, NULL, NULL, font); - } + theme_manager_apply_channel_tree_style (w, + theme_manager_get_channel_tree_palette_behavior (font)); +} + +static void +chanview_theme_changed (const ThemeChangedEvent *event, gpointer userdata) +{ + chanview *cv = userdata; + + (void) event; + chanview_apply_theme (cv); } static char * @@ -278,6 +282,12 @@ chanview_destroy_store (chanview *cv) /* free every (chan *) in the store */ static void chanview_destroy (chanview *cv) { + if (cv->theme_listener_id) + { + theme_listener_unregister (cv->theme_listener_id); + cv->theme_listener_id = 0; + } + if (cv->func_cleanup) cv->func_cleanup (cv); @@ -312,6 +322,7 @@ chanview_new (int type, int trunc_len, gboolean sort, gboolean use_icons, cv->use_icons = use_icons; gtk_widget_show (cv->box); chanview_set_impl (cv, type); + cv->theme_listener_id = theme_listener_register ("chanview", chanview_theme_changed, cv); g_signal_connect (G_OBJECT (cv->box), "destroy", G_CALLBACK (chanview_box_destroy_cb), cv); @@ -608,6 +619,45 @@ cv_find_chan_by_number (chanview *cv, int num) return NULL; } +static void +cv_find_neighbors_for_removal (chanview *cv, chan *find_ch, chan **left_ch, chan **first_ch) +{ + GtkTreeIter iter, inner; + chan *ch; + chan *prev = NULL; + + *left_ch = NULL; + *first_ch = NULL; + + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (cv->store), &iter)) + { + do + { + gtk_tree_model_get (GTK_TREE_MODEL (cv->store), &iter, COL_CHAN, &ch, -1); + if (ch == find_ch) + *left_ch = prev; + else if (*first_ch == NULL) + *first_ch = ch; + prev = ch; + + if (gtk_tree_model_iter_children (GTK_TREE_MODEL (cv->store), &inner, &iter)) + { + do + { + gtk_tree_model_get (GTK_TREE_MODEL (cv->store), &inner, COL_CHAN, &ch, -1); + if (ch == find_ch) + *left_ch = prev; + else if (*first_ch == NULL) + *first_ch = ch; + prev = ch; + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (cv->store), &inner)); + } + } + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (cv->store), &iter)); + } +} + static void chan_emancipate_children (chan *ch) { @@ -638,7 +688,7 @@ gboolean chan_remove (chan *ch, gboolean force) { chan *new_ch; - int i, num; + chan *first_ch; extern int zoitechat_is_quitting; if (zoitechat_is_quitting) /* avoid lots of looping on exit */ @@ -658,25 +708,14 @@ chan_remove (chan *ch, gboolean force) { ch->cv->focused = NULL; - /* try to move the focus to some other valid channel */ - num = cv_find_number_of_chan (ch->cv, ch); - /* move to the one left of the closing tab */ - new_ch = cv_find_chan_by_number (ch->cv, num - 1); + cv_find_neighbors_for_removal (ch->cv, ch, &new_ch, &first_ch); if (new_ch && new_ch != ch) { - chan_focus (new_ch); /* this'll will set ch->cv->focused for us too */ - } else + chan_focus (new_ch); + } + else if (first_ch && first_ch != ch) { - /* if it fails, try focus from tab 0 and up */ - for (i = 0; i < ch->cv->size; i++) - { - new_ch = cv_find_chan_by_number (ch->cv, i); - if (new_ch && new_ch != ch) - { - chan_focus (new_ch); /* this'll will set ch->cv->focused for us too */ - break; - } - } + chan_focus (first_ch); } } diff --git a/src/fe-gtk/dccgui.c b/src/fe-gtk/dccgui.c index 6aebff24..fa3803ae 100644 --- a/src/fe-gtk/dccgui.c +++ b/src/fe-gtk/dccgui.c @@ -34,8 +34,9 @@ #include "../common/util.h" #include "../common/network.h" #include "gtkutil.h" -#include "palette.h" +#include "theme/theme-gtk.h" #include "maingui.h" +#include "theme/theme-access.h" #define ICON_DCC_CANCEL "dialog-cancel" #define ICON_DCC_ACCEPT "dialog-apply" @@ -55,7 +56,7 @@ enum /* DCC SEND/RECV */ COL_ETA, COL_NICK, COL_DCC, /* struct DCC * */ - COL_COLOR, /* PaletteColor */ + COL_COLOR, /* GdkRGBA */ N_COLUMNS }; @@ -67,7 +68,7 @@ enum /* DCC CHAT */ CCOL_SENT, CCOL_START, CCOL_DCC, /* struct DCC * */ - CCOL_COLOR, /* PaletteColor * */ + CCOL_COLOR, /* GdkRGBA * */ CN_COLUMNS }; @@ -177,15 +178,15 @@ fe_dcc_send_filereq (struct session *sess, char *nick, int maxcps, int passive) static void dcc_store_color (GtkListStore *store, GtkTreeIter *iter, int column, int color_index) { - const PaletteColor *color = NULL; + GdkRGBA rgba; + const GdkRGBA *color = NULL; - if (color_index != 1) - color = &colors[color_index]; + if (color_index != 1 && theme_get_mirc_color ((unsigned int) color_index, &rgba)) + color = &rgba; if (color) { - GdkRGBA rgba = *color; - gtk_list_store_set (store, iter, column, &rgba, -1); + gtk_list_store_set (store, iter, column, color, -1); } else { @@ -753,7 +754,7 @@ dcc_add_column (GtkWidget *tree, int textcol, int colorcol, char *title, gboolea if (right_justified) g_object_set (G_OBJECT (renderer), "xalign", (float) 1.0, NULL); gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (tree), -1, title, renderer, - "text", textcol, PALETTE_FOREGROUND_PROPERTY, colorcol, + "text", textcol, THEME_GTK_FOREGROUND_PROPERTY, colorcol, NULL); gtk_cell_renderer_text_set_fixed_height_from_font (GTK_CELL_RENDERER_TEXT (renderer), 1); } @@ -839,7 +840,7 @@ fe_dcc_open_recv_win (int passive) store = gtk_list_store_new (N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, - G_TYPE_STRING, G_TYPE_POINTER, PALETTE_GDK_TYPE); + G_TYPE_STRING, G_TYPE_POINTER, THEME_GTK_COLOR_TYPE); view = gtkutil_treeview_new (vbox, GTK_TREE_MODEL (store), NULL, -1); view_scrolled = gtk_widget_get_parent (view); gtk_widget_set_hexpand (view_scrolled, TRUE); @@ -1111,7 +1112,7 @@ fe_dcc_open_chat_win (int passive) store = gtk_list_store_new (CN_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, - G_TYPE_POINTER, PALETTE_GDK_TYPE); + G_TYPE_POINTER, THEME_GTK_COLOR_TYPE); view = gtkutil_treeview_new (vbox, GTK_TREE_MODEL (store), NULL, -1); scroll = gtk_widget_get_parent (view); gtk_box_set_child_packing (GTK_BOX (vbox), scroll, TRUE, TRUE, 0, GTK_PACK_START); diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c index 7bb5e37d..4126a543 100644 --- a/src/fe-gtk/fe-gtk.c +++ b/src/fe-gtk/fe-gtk.c @@ -48,7 +48,7 @@ #include "chanlist.h" #include "joind.h" #include "xtext.h" -#include "palette.h" +#include "theme/theme-gtk.h" #include "menu.h" #include "notifygui.h" #include "textgui.h" @@ -57,6 +57,8 @@ #include "urlgrab.h" #include "setup.h" #include "plugin-notification.h" +#include "theme/theme-manager.h" +#include "theme/theme-application.h" #ifdef USE_LIBCANBERRA #include @@ -429,37 +431,12 @@ fe_args (int argc, char *argv[]) return -1; } -const char cursor_color_rc[] = - "style \"xc-ib-st\"" - "{" - "GtkEntry::cursor-color=\"#%02x%02x%02x\"" - "}" - "widget \"*.zoitechat-inputbox\" style : application \"xc-ib-st\""; - -InputStyle *create_input_style (InputStyle *style); - -static const char adwaita_workaround_rc[] = - "style \"zoitechat-input-workaround\"" - "{" - "engine \"pixmap\" {" - "image {" - "function = FLAT_BOX\n" - "state = NORMAL\n" - "}" - "image {" - "function = FLAT_BOX\n" - "state = ACTIVE\n" - "}" - "}" - "}" - "widget \"*.zoitechat-inputbox\" style \"zoitechat-input-workaround\""; - #ifdef G_OS_WIN32 #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif -static gboolean +gboolean fe_win32_high_contrast_is_enabled (void) { HIGHCONTRASTW hc; @@ -472,7 +449,7 @@ fe_win32_high_contrast_is_enabled (void) return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0; } -static gboolean +gboolean fe_win32_try_get_system_dark (gboolean *prefer_dark) { DWORD value = 1; @@ -502,7 +479,7 @@ fe_win32_try_get_system_dark (gboolean *prefer_dark) return TRUE; } -static void +void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) { HWND hwnd; @@ -525,7 +502,7 @@ fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) sizeof (use_dark)); } #else -static void +void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) { (void) window; @@ -533,163 +510,14 @@ fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode) } #endif -static gboolean -fe_system_prefers_dark (void) -{ - GtkSettings *settings = gtk_settings_get_default (); - gboolean prefer_dark = FALSE; - char *theme_name = NULL; -#ifdef G_OS_WIN32 - gboolean have_win_pref = FALSE; - - if (fe_win32_high_contrast_is_enabled ()) - return FALSE; -#endif - - if (!settings) - return FALSE; - -#ifdef G_OS_WIN32 - have_win_pref = fe_win32_try_get_system_dark (&prefer_dark); - if (!have_win_pref) -#endif - if (g_object_class_find_property (G_OBJECT_GET_CLASS (settings), - "gtk-application-prefer-dark-theme")) - { - g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark, NULL); - } - - if (!prefer_dark) - { - g_object_get (settings, "gtk-theme-name", &theme_name, NULL); - if (theme_name) - { - char *lower = g_ascii_strdown (theme_name, -1); - if (g_str_has_suffix (lower, "-dark") || g_strrstr (lower, "dark")) - prefer_dark = TRUE; - g_free (lower); - g_free (theme_name); - } - } - - return prefer_dark; -} - static gboolean auto_dark_mode_enabled = FALSE; -#ifdef G_OS_WIN32 -static void -fe_apply_windows_theme (gboolean dark) -{ - GtkSettings *settings = gtk_settings_get_default (); - - if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), - "gtk-application-prefer-dark-theme")) - { - g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL); - } - - { - static GtkCssProvider *win_theme_provider = NULL; - GdkScreen *screen = gdk_screen_get_default (); - const char *css = - "window.zoitechat-dark, .zoitechat-dark {" - "background-color: #202020;" - "color: #f0f0f0;" - "}" - "window.zoitechat-light, .zoitechat-light {" - "background-color: #f6f6f6;" - "color: #101010;" - "}"; - - if (!win_theme_provider) - win_theme_provider = gtk_css_provider_new (); - - gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL); - if (screen) - gtk_style_context_add_provider_for_screen ( - screen, - GTK_STYLE_PROVIDER (win_theme_provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - } -} -#endif - -static void -fe_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer data) -{ - gboolean enabled; - - (void) settings; - (void) pspec; - (void) data; - - if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO) - return; - - enabled = fe_system_prefers_dark (); - if (enabled == auto_dark_mode_enabled) - return; - - auto_dark_mode_enabled = enabled; - fe_apply_theme_for_mode (ZOITECHAT_DARK_MODE_AUTO, NULL); - setup_apply_real (0, TRUE, FALSE, FALSE); -} - void fe_set_auto_dark_mode_state (gboolean enabled) { auto_dark_mode_enabled = enabled; } -void -fe_refresh_auto_dark_mode (void) -{ - fe_auto_dark_mode_changed (NULL, NULL, NULL); -} - -gboolean -fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed) -{ - gboolean enabled = fe_dark_mode_is_enabled_for (mode); - gboolean changed = palette_apply_dark_mode (enabled); - -#ifdef G_OS_WIN32 - fe_apply_windows_theme (enabled); -#endif - - if (palette_changed) - *palette_changed = changed; - - if (input_style) - create_input_style (input_style); - - return enabled; -} - -void -fe_apply_theme_to_toplevel (GtkWidget *window) -{ - if (!window) - return; - -#ifdef G_OS_WIN32 - { - GtkStyleContext *context = gtk_widget_get_style_context (window); - gboolean dark = fe_dark_mode_is_enabled (); - - if (context) - { - gtk_style_context_remove_class (context, "zoitechat-dark"); - gtk_style_context_remove_class (context, "zoitechat-light"); - gtk_style_context_add_class (context, dark ? "zoitechat-dark" : "zoitechat-light"); - } - } -#endif - - fe_win32_apply_native_titlebar (window, fe_dark_mode_is_enabled ()); -} - gboolean fe_dark_mode_is_enabled_for (unsigned int mode) { @@ -711,174 +539,10 @@ fe_dark_mode_is_enabled (void) return fe_dark_mode_is_enabled_for (prefs.hex_gui_dark_mode); } -InputStyle * -create_input_style (InputStyle *style) -{ - char buf[256]; - static int done_rc = FALSE; - static GtkCssProvider *input_css_provider = NULL; - static char *last_theme_name = NULL; - static gboolean last_dark_mode = FALSE; - static gboolean last_input_style = FALSE; - static gboolean last_colors_set = FALSE; - static guint16 last_fg_red; - static guint16 last_fg_green; - static guint16 last_fg_blue; - static guint16 last_bg_red; - static guint16 last_bg_green; - static guint16 last_bg_blue; - - if (!style) - style = g_new0 (InputStyle, 1); - - if (style->font_desc) - pango_font_description_free (style->font_desc); - style->font_desc = pango_font_description_from_string (prefs.hex_text_font); - - /* fall back */ - if (pango_font_description_get_size (style->font_desc) == 0) - { - g_snprintf (buf, sizeof (buf), _("Failed to open font:\n\n%s"), prefs.hex_text_font); - fe_message (buf, FE_MSG_ERROR); - pango_font_description_free (style->font_desc); - style->font_desc = pango_font_description_from_string ("sans 11"); - } - - if (prefs.hex_gui_input_style) - { - GtkSettings *settings = gtk_settings_get_default (); - GdkScreen *screen = gdk_screen_get_default (); - char *theme_name; - char cursor_rc[sizeof (cursor_color_rc)]; - char cursor_color[8]; - const char *cursor_color_start = NULL; - guint16 fg_red; - guint16 fg_green; - guint16 fg_blue; - guint16 bg_red; - guint16 bg_green; - guint16 bg_blue; - gboolean dark_mode = fe_dark_mode_is_enabled (); - gboolean needs_reload; - - g_object_get (settings, "gtk-theme-name", &theme_name, NULL); - - palette_color_get_rgb16 (&colors[COL_FG], &fg_red, &fg_green, &fg_blue); - palette_color_get_rgb16 (&colors[COL_BG], &bg_red, &bg_green, &bg_blue); - needs_reload = !done_rc - || !last_input_style - || last_dark_mode != dark_mode - || g_strcmp0 (last_theme_name, theme_name) != 0 - || !last_colors_set - || last_fg_red != fg_red - || last_fg_green != fg_green - || last_fg_blue != fg_blue - || last_bg_red != bg_red - || last_bg_green != bg_green - || last_bg_blue != bg_blue; - - if (needs_reload) - { - if (!input_css_provider) - input_css_provider = gtk_css_provider_new (); - - g_snprintf (buf, sizeof (buf), "#%02x%02x%02x", - (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8)); - g_snprintf (cursor_rc, sizeof (cursor_rc), cursor_color_rc, - (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8)); - cursor_color_start = g_strstr_len (cursor_rc, -1, "cursor-color=\""); - if (cursor_color_start) - { - cursor_color_start += strlen ("cursor-color=\""); - g_strlcpy (cursor_color, cursor_color_start, sizeof (cursor_color)); - cursor_color[7] = '\0'; - } - else - { - g_strlcpy (cursor_color, buf, sizeof (cursor_color)); - } - { - GString *css = g_string_new ("#zoitechat-inputbox {"); - - /* GTK3 equivalents for adwaita_workaround_rc/cursor_color_rc. */ - if (adwaita_workaround_rc[0] != '\0' - && (g_str_has_prefix (theme_name, "Adwaita") - || g_str_has_prefix (theme_name, "Yaru"))) - g_string_append (css, "background-image: none;"); - - g_string_append_printf ( - css, - "background-color: #%02x%02x%02x;" - "color: #%02x%02x%02x;" - "caret-color: %s;" - "}" - "#zoitechat-inputbox text {" - "color: #%02x%02x%02x;" - "caret-color: %s;" - "}", - (bg_red >> 8), (bg_green >> 8), (bg_blue >> 8), - (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), - cursor_color, - (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), - cursor_color); - gtk_css_provider_load_from_data (input_css_provider, css->str, -1, NULL); - g_string_free (css, TRUE); - } - - if (screen) - gtk_style_context_add_provider_for_screen ( - screen, - GTK_STYLE_PROVIDER (input_css_provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - - done_rc = TRUE; - last_input_style = TRUE; - last_dark_mode = dark_mode; - last_colors_set = TRUE; - last_fg_red = fg_red; - last_fg_green = fg_green; - last_fg_blue = fg_blue; - last_bg_red = bg_red; - last_bg_green = bg_green; - last_bg_blue = bg_blue; - g_free (last_theme_name); - last_theme_name = g_strdup (theme_name); - } - - g_free (theme_name); - } - else - { - GdkScreen *screen = gdk_screen_get_default (); - - if (input_css_provider && screen) - { - gtk_style_context_remove_provider_for_screen ( - screen, - GTK_STYLE_PROVIDER (input_css_provider)); - } - g_clear_object (&input_css_provider); - g_clear_pointer (&last_theme_name, g_free); - done_rc = FALSE; - last_input_style = FALSE; - last_colors_set = FALSE; - } - - - return style; -} - void fe_init (void) { - GtkSettings *settings; - - palette_load (); - settings = gtk_settings_get_default (); - if (settings) - auto_dark_mode_enabled = fe_system_prefers_dark (); - - fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL); + theme_manager_init (); key_init (); pixmaps_init (); @@ -886,15 +550,8 @@ fe_init (void) gtkosx_application_set_dock_icon_pixbuf (osx_app, pix_zoitechat); #endif channelwin_pix = pixmap_load_from_file (prefs.hex_text_background); - input_style = create_input_style (input_style); + theme_application_reload_input_style (); - if (settings) - { - g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme", - G_CALLBACK (fe_auto_dark_mode_changed), NULL); - g_signal_connect (settings, "notify::gtk-theme-name", - G_CALLBACK (fe_auto_dark_mode_changed), NULL); - } } #ifdef HAVE_GTK_MAC @@ -1440,7 +1097,7 @@ fe_ctrl_gui (session *sess, fe_gui_action action, int arg) mg_detach (sess, arg); /* arg: 0=toggle 1=detach 2=attach */ break; case FE_GUI_APPLY: - setup_apply_real (TRUE, TRUE, TRUE, FALSE); + theme_manager_dispatch_changed (THEME_CHANGED_REASON_PIXMAP | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_LAYOUT | THEME_CHANGED_REASON_WIDGET_STYLE); } } @@ -1630,7 +1287,7 @@ uri_contains_forbidden_characters (const char *uri) { while (*uri) { - if (!g_ascii_isalnum (*uri) && !strchr ("-._~:/?#[]@!$&'()*+,;=", *uri)) + if (!g_ascii_isalnum (*uri) && !strchr ("-._~%:/?#[]@!$&'()*+,;=", *uri)) return TRUE; uri++; } diff --git a/src/fe-gtk/fe-gtk.h b/src/fe-gtk/fe-gtk.h index 779f8d80..3ddddeb0 100644 --- a/src/fe-gtk/fe-gtk.h +++ b/src/fe-gtk/fe-gtk.h @@ -114,6 +114,7 @@ typedef struct restore_gui /* information stored when this tab isn't front-most */ GtkListStore *user_model; /* for filling the GtkTreeView */ + GHashTable *user_row_refs; void *buffer; /* xtext_Buffer */ char *input_text; /* input text buffer (while not-front tab) */ char *topic_text; /* topic GtkEntry buffer */ @@ -178,6 +179,8 @@ typedef struct session_gui int pane_left_size; /*last position of the pane*/ int pane_right_size; + guint theme_window_listener_id; + guint theme_userlist_listener_id; guint16 is_tab; /* is tab or toplevel? */ guint16 ul_hidden; /* userlist hidden? */ @@ -190,9 +193,12 @@ extern cairo_surface_t *dialogwin_pix; gboolean fe_dark_mode_is_enabled (void); gboolean fe_dark_mode_is_enabled_for (unsigned int mode); void fe_set_auto_dark_mode_state (gboolean enabled); -void fe_refresh_auto_dark_mode (void); -gboolean fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed); -void fe_apply_theme_to_toplevel (GtkWidget *window); + +#ifdef G_OS_WIN32 +gboolean fe_win32_high_contrast_is_enabled (void); +gboolean fe_win32_try_get_system_dark (gboolean *prefer_dark); +#endif +void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark_mode); #define SPELL_ENTRY_GET_TEXT(e) ((char *)(gtk_entry_get_text (GTK_ENTRY(e)))) #define SPELL_ENTRY_SET_TEXT(e,txt) gtk_entry_set_text(GTK_ENTRY(e),txt) diff --git a/src/fe-gtk/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj index bfe15bc7..35fc5072 100644 --- a/src/fe-gtk/fe-gtk.vcxproj +++ b/src/fe-gtk/fe-gtk.vcxproj @@ -77,6 +77,15 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out- + + + + + + + + + @@ -96,7 +105,6 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out- - @@ -111,6 +119,14 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out- + + + + + + + + @@ -121,6 +137,12 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out- + + + + + + diff --git a/src/fe-gtk/fe-gtk.vcxproj.filters b/src/fe-gtk/fe-gtk.vcxproj.filters index e83ff18f..ad3d5540 100644 --- a/src/fe-gtk/fe-gtk.vcxproj.filters +++ b/src/fe-gtk/fe-gtk.vcxproj.filters @@ -99,6 +99,33 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + @@ -149,9 +176,6 @@ Source Files - - Source Files - Source Files @@ -197,6 +221,30 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -212,6 +260,24 @@ Resource Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + diff --git a/src/fe-gtk/fkeys.c b/src/fe-gtk/fkeys.c index 7fa11cbe..a7f35778 100644 --- a/src/fe-gtk/fkeys.c +++ b/src/fe-gtk/fkeys.c @@ -48,7 +48,9 @@ #include "gtkutil.h" #include "menu.h" #include "xtext.h" -#include "palette.h" +#include "theme/theme-access.h" +#include "theme/theme-manager.h" +#include "theme/theme-css.h" #include "maingui.h" #include "textgui.h" #include "fkeys.h" @@ -142,6 +144,53 @@ static int key_action_put_history (GtkWidget * wid, GdkEventKey * evt, static GSList *keybind_list = NULL; +#define KEY_DIALOG_THEME_LISTENER_ID_KEY "fkeys.theme-listener-id" + +static void +key_dialog_theme_apply (GtkWidget *window) +{ + GtkWidget *xtext; + XTextColor xtext_palette[XTEXT_COLS]; + + if (!window) + return; + + xtext = g_object_get_data (G_OBJECT (window), "xtext"); + if (!xtext) + return; + + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); + gtk_xtext_set_palette (GTK_XTEXT (xtext), xtext_palette); +} + +static void +key_dialog_theme_changed (const ThemeChangedEvent *event, gpointer userdata) +{ + GtkWidget *window = userdata; + + if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE)) + return; + + key_dialog_theme_apply (window); +} + +static void +key_dialog_theme_destroy_cb (GtkWidget *widget, gpointer userdata) +{ + guint listener_id; + + (void) userdata; + + listener_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), KEY_DIALOG_THEME_LISTENER_ID_KEY)); + if (listener_id) + { + theme_listener_unregister (listener_id); + g_object_set_data (G_OBJECT (widget), KEY_DIALOG_THEME_LISTENER_ID_KEY, NULL); + } +} + static gsize key_action_decode_escapes (const char *input, char *output) { @@ -748,7 +797,6 @@ key_dialog_treeview_new (GtkWidget *box) gtk_widget_set_name (view, "fkeys-treeview"); { GtkCssProvider *provider = gtk_css_provider_new (); - GtkStyleContext *context = gtk_widget_get_style_context (view); gtk_css_provider_load_from_data ( provider, @@ -760,8 +808,7 @@ key_dialog_treeview_new (GtkWidget *box) "}", -1, NULL); - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + theme_css_apply_widget_provider (view, GTK_STYLE_PROVIDER (provider)); g_object_unref (provider); } @@ -904,13 +951,16 @@ key_dialog_show () NULL, 600, 360, &vbox, 0); view = key_dialog_treeview_new (vbox); - palette_get_xtext_colors (xtext_palette, XTEXT_COLS); + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); xtext = gtk_xtext_new (xtext_palette, 0); gtk_box_pack_start (GTK_BOX (vbox), xtext, FALSE, TRUE, 2); gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font); g_object_set_data (G_OBJECT (key_dialog), "view", view); g_object_set_data (G_OBJECT (key_dialog), "xtext", xtext); + g_object_set_data (G_OBJECT (key_dialog), KEY_DIALOG_THEME_LISTENER_ID_KEY, + GUINT_TO_POINTER (theme_listener_register ("fkeys.window", key_dialog_theme_changed, key_dialog))); + g_signal_connect (G_OBJECT (key_dialog), "destroy", G_CALLBACK (key_dialog_theme_destroy_cb), NULL); box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); gtk_button_box_set_layout (GTK_BUTTON_BOX (box), GTK_BUTTONBOX_SPREAD); diff --git a/src/fe-gtk/gtkutil.c b/src/fe-gtk/gtkutil.c index 9d49b6c5..6af45925 100644 --- a/src/fe-gtk/gtkutil.c +++ b/src/fe-gtk/gtkutil.c @@ -44,6 +44,7 @@ #include "gtkutil.h" #include "icon-resolver.h" #include "pixmaps.h" +#include "theme/theme-manager.h" #ifdef WIN32 #include @@ -277,72 +278,7 @@ void gtkutil_apply_palette (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, const PangoFontDescription *font_desc) { - if (!widget) - return; - - { - static const char *class_name = "zoitechat-palette"; - GtkStyleContext *context = gtk_widget_get_style_context (widget); - GtkCssProvider *provider = g_object_get_data (G_OBJECT (widget), - "zoitechat-palette-provider"); - gboolean new_provider = FALSE; - GString *css; - gchar *bg_color = NULL; - gchar *fg_color = NULL; - - if (!bg && !fg && !font_desc) - { - gtk_style_context_remove_class (context, class_name); - if (provider) - { - gtk_style_context_remove_provider (context, GTK_STYLE_PROVIDER (provider)); - g_object_set_data (G_OBJECT (widget), "zoitechat-palette-provider", NULL); - } - return; - } - - if (!provider) - { - provider = gtk_css_provider_new (); - g_object_set_data_full (G_OBJECT (widget), "zoitechat-palette-provider", - provider, g_object_unref); - new_provider = TRUE; - } - - css = g_string_new ("."); - g_string_append (css, class_name); - g_string_append (css, " {"); - if (bg) - { - bg_color = gdk_rgba_to_string (bg); - g_string_append_printf (css, " background-color: %s;", bg_color); - } - if (fg) - { - fg_color = gdk_rgba_to_string (fg); - g_string_append_printf (css, " color: %s;", fg_color); - } - gtkutil_append_font_css (css, font_desc); - g_string_append (css, " }"); - g_string_append_printf (css, ".%s *:selected {", class_name); - if (bg) - g_string_append (css, " background-color: @theme_selected_bg_color;"); - if (fg) - g_string_append (css, " color: @theme_selected_fg_color;"); - g_string_append (css, " }"); - - gtk_css_provider_load_from_data (provider, css->str, -1, NULL); - if (new_provider) - { - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - } - gtk_style_context_add_class (context, class_name); - - g_string_free (css, TRUE); - g_free (bg_color); - g_free (fg_color); - } + theme_manager_apply_palette_widget (widget, bg, fg, font_desc); } static void diff --git a/src/fe-gtk/gtkutil.h b/src/fe-gtk/gtkutil.h index 4c7fd6cd..a31aea5b 100644 --- a/src/fe-gtk/gtkutil.h +++ b/src/fe-gtk/gtkutil.h @@ -22,7 +22,7 @@ #include #include "../common/fe.h" -#include "palette.h" +#include "theme/theme-gtk.h" typedef void (*filereqcallback) (void *, char *file); diff --git a/src/fe-gtk/icon-resolver.c b/src/fe-gtk/icon-resolver.c index b23441d5..1a825752 100644 --- a/src/fe-gtk/icon-resolver.c +++ b/src/fe-gtk/icon-resolver.c @@ -2,6 +2,7 @@ #include "fe-gtk.h" #include "icon-resolver.h" +#include "theme/theme-policy.h" #include "../common/cfgfiles.h" typedef struct @@ -249,28 +250,10 @@ icon_resolver_system_icon_name (IconResolverRole role, int item) IconResolverThemeVariant icon_resolver_detect_theme_variant (void) { - GtkSettings *settings; - gboolean prefer_dark = FALSE; - char *theme_name = NULL; - char *theme_name_lower = NULL; - IconResolverThemeVariant theme_variant = ICON_RESOLVER_THEME_LIGHT; + if (theme_policy_is_app_dark_mode_active ()) + return ICON_RESOLVER_THEME_DARK; - settings = gtk_settings_get_default (); - if (settings) - { - g_object_get (G_OBJECT (settings), "gtk-application-prefer-dark-theme", &prefer_dark, NULL); - g_object_get (G_OBJECT (settings), "gtk-theme-name", &theme_name, NULL); - } - - if (theme_name) - theme_name_lower = g_ascii_strdown (theme_name, -1); - if (prefer_dark || (theme_name_lower && g_strrstr (theme_name_lower, "dark"))) - theme_variant = ICON_RESOLVER_THEME_DARK; - - g_free (theme_name_lower); - g_free (theme_name); - - return theme_variant; + return ICON_RESOLVER_THEME_LIGHT; } static gboolean diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index cad767bd..4e19057b 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -41,10 +41,13 @@ #include "../common/cfgfiles.h" #include "fe-gtk.h" +#include "theme/theme-manager.h" +#include "theme/theme-css.h" #include "banlist.h" #include "gtkutil.h" #include "joind.h" -#include "palette.h" +#include "theme/theme-access.h" +#include "theme/theme-palette.h" #include "maingui.h" #include "menu.h" #include "fkeys.h" @@ -216,8 +219,7 @@ mg_apply_font_css (GtkWidget *widget, const PangoFontDescription *desc, g_string_free (css, TRUE); gtk_style_context_add_class (context, class_name); - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + theme_css_apply_widget_provider (widget, GTK_STYLE_PROVIDER (provider)); } static void @@ -405,7 +407,7 @@ mg_attr_list_create (const XTextColor *col, int size) static void mg_create_tab_colors (void) { - XTextColor gui_palette[MAX_COL + 1]; + XTextColor gui_palette[XTEXT_COLS]; if (plain_list) { @@ -416,12 +418,12 @@ mg_create_tab_colors (void) pango_attr_list_unref (away_list); } - palette_get_xtext_colors (gui_palette, G_N_ELEMENTS (gui_palette)); + theme_get_xtext_colors (gui_palette, G_N_ELEMENTS (gui_palette)); plain_list = mg_attr_list_create (NULL, prefs.hex_gui_tab_small); - newdata_list = mg_attr_list_create (&gui_palette[COL_NEW_DATA], prefs.hex_gui_tab_small); - nickseen_list = mg_attr_list_create (&gui_palette[COL_HILIGHT], prefs.hex_gui_tab_small); - newmsg_list = mg_attr_list_create (&gui_palette[COL_NEW_MSG], prefs.hex_gui_tab_small); - away_list = mg_attr_list_create (&gui_palette[COL_AWAY], FALSE); + newdata_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_NEW_DATA], prefs.hex_gui_tab_small); + nickseen_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_HIGHLIGHT], prefs.hex_gui_tab_small); + newmsg_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_NEW_MESSAGE], prefs.hex_gui_tab_small); + away_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_AWAY], FALSE); } static void @@ -1751,7 +1753,13 @@ mg_create_color_menu (GtkWidget *menu, session *sess) guint16 green; guint16 blue; - palette_color_get_rgb16 (&colors[i], &red, &green, &blue); + GdkRGBA color; + + if (!theme_get_mirc_color ((unsigned int) i, &color)) + continue; + red = (guint16) CLAMP (color.red * 65535.0 + 0.5, 0.0, 65535.0); + green = (guint16) CLAMP (color.green * 65535.0 + 0.5, 0.0, 65535.0); + blue = (guint16) CLAMP (color.blue * 65535.0 + 0.5, 0.0, 65535.0); sprintf (buf, "%02d " " ", i, red >> 8, green >> 8, blue >> 8); @@ -1766,7 +1774,13 @@ mg_create_color_menu (GtkWidget *menu, session *sess) guint16 green; guint16 blue; - palette_color_get_rgb16 (&colors[i], &red, &green, &blue); + GdkRGBA color; + + if (!theme_get_mirc_color ((unsigned int) i, &color)) + continue; + red = (guint16) CLAMP (color.red * 65535.0 + 0.5, 0.0, 65535.0); + green = (guint16) CLAMP (color.green * 65535.0 + 0.5, 0.0, 65535.0); + blue = (guint16) CLAMP (color.blue * 65535.0 + 0.5, 0.0, 65535.0); sprintf (buf, "%02d " " ", i, red >> 8, green >> 8, blue >> 8); @@ -2359,8 +2373,7 @@ mg_limit_entry_cb (GtkWidget * igad, gpointer userdata) static void mg_apply_entry_style (GtkWidget *entry) { - gtkutil_apply_palette (entry, &colors[COL_BG], &colors[COL_FG], - input_style->font_desc); + theme_manager_apply_entry_palette (entry, input_style->font_desc); } static void @@ -2604,7 +2617,7 @@ mg_update_xtext (GtkWidget *wid) const gchar *font_name; XTextColor xtext_palette[XTEXT_COLS]; - palette_get_xtext_colors (xtext_palette, XTEXT_COLS); + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); gtk_xtext_set_palette (xtext, xtext_palette); gtk_xtext_set_max_lines (xtext, prefs.hex_text_max_lines); gtk_xtext_set_background (xtext, channelwin_pix); @@ -2651,7 +2664,7 @@ mg_create_textarea (session *sess, GtkWidget *box) gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN); gtk_box_pack_start (GTK_BOX (inbox), frame, TRUE, TRUE, 0); - palette_get_xtext_colors (xtext_palette, XTEXT_COLS); + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); gui->xtext = gtk_xtext_new (xtext_palette, TRUE); xtext = GTK_XTEXT (gui->xtext); gtk_xtext_set_max_indent (xtext, prefs.hex_text_max_indent); @@ -2770,6 +2783,81 @@ mg_update_meters (session_gui *gui) gtk_widget_show_all (gui->meter_box); } +static void +mg_theme_apply_userlist_style (session_gui *gui) +{ + const PangoFontDescription *font = NULL; + + if (!gui || !gui->user_tree) + return; + + if (input_style) + font = input_style->font_desc; + + theme_manager_apply_userlist_style (gui->user_tree, + theme_manager_get_userlist_palette_behavior (font)); +} + +static void +mg_theme_userlist_changed (const ThemeChangedEvent *event, gpointer userdata) +{ + session_gui *gui = userdata; + + if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_USERLIST) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_WIDGET_STYLE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK)) + return; + + mg_theme_apply_userlist_style (gui); +} + +static void +mg_theme_window_changed (const ThemeChangedEvent *event, gpointer userdata) +{ + session_gui *gui = userdata; + + if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_WIDGET_STYLE)) + return; + + if (gui) + theme_manager_apply_to_window (gui->window); +} + +static void +mg_theme_userlist_destroy_cb (GtkWidget *widget, gpointer userdata) +{ + session_gui *gui = userdata; + + (void) widget; + if (!gui) + return; + if (gui->theme_userlist_listener_id) + { + theme_listener_unregister (gui->theme_userlist_listener_id); + gui->theme_userlist_listener_id = 0; + } +} + +static void +mg_theme_window_destroy_cb (GtkWidget *widget, gpointer userdata) +{ + session_gui *gui = userdata; + + (void) widget; + if (!gui) + return; + theme_manager_detach_window (gui->window); + if (gui->theme_window_listener_id) + { + theme_listener_unregister (gui->theme_window_listener_id); + gui->theme_window_listener_id = 0; + } +} + static void mg_create_userlist (session_gui *gui, GtkWidget *box) { @@ -2787,19 +2875,10 @@ mg_create_userlist (session_gui *gui, GtkWidget *box) gui->user_tree = ulist = userlist_create (vbox); - /* - * Keep the user list in sync with the chat palette. - * - * - When "Use the text box font and colors" is enabled, we already want the - * palette background. - * - When "Dark mode" is enabled, we also force the user list to use the - * palette colors so it doesn't stay blindingly white on dark themes. - */ - if (prefs.hex_gui_ulist_style || fe_dark_mode_is_enabled ()) - { - gtkutil_apply_palette (ulist, &colors[COL_BG], &colors[COL_FG], - input_style ? input_style->font_desc : NULL); - } + if (!gui->theme_userlist_listener_id) + gui->theme_userlist_listener_id = theme_listener_register ("maingui.userlist", mg_theme_userlist_changed, gui); + g_signal_connect (G_OBJECT (ulist), "destroy", G_CALLBACK (mg_theme_userlist_destroy_cb), gui); + mg_theme_apply_userlist_style (gui); mg_create_meters (gui, vbox); @@ -3626,7 +3705,6 @@ mg_create_topwindow (session *sess) g_signal_connect (G_OBJECT (win), "configure_event", G_CALLBACK (mg_configure_cb), sess); - palette_alloc (win); table = gtk_grid_new (); /* spacing under the menubar */ @@ -3681,7 +3759,10 @@ mg_create_topwindow (session *sess) mg_place_userlist_and_chanview (sess->gui); gtk_widget_show (win); - fe_apply_theme_to_toplevel (win); + if (!sess->gui->theme_window_listener_id) + sess->gui->theme_window_listener_id = theme_listener_register ("maingui.window", mg_theme_window_changed, sess->gui); + g_signal_connect (G_OBJECT (win), "destroy", G_CALLBACK (mg_theme_window_destroy_cb), sess->gui); + theme_manager_attach_window (win); #ifdef G_OS_WIN32 parent_win = gtk_widget_get_window (win); @@ -3730,7 +3811,7 @@ mg_win32_filter (GdkXEvent *xevent, GdkEvent *event, gpointer data) if (msg->message == WM_SETTINGCHANGE || msg->message == WM_THEMECHANGED) { - fe_refresh_auto_dark_mode (); + theme_manager_refresh_auto_mode (); return GDK_FILTER_CONTINUE; } @@ -3818,7 +3899,6 @@ mg_create_tabwindow (session *sess) g_signal_connect (G_OBJECT (win), "window_state_event", G_CALLBACK (mg_windowstate_cb), NULL); - palette_alloc (win); sess->gui->main_table = table = gtk_grid_new (); /* spacing under the menubar */ @@ -3857,7 +3937,10 @@ mg_create_tabwindow (session *sess) mg_place_userlist_and_chanview (sess->gui); gtk_widget_show (win); - fe_apply_theme_to_toplevel (win); + if (!sess->gui->theme_window_listener_id) + sess->gui->theme_window_listener_id = theme_listener_register ("maingui.window", mg_theme_window_changed, sess->gui); + g_signal_connect (G_OBJECT (win), "destroy", G_CALLBACK (mg_theme_window_destroy_cb), sess->gui); + theme_manager_attach_window (win); #ifdef G_OS_WIN32 parent_win = gtk_widget_get_window (win); @@ -3881,8 +3964,6 @@ mg_apply_setup (void) ((xtext_buffer *)sess->res->buffer)->needs_recalc = TRUE; if (!sess->gui->is_tab || !done_main) mg_place_userlist_and_chanview (sess->gui); - if (sess->gui->window) - fe_apply_theme_to_toplevel (sess->gui->window); if (sess->gui->is_tab) done_main = TRUE; list = list->next; @@ -4218,6 +4299,8 @@ fe_session_callback (session *sess) { gtk_xtext_buffer_free (sess->res->buffer); g_object_unref (G_OBJECT (sess->res->user_model)); + if (sess->res->user_row_refs) + g_hash_table_destroy (sess->res->user_row_refs); if (sess->res->banlist && sess->res->banlist->window) mg_close_gen (NULL, sess->res->banlist->window); diff --git a/src/fe-gtk/menu.c b/src/fe-gtk/menu.c index b94d1762..f32cce27 100644 --- a/src/fe-gtk/menu.c +++ b/src/fe-gtk/menu.c @@ -54,7 +54,7 @@ #include "notifygui.h" #include "pixmaps.h" #include "rawlog.h" -#include "palette.h" +#include "theme/theme-gtk.h" #include "plugingui.h" #include "search.h" #include "textgui.h" diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index 7019cce0..af32f106 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -1,3 +1,14 @@ +zoitechat_theme_sources = [ + 'theme/theme-access.c', + 'theme/theme-application.c', + 'theme/theme-css.c', + 'theme/theme-manager.c', + 'theme/theme-palette.c', + 'theme/theme-preferences.c', + 'theme/theme-policy.c', + 'theme/theme-runtime.c', +] + zoitechat_gtk_sources = [ 'ascii.c', 'banlist.c', @@ -15,7 +26,6 @@ zoitechat_gtk_sources = [ 'menu.c', 'maingui.c', 'notifygui.c', - 'palette.c', 'pixmaps.c', 'plugin-tray.c', 'plugin-notification.c', @@ -38,6 +48,8 @@ zoitechat_gtk_deps = [ gtk_dep = dependency('gtk+-3.0', version: '>= 3.22') +zoitechat_theme_deps = [gtk_dep] + if host_machine.system() != 'windows' appindicator_opt = get_option('appindicator') @@ -77,11 +89,19 @@ zoitechat_gtk_ldflags = [] if host_machine.system() == 'windows' zoitechat_gtk_sources += 'notifications/notification-windows.c' zoitechat_gtk_deps += cc.find_library('dwmapi', required: true) + zoitechat_theme_deps += cc.find_library('dwmapi', required: true) else zoitechat_gtk_sources += 'notifications/notification-freedesktop.c' endif +zoitechat_theme_lib = static_library('zoitechat_theme', + sources: zoitechat_theme_sources, + include_directories: [config_h_include], + dependencies: zoitechat_theme_deps, + install: false, +) + iso_codes = dependency('iso-codes', required: false) if iso_codes.found() zoitechat_gtk_sources += 'sexy-iso-codes.c' @@ -109,9 +129,94 @@ endif executable('zoitechat', sources: resources + zoitechat_gtk_sources, dependencies: zoitechat_gtk_deps, + link_with: zoitechat_theme_lib, c_args: zoitechat_gtk_cflags, link_args: zoitechat_gtk_ldflags, pie: true, install: true, gui_app: true, ) + +theme_manager_policy_tests = executable('theme_manager_policy_tests', + [ + 'theme/tests/test-theme-manager-policy.c', + 'theme/theme-manager.c', + 'theme/theme-palette.c', + ], + include_directories: [config_h_include], + dependencies: [gtk_dep], +) + +test('Theme Manager/Policy Tests', theme_manager_policy_tests, + protocol: 'tap', + timeout: 120, +) + + +theme_manager_dispatch_tests = executable('theme_manager_dispatch_routing_tests', + [ + 'theme/tests/test-theme-manager-dispatch-routing.c', + 'theme/theme-manager.c', + 'theme/theme-palette.c', + ], + include_directories: [config_h_include], + dependencies: [gtk_dep], +) + +test('Theme Manager Dispatch Routing Tests', theme_manager_dispatch_tests, + protocol: 'tap', + timeout: 120, +) + +theme_manager_auto_refresh_tests = executable('theme_manager_auto_refresh_tests', + [ + 'theme/tests/test-theme-manager-auto-refresh.c', + 'theme/theme-manager.c', + 'theme/theme-palette.c', + ], + include_directories: [config_h_include], + dependencies: [gtk_dep], +) + +test('Theme Manager Auto Refresh Tests', theme_manager_auto_refresh_tests, + protocol: 'tap', + timeout: 120, +) + +theme_application_input_style_tests = executable('theme_application_input_style_tests', + [ + 'theme/tests/test-theme-application-input-style.c', + 'theme/theme-application.c', + ], + include_directories: [config_h_include], + dependencies: [gtk_dep], +) + +test('Theme Application Input Style Tests', theme_application_input_style_tests, + protocol: 'tap', + timeout: 120, +) + +theme_runtime_tests = executable('theme_runtime_persistence_tests', + 'theme/tests/test-theme-runtime-persistence.c', + include_directories: [config_h_include], + dependencies: [gtk_dep], + link_with: zoitechat_theme_lib, +) + +test('Theme Runtime Persistence Tests', theme_runtime_tests, + protocol: 'tap', + timeout: 120, +) + +theme_access_tests = executable('theme_access_routing_tests', + 'theme/tests/test-theme-access-routing.c', + include_directories: [config_h_include], + dependencies: [gtk_dep], + link_with: zoitechat_theme_lib, +) + +test('Theme Access Routing Tests', theme_access_tests, + protocol: 'tap', + timeout: 120, +) diff --git a/src/fe-gtk/notifygui.c b/src/fe-gtk/notifygui.c index 9099e8e9..b28dc00f 100644 --- a/src/fe-gtk/notifygui.c +++ b/src/fe-gtk/notifygui.c @@ -34,8 +34,9 @@ #include "../common/outbound.h" #include "gtkutil.h" #include "maingui.h" -#include "palette.h" +#include "theme/theme-gtk.h" #include "notifygui.h" +#include "theme/theme-access.h" #define ICON_NOTIFY_NEW "document-new" #define ICON_NOTIFY_DELETE "edit-delete" @@ -69,7 +70,7 @@ notify_closegui (void) } /* Need this to be able to set the foreground colour property of a row - * from a PaletteColor * in the model -Vince + * from a GdkRGBA * in the model -Vince */ static void notify_treecell_property_mapper (GtkTreeViewColumn *col, GtkCellRenderer *cell, @@ -77,21 +78,21 @@ notify_treecell_property_mapper (GtkTreeViewColumn *col, GtkCellRenderer *cell, gpointer data) { gchar *text; - PaletteColor *colour; + GdkRGBA *colour; int model_column = GPOINTER_TO_INT (data); gtk_tree_model_get (GTK_TREE_MODEL (model), iter, COLOUR_COLUMN, &colour, model_column, &text, -1); g_object_set (G_OBJECT (cell), "text", text, - PALETTE_FOREGROUND_PROPERTY, colour, NULL); + THEME_GTK_FOREGROUND_PROPERTY, colour, NULL); if (colour) gdk_rgba_free (colour); g_free (text); } static void -notify_store_color (GtkListStore *store, GtkTreeIter *iter, const PaletteColor *color) +notify_store_color (GtkListStore *store, GtkTreeIter *iter, const GdkRGBA *color) { if (color) { @@ -135,7 +136,7 @@ notify_treeview_new (GtkWidget *box) G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, - PALETTE_GDK_TYPE, + THEME_GTK_COLOR_TYPE, G_TYPE_POINTER ); g_return_val_if_fail (store != NULL, NULL); @@ -170,6 +171,7 @@ notify_gui_update (void) GSList *slist; gchar *name, *status, *server, *seen; int online, servcount, lastseenminutes; + GdkRGBA color; time_t lastseen; char agobuf[128]; @@ -225,7 +227,10 @@ notify_gui_update (void) gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, name, 1, status, 2, server, 3, seen, 5, NULL, -1); - notify_store_color (store, &iter, &colors[4]); + if (theme_get_color (THEME_TOKEN_MIRC_4, &color)) + notify_store_color (store, &iter, &color); + else + notify_store_color (store, &iter, NULL); if (valid) valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter); @@ -251,7 +256,10 @@ notify_gui_update (void) gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, name, 1, status, 2, server, 3, seen, 5, servnot, -1); - notify_store_color (store, &iter, &colors[3]); + if (theme_get_color (THEME_TOKEN_MIRC_3, &color)) + notify_store_color (store, &iter, &color); + else + notify_store_color (store, &iter, NULL); if (valid) valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter); diff --git a/src/fe-gtk/palette.c b/src/fe-gtk/palette.c deleted file mode 100644 index a9303c8e..00000000 --- a/src/fe-gtk/palette.c +++ /dev/null @@ -1,453 +0,0 @@ -/* X-Chat - * Copyright (C) 1998 Peter Zelezny. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA - */ - -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#else -#include -#endif - -#include "fe-gtk.h" -#include "palette.h" - -#include "../common/zoitechat.h" -#include "../common/zoitechatc.h" /* prefs */ -#include "../common/util.h" -#include "../common/cfgfiles.h" -#include "../common/typedef.h" - -#define PALETTE_COLOR_INIT(r, g, b) { (r) / 65535.0, (g) / 65535.0, (b) / 65535.0, 1.0 } - -static void -palette_color_set_rgb16 (PaletteColor *color, guint16 red, guint16 green, guint16 blue) -{ - char color_string[16]; - GdkRGBA parsed = { 0 }; - gboolean parsed_ok; - - g_snprintf (color_string, sizeof (color_string), "#%04x%04x%04x", - red, green, blue); - parsed_ok = gdk_rgba_parse (&parsed, color_string); - if (!parsed_ok) - { - parsed.red = red / 65535.0; - parsed.green = green / 65535.0; - parsed.blue = blue / 65535.0; - parsed.alpha = 1.0; - } - *color = parsed; -} - -static XTextColor -palette_color_from_gdk (const PaletteColor *color) -{ - XTextColor result; - - result.red = color->red; - result.green = color->green; - result.blue = color->blue; - result.alpha = color->alpha; - - return result; -} - -PaletteColor colors[] = { - /* colors for xtext */ - PALETTE_COLOR_INIT (0xd3d3, 0xd7d7, 0xcfcf), /* 0 white */ - PALETTE_COLOR_INIT (0x2e2e, 0x3434, 0x3636), /* 1 black */ - PALETTE_COLOR_INIT (0x3434, 0x6565, 0xa4a4), /* 2 blue */ - PALETTE_COLOR_INIT (0x4e4e, 0x9a9a, 0x0606), /* 3 green */ - PALETTE_COLOR_INIT (0xcccc, 0x0000, 0x0000), /* 4 red */ - PALETTE_COLOR_INIT (0x8f8f, 0x3939, 0x0202), /* 5 light red */ - PALETTE_COLOR_INIT (0x5c5c, 0x3535, 0x6666), /* 6 purple */ - PALETTE_COLOR_INIT (0xcece, 0x5c5c, 0x0000), /* 7 orange */ - PALETTE_COLOR_INIT (0xc4c4, 0xa0a0, 0x0000), /* 8 yellow */ - PALETTE_COLOR_INIT (0x7373, 0xd2d2, 0x1616), /* 9 green */ - PALETTE_COLOR_INIT (0x1111, 0xa8a8, 0x7979), /* 10 aqua */ - PALETTE_COLOR_INIT (0x5858, 0xa1a1, 0x9d9d), /* 11 light aqua */ - PALETTE_COLOR_INIT (0x5757, 0x7979, 0x9e9e), /* 12 blue */ - PALETTE_COLOR_INIT (0xa0d0, 0x42d4, 0x6562), /* 13 light purple */ - PALETTE_COLOR_INIT (0x5555, 0x5757, 0x5353), /* 14 grey */ - PALETTE_COLOR_INIT (0x8888, 0x8a8a, 0x8585), /* 15 light grey */ - - PALETTE_COLOR_INIT (0xd3d3, 0xd7d7, 0xcfcf), /* 16 white */ - PALETTE_COLOR_INIT (0x2e2e, 0x3434, 0x3636), /* 17 black */ - PALETTE_COLOR_INIT (0x3434, 0x6565, 0xa4a4), /* 18 blue */ - PALETTE_COLOR_INIT (0x4e4e, 0x9a9a, 0x0606), /* 19 green */ - PALETTE_COLOR_INIT (0xcccc, 0x0000, 0x0000), /* 20 red */ - PALETTE_COLOR_INIT (0x8f8f, 0x3939, 0x0202), /* 21 light red */ - PALETTE_COLOR_INIT (0x5c5c, 0x3535, 0x6666), /* 22 purple */ - PALETTE_COLOR_INIT (0xcece, 0x5c5c, 0x0000), /* 23 orange */ - PALETTE_COLOR_INIT (0xc4c4, 0xa0a0, 0x0000), /* 24 yellow */ - PALETTE_COLOR_INIT (0x7373, 0xd2d2, 0x1616), /* 25 green */ - PALETTE_COLOR_INIT (0x1111, 0xa8a8, 0x7979), /* 26 aqua */ - PALETTE_COLOR_INIT (0x5858, 0xa1a1, 0x9d9d), /* 27 light aqua */ - PALETTE_COLOR_INIT (0x5757, 0x7979, 0x9e9e), /* 28 blue */ - PALETTE_COLOR_INIT (0xa0d0, 0x42d4, 0x6562), /* 29 light purple */ - PALETTE_COLOR_INIT (0x5555, 0x5757, 0x5353), /* 30 grey */ - PALETTE_COLOR_INIT (0x8888, 0x8a8a, 0x8585), /* 31 light grey */ - - PALETTE_COLOR_INIT (0xd3d3, 0xd7d7, 0xcfcf), /* 32 marktext Fore (white) */ - PALETTE_COLOR_INIT (0x2020, 0x4a4a, 0x8787), /* 33 marktext Back (blue) */ - PALETTE_COLOR_INIT (0x2512, 0x29e8, 0x2b85), /* 34 foreground (black) */ - PALETTE_COLOR_INIT (0xfae0, 0xfae0, 0xf8c4), /* 35 background (white) */ - PALETTE_COLOR_INIT (0x8f8f, 0x3939, 0x0202), /* 36 marker line (red) */ - - /* colors for GUI */ - PALETTE_COLOR_INIT (0x3434, 0x6565, 0xa4a4), /* 37 tab New Data (dark red) */ - PALETTE_COLOR_INIT (0x4e4e, 0x9a9a, 0x0606), /* 38 tab Nick Mentioned (blue) */ - PALETTE_COLOR_INIT (0xcece, 0x5c5c, 0x0000), /* 39 tab New Message (red) */ - PALETTE_COLOR_INIT (0x8888, 0x8a8a, 0x8585), /* 40 away user (grey) */ - PALETTE_COLOR_INIT (0xa4a4, 0x0000, 0x0000), /* 41 spell checker color (red) */ -}; - -/* User palette snapshot (what we write to colors.conf) */ -static PaletteColor user_colors[MAX_COL + 1]; -static gboolean user_colors_valid = FALSE; - -/* Dark palette snapshot (saved separately so dark mode can have its own custom palette). */ -static PaletteColor dark_user_colors[MAX_COL + 1]; -static gboolean dark_user_colors_valid = FALSE; - -/* ZoiteChat's curated dark palette (applies when prefs.hex_gui_dark_mode is enabled). */ -static const PaletteColor dark_colors[MAX_COL + 1] = { - /* mIRC colors 0-15 */ - PALETTE_COLOR_INIT (0xe5e5, 0xe5e5, 0xe5e5), /* 0 white */ - PALETTE_COLOR_INIT (0x3c3c, 0x3c3c, 0x3c3c), /* 1 black (dark gray for contrast) */ - PALETTE_COLOR_INIT (0x5656, 0x9c9c, 0xd6d6), /* 2 blue */ - PALETTE_COLOR_INIT (0x0d0d, 0xbcbc, 0x7979), /* 3 green */ - PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), /* 4 red */ - PALETTE_COLOR_INIT (0xcece, 0x9191, 0x7878), /* 5 light red / brown */ - PALETTE_COLOR_INIT (0xc5c5, 0x8686, 0xc0c0), /* 6 purple */ - PALETTE_COLOR_INIT (0xd7d7, 0xbaba, 0x7d7d), /* 7 orange */ - PALETTE_COLOR_INIT (0xdcdc, 0xdcdc, 0xaaaa), /* 8 yellow */ - PALETTE_COLOR_INIT (0xb5b5, 0xcece, 0xa8a8), /* 9 light green */ - PALETTE_COLOR_INIT (0x4e4e, 0xc9c9, 0xb0b0), /* 10 aqua */ - PALETTE_COLOR_INIT (0x9c9c, 0xdcdc, 0xfefe), /* 11 light aqua */ - PALETTE_COLOR_INIT (0x3737, 0x9494, 0xffff), /* 12 light blue */ - PALETTE_COLOR_INIT (0xd6d6, 0x7070, 0xd6d6), /* 13 pink */ - PALETTE_COLOR_INIT (0x8080, 0x8080, 0x8080), /* 14 gray */ - PALETTE_COLOR_INIT (0xc0c0, 0xc0c0, 0xc0c0), /* 15 light gray */ - /* mIRC colors 16-31 (repeat) */ - PALETTE_COLOR_INIT (0xe5e5, 0xe5e5, 0xe5e5), PALETTE_COLOR_INIT (0x3c3c, 0x3c3c, 0x3c3c), - PALETTE_COLOR_INIT (0x5656, 0x9c9c, 0xd6d6), PALETTE_COLOR_INIT (0x0d0d, 0xbcbc, 0x7979), - PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), PALETTE_COLOR_INIT (0xcece, 0x9191, 0x7878), - PALETTE_COLOR_INIT (0xc5c5, 0x8686, 0xc0c0), PALETTE_COLOR_INIT (0xd7d7, 0xbaba, 0x7d7d), - PALETTE_COLOR_INIT (0xdcdc, 0xdcdc, 0xaaaa), PALETTE_COLOR_INIT (0xb5b5, 0xcece, 0xa8a8), - PALETTE_COLOR_INIT (0x4e4e, 0xc9c9, 0xb0b0), PALETTE_COLOR_INIT (0x9c9c, 0xdcdc, 0xfefe), - PALETTE_COLOR_INIT (0x3737, 0x9494, 0xffff), PALETTE_COLOR_INIT (0xd6d6, 0x7070, 0xd6d6), - PALETTE_COLOR_INIT (0x8080, 0x8080, 0x8080), PALETTE_COLOR_INIT (0xc0c0, 0xc0c0, 0xc0c0), - - /* selection colors */ - PALETTE_COLOR_INIT (0xffff, 0xffff, 0xffff), /* 32 COL_MARK_FG */ - PALETTE_COLOR_INIT (0x2626, 0x4f4f, 0x7878), /* 33 COL_MARK_BG */ - - /* foreground/background */ - PALETTE_COLOR_INIT (0xd4d4, 0xd4d4, 0xd4d4), /* 34 COL_FG */ - PALETTE_COLOR_INIT (0x1e1e, 0x1e1e, 0x1e1e), /* 35 COL_BG */ - - /* interface colors */ - PALETTE_COLOR_INIT (0x4040, 0x4040, 0x4040), /* 36 COL_MARKER (marker line) */ - PALETTE_COLOR_INIT (0x3737, 0x9494, 0xffff), /* 37 COL_NEW_DATA (tab: new data) */ - PALETTE_COLOR_INIT (0xd7d7, 0xbaba, 0x7d7d), /* 38 COL_HILIGHT (tab: nick mentioned) */ - PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), /* 39 COL_NEW_MSG (tab: new message) */ - PALETTE_COLOR_INIT (0x8080, 0x8080, 0x8080), /* 40 COL_AWAY (tab: away) */ - PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), /* 41 COL_SPELL (spellcheck underline) */ -}; - -void -palette_get_xtext_colors (XTextColor *palette, size_t palette_len) -{ - size_t i; - size_t count = palette_len < G_N_ELEMENTS (colors) ? palette_len : G_N_ELEMENTS (colors); - - for (i = 0; i < count; i++) - { - palette[i] = palette_color_from_gdk (&colors[i]); - } -} - -void -palette_user_set_color (int idx, const PaletteColor *col) -{ - if (!col) - return; - if (idx < 0 || idx > MAX_COL) - return; - - if (!user_colors_valid) - { - memcpy (user_colors, colors, sizeof (user_colors)); - user_colors_valid = TRUE; - } - - user_colors[idx] = *col; -} - -void -palette_dark_set_color (int idx, const PaletteColor *col) -{ - if (!col) - return; - if (idx < 0 || idx > MAX_COL) - return; - - if (!dark_user_colors_valid) - { - /* Start from the currently active palette (should be dark when editing dark mode). */ - memcpy (dark_user_colors, colors, sizeof (dark_user_colors)); - dark_user_colors_valid = TRUE; - } - - dark_user_colors[idx] = *col; -} - -void -palette_alloc (GtkWidget * widget) -{ - (void) widget; -} - -void -palette_load (void) -{ - int i, j, fh; - char prefname[256]; - struct stat st; - char *cfg; - guint16 red, green, blue; - gboolean dark_found = FALSE; - - fh = zoitechat_open_file ("colors.conf", O_RDONLY, 0, 0); - if (fh != -1) - { - fstat (fh, &st); - cfg = g_malloc0 (st.st_size + 1); - read (fh, cfg, st.st_size); - - /* Light palette (default behavior): mIRC colors 0-31. */ - for (i = 0; i < 32; i++) - { - g_snprintf (prefname, sizeof prefname, "color_%d", i); - if (cfg_get_color (cfg, prefname, &red, &green, &blue)) - { - palette_color_set_rgb16 (&colors[i], red, green, blue); - } - } - - /* Light palette: our special colors are mapped at 256+. */ - for (i = 256, j = 32; j < MAX_COL + 1; i++, j++) - { - g_snprintf (prefname, sizeof prefname, "color_%d", i); - if (cfg_get_color (cfg, prefname, &red, &green, &blue)) - { - palette_color_set_rgb16 (&colors[j], red, green, blue); - } - } - - /* Dark palette: start from curated defaults and optionally override from colors.conf. */ - memcpy (dark_user_colors, dark_colors, sizeof (dark_user_colors)); - - for (i = 0; i < 32; i++) - { - g_snprintf (prefname, sizeof prefname, "dark_color_%d", i); - if (cfg_get_color (cfg, prefname, &red, &green, &blue)) - { - palette_color_set_rgb16 (&dark_user_colors[i], red, green, blue); - dark_found = TRUE; - } - } - - for (i = 256, j = 32; j < MAX_COL + 1; i++, j++) - { - g_snprintf (prefname, sizeof prefname, "dark_color_%d", i); - if (cfg_get_color (cfg, prefname, &red, &green, &blue)) - { - palette_color_set_rgb16 (&dark_user_colors[j], red, green, blue); - dark_found = TRUE; - } - } - - dark_user_colors_valid = dark_found; - - g_free (cfg); - close (fh); - } - - /* Snapshot the user's (light) palette for dark mode toggling. */ - memcpy (user_colors, colors, sizeof (user_colors)); - user_colors_valid = TRUE; -} - - -void -palette_save (void) -{ - int i, j, fh; - char prefname[256]; - const PaletteColor *lightpal = colors; - const PaletteColor *darkpal = NULL; - gboolean dark_mode_active = fe_dark_mode_is_enabled (); - - /* If we're currently in dark mode, keep colors.conf's legacy keys as the user's light palette. */ - if (dark_mode_active && user_colors_valid) - lightpal = user_colors; - - /* If we're currently in light mode, ensure the snapshot stays in sync. */ - if (!dark_mode_active) - { - memcpy (user_colors, colors, sizeof (user_colors)); - user_colors_valid = TRUE; - } - - /* If dark mode is enabled but we haven't snapshotted a custom dark palette yet, capture it now. */ - if (dark_mode_active && !dark_user_colors_valid) - { - memcpy (dark_user_colors, colors, sizeof (dark_user_colors)); - dark_user_colors_valid = TRUE; - } - - if (dark_user_colors_valid) - darkpal = dark_user_colors; - else if (dark_mode_active) - darkpal = colors; /* current dark palette (likely defaults) */ - - fh = zoitechat_open_file ("colors.conf", O_TRUNC | O_WRONLY | O_CREAT, 0600, XOF_DOMODE); - if (fh != -1) - { - /* Light palette (legacy keys) */ - for (i = 0; i < 32; i++) - { - g_snprintf (prefname, sizeof prefname, "color_%d", i); - guint16 red; - guint16 green; - guint16 blue; - - palette_color_get_rgb16 (&lightpal[i], &red, &green, &blue); - cfg_put_color (fh, red, green, blue, prefname); - } - - for (i = 256, j = 32; j < MAX_COL + 1; i++, j++) - { - g_snprintf (prefname, sizeof prefname, "color_%d", i); - { - guint16 red; - guint16 green; - guint16 blue; - - palette_color_get_rgb16 (&lightpal[j], &red, &green, &blue); - cfg_put_color (fh, red, green, blue, prefname); - } - } - - /* Dark palette (new keys) */ - if (darkpal) - { - for (i = 0; i < 32; i++) - { - g_snprintf (prefname, sizeof prefname, "dark_color_%d", i); - guint16 red; - guint16 green; - guint16 blue; - - palette_color_get_rgb16 (&darkpal[i], &red, &green, &blue); - cfg_put_color (fh, red, green, blue, prefname); - } - - for (i = 256, j = 32; j < MAX_COL + 1; i++, j++) - { - g_snprintf (prefname, sizeof prefname, "dark_color_%d", i); - { - guint16 red; - guint16 green; - guint16 blue; - - palette_color_get_rgb16 (&darkpal[j], &red, &green, &blue); - cfg_put_color (fh, red, green, blue, prefname); - } - } - } - - close (fh); - } -} - - -static gboolean -palette_color_eq (const PaletteColor *a, const PaletteColor *b) -{ - guint16 red_a; - guint16 green_a; - guint16 blue_a; - guint16 red_b; - guint16 green_b; - guint16 blue_b; - - palette_color_get_rgb16 (a, &red_a, &green_a, &blue_a); - palette_color_get_rgb16 (b, &red_b, &green_b, &blue_b); - - return red_a == red_b && green_a == green_b && blue_a == blue_b; -} - -gboolean -palette_apply_dark_mode (gboolean enable) -{ - PaletteColor old_colors[MAX_COL + 1]; - int i; - gboolean changed = FALSE; - - memcpy (old_colors, colors, sizeof (old_colors)); - - /* Ensure we have a snapshot of the user's palette before overriding anything. */ - if (!user_colors_valid) - { - memcpy (user_colors, colors, sizeof (user_colors)); - user_colors_valid = TRUE; - } - - if (enable) - { - if (dark_user_colors_valid) - memcpy (colors, dark_user_colors, sizeof (colors)); - else - memcpy (colors, dark_colors, sizeof (colors)); - } - else - memcpy (colors, user_colors, sizeof (colors)); - - /* Track whether any palette entries changed. */ - (void) i; - - for (i = 0; i <= MAX_COL; i++) - { - if (!palette_color_eq (&old_colors[i], &colors[i])) - { - changed = TRUE; - break; - } - } - - return changed; -} diff --git a/src/fe-gtk/palette.h b/src/fe-gtk/palette.h index 9d9c9794..0f96817b 100644 --- a/src/fe-gtk/palette.h +++ b/src/fe-gtk/palette.h @@ -20,61 +20,12 @@ #ifndef ZOITECHAT_PALETTE_H #define ZOITECHAT_PALETTE_H -#include - -#include "xtext-color.h" +#include "theme/theme-gtk.h" +#include "theme/theme-palette.h" typedef GdkRGBA PaletteColor; -#define PALETTE_GDK_TYPE GDK_TYPE_RGBA -#define PALETTE_FOREGROUND_PROPERTY "foreground-rgba" +#define PALETTE_GDK_TYPE THEME_GTK_COLOR_TYPE +#define PALETTE_FOREGROUND_PROPERTY THEME_GTK_FOREGROUND_PROPERTY -extern PaletteColor colors[]; - -static inline void -palette_color_get_rgb16 (const PaletteColor *color, guint16 *red, guint16 *green, guint16 *blue) -{ - *red = (guint16) CLAMP (color->red * 65535.0 + 0.5, 0.0, 65535.0); - *green = (guint16) CLAMP (color->green * 65535.0 + 0.5, 0.0, 65535.0); - *blue = (guint16) CLAMP (color->blue * 65535.0 + 0.5, 0.0, 65535.0); -} - -#define COL_MARK_FG 32 -#define COL_MARK_BG 33 -#define COL_FG 34 -#define COL_BG 35 -#define COL_MARKER 36 -#define COL_NEW_DATA 37 -#define COL_HILIGHT 38 -#define COL_NEW_MSG 39 -#define COL_AWAY 40 -#define COL_SPELL 41 -#define MAX_COL 41 - -void palette_alloc (GtkWidget * widget); -void palette_load (void); -void palette_save (void); - -/* Keep a copy of the user's palette so dark mode can be toggled without losing it. */ -void palette_user_set_color (int idx, const PaletteColor *col); -void palette_dark_set_color (int idx, const PaletteColor *col); - -/* - * Apply ZoiteChat's built-in "dark mode" palette. - * - * When enabled, ZoiteChat switches to a curated dark-friendly palette for: - * - message colors (mIRC palette) - * - selection colors - * - tab highlight colors - * - chat/user/channel list background + foreground - * - * The user's palette is preserved in-memory and written to colors.conf even - * while dark mode is enabled, so disabling dark mode restores the previous - * colors without surprises. - * - * Returns TRUE if any palette entries were changed. - */ -gboolean palette_apply_dark_mode (gboolean enable); - -void palette_get_xtext_colors (XTextColor *palette, size_t palette_len); #endif diff --git a/src/fe-gtk/rawlog.c b/src/fe-gtk/rawlog.c index 967ac2c6..3a7dd57c 100644 --- a/src/fe-gtk/rawlog.c +++ b/src/fe-gtk/rawlog.c @@ -36,7 +36,8 @@ #include "../common/cfgfiles.h" #include "../common/server.h" #include "gtkutil.h" -#include "palette.h" +#include "theme/theme-access.h" +#include "theme/theme-manager.h" #include "maingui.h" #include "rawlog.h" #include "xtext.h" @@ -45,6 +46,53 @@ #define ICON_RAWLOG_CLEAR "zc-menu-clear" #define ICON_RAWLOG_SAVE_AS "zc-menu-save-as" +#define RAWLOG_THEME_LISTENER_ID_KEY "rawlog.theme-listener-id" + +static void +rawlog_theme_apply (GtkWidget *window) +{ + GtkWidget *xtext_widget; + XTextColor xtext_palette[XTEXT_COLS]; + + if (!window) + return; + + xtext_widget = g_object_get_data (G_OBJECT (window), "rawlog-xtext"); + if (!xtext_widget) + return; + + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); + gtk_xtext_set_palette (GTK_XTEXT (xtext_widget), xtext_palette); +} + +static void +rawlog_theme_changed (const ThemeChangedEvent *event, gpointer userdata) +{ + GtkWidget *window = userdata; + + if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE)) + return; + + rawlog_theme_apply (window); +} + +static void +rawlog_theme_destroy_cb (GtkWidget *widget, gpointer userdata) +{ + guint listener_id; + + (void) userdata; + + listener_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), RAWLOG_THEME_LISTENER_ID_KEY)); + if (listener_id) + { + theme_listener_unregister (listener_id); + g_object_set_data (G_OBJECT (widget), RAWLOG_THEME_LISTENER_ID_KEY, NULL); + } +} + static void close_rawlog (GtkWidget *wid, server *serv) { @@ -126,11 +174,12 @@ open_rawlog (struct server *serv) gtk_widget_set_vexpand (scrolledwindow, TRUE); gtk_box_pack_start (GTK_BOX (vbox), scrolledwindow, TRUE, TRUE, 0); - palette_get_xtext_colors (xtext_palette, XTEXT_COLS); + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); serv->gui->rawlog_textlist = gtk_xtext_new (xtext_palette, 0); gtk_container_add (GTK_CONTAINER (scrolledwindow), serv->gui->rawlog_textlist); gtk_xtext_set_font (GTK_XTEXT (serv->gui->rawlog_textlist), prefs.hex_text_font); GTK_XTEXT (serv->gui->rawlog_textlist)->ignore_hidden = 1; + g_object_set_data (G_OBJECT (serv->gui->rawlog_window), "rawlog-xtext", serv->gui->rawlog_textlist); bbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); gtk_button_box_set_layout (GTK_BUTTON_BOX (bbox), GTK_BUTTONBOX_SPREAD); @@ -144,6 +193,9 @@ open_rawlog (struct server *serv) /* Copy selection to clipboard when Ctrl+Shift+C is pressed AND text auto-copy is disabled */ g_signal_connect (G_OBJECT (serv->gui->rawlog_window), "key_press_event", G_CALLBACK (rawlog_key_cb), serv->gui->rawlog_textlist); + g_object_set_data (G_OBJECT (serv->gui->rawlog_window), RAWLOG_THEME_LISTENER_ID_KEY, + GUINT_TO_POINTER (theme_listener_register ("rawlog.window", rawlog_theme_changed, serv->gui->rawlog_window))); + g_signal_connect (G_OBJECT (serv->gui->rawlog_window), "destroy", G_CALLBACK (rawlog_theme_destroy_cb), NULL); gtk_widget_show_all (serv->gui->rawlog_window); } diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 36fc0822..ed8de201 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -34,10 +34,10 @@ #include "../common/zoitechatc.h" #include "../common/outbound.h" #include "fe-gtk.h" +#include "theme/theme-manager.h" +#include "theme/theme-preferences.h" #include "gtkutil.h" #include "maingui.h" -#include "chanview.h" -#include "palette.h" #include "pixmaps.h" #include "menu.h" #include "plugin-tray.h" @@ -48,8 +48,6 @@ #endif #include "sexy-spell-entry.h" -InputStyle *create_input_style (InputStyle *); - #define LABEL_INDENT 12 static GtkWidget *setup_window = NULL; @@ -57,17 +55,8 @@ static int last_selected_page = 0; static int last_selected_row = 0; /* sound row */ static gboolean color_change; static struct zoitechatprefs setup_prefs; -static GSList *color_selector_widgets; static GtkWidget *cancel_button; static GtkWidget *font_dialog = NULL; -void setup_apply_real (int new_pix, int do_ulist, int do_layout, int do_identd); - -typedef struct -{ - GtkWidget *combo; - GtkWidget *apply_button; - GtkWidget *status_label; -} setup_theme_ui; enum { @@ -344,34 +333,6 @@ static const setting tabs_settings[] = {ST_END, 0, 0, 0, 0, 0} }; -static const setting color_settings[] = -{ - {ST_TOGGLE, N_("Messages"), P_OFFINTNL(hex_text_stripcolor_msg), 0, 0, 0}, - {ST_TOGGLE, N_("Scrollback"), P_OFFINTNL(hex_text_stripcolor_replay), 0, 0, 0}, - {ST_TOGGLE, N_("Topic"), P_OFFINTNL(hex_text_stripcolor_topic), 0, 0, 0}, - - {ST_END, 0, 0, 0, 0, 0} -}; - -static const char *const dark_mode_modes[] = -{ - N_("Auto (system)"), - N_("Dark"), - N_("Light"), - NULL -}; - -static const setting dark_mode_setting = -{ - ST_MENU, - N_("Dark mode:"), - P_OFFINTNL(hex_gui_dark_mode), - N_("Choose how ZoiteChat selects its color palette for the chat buffer, channel list, and user list.\n" - "This includes message colors, selection colors, and interface highlights.\n"), - dark_mode_modes, - 0 -}; - static const char *const dccaccept[] = { N_("Ask for confirmation"), @@ -1474,471 +1435,18 @@ setup_create_page (const setting *set) return tab; } -static void -setup_color_selectors_set_sensitive (gboolean sensitive) -{ - GSList *l = color_selector_widgets; - while (l) - { - GtkWidget *w = (GtkWidget *) l->data; - if (GTK_IS_WIDGET (w)) - gtk_widget_set_sensitive (w, sensitive); - l = l->next; - } -} - -static void -setup_dark_mode_menu_cb (GtkWidget *cbox, const setting *set) -{ - setup_menu_cb (cbox, set); - /* Keep color selectors usable even when dark mode is enabled. */ - setup_color_selectors_set_sensitive (TRUE); -} - -static GtkWidget * -setup_create_dark_mode_menu (GtkWidget *table, int row, const setting *set) -{ - GtkWidget *wid, *cbox, *box; - const char **text = (const char **)set->list; - int i; - - wid = gtk_label_new (_(set->label)); - gtk_widget_set_halign (wid, GTK_ALIGN_START); - gtk_widget_set_valign (wid, GTK_ALIGN_CENTER); - setup_table_attach (table, wid, 2, 3, row, row + 1, FALSE, FALSE, - SETUP_ALIGN_START, SETUP_ALIGN_CENTER, - LABEL_INDENT, 0); - - cbox = gtk_combo_box_text_new (); - - for (i = 0; text[i]; i++) - gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (cbox), _(text[i])); - - gtk_combo_box_set_active (GTK_COMBO_BOX (cbox), - setup_get_int (&setup_prefs, set) - set->extra); - g_signal_connect (G_OBJECT (cbox), "changed", - G_CALLBACK (setup_dark_mode_menu_cb), (gpointer)set); - - box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0); - gtk_box_pack_start (GTK_BOX (box), cbox, 0, 0, 0); - setup_table_attach (table, box, 3, 4, row, row + 1, TRUE, FALSE, - SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0); - - return cbox; -} - -static void -setup_color_button_apply (GtkWidget *button, const PaletteColor *color) -{ - GtkWidget *target = g_object_get_data (G_OBJECT (button), "zoitechat-color-box"); - GtkWidget *apply_widget = GTK_IS_WIDGET (target) ? target : button; - - gtkutil_apply_palette (apply_widget, color, NULL, NULL); - - if (apply_widget != button) - gtkutil_apply_palette (button, color, NULL, NULL); - - gtk_widget_queue_draw (button); -} - -typedef struct -{ - GtkWidget *button; - PaletteColor *color; -} setup_color_dialog_data; - -static void -setup_rgba_from_palette (const PaletteColor *color, GdkRGBA *rgba) -{ - guint16 red, green, blue; - char color_string[16]; - - palette_color_get_rgb16 (color, &red, &green, &blue); - g_snprintf (color_string, sizeof (color_string), "#%04x%04x%04x", - red, green, blue); - if (!gdk_rgba_parse (rgba, color_string)) - { - rgba->red = red / 65535.0; - rgba->green = green / 65535.0; - rgba->blue = blue / 65535.0; - rgba->alpha = 1.0; - } -} - -static void -setup_color_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data) -{ - setup_color_dialog_data *data = user_data; - - if (response_id == GTK_RESPONSE_OK) - { - GdkRGBA rgba; - - gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (dialog), &rgba); - *data->color = rgba; - color_change = TRUE; - setup_color_button_apply (data->button, data->color); - - if (fe_dark_mode_is_enabled_for (setup_prefs.hex_gui_dark_mode)) - palette_dark_set_color ((int)(data->color - colors), data->color); - else - palette_user_set_color ((int)(data->color - colors), data->color); - } - - gtk_widget_destroy (GTK_WIDGET (dialog)); - g_free (data); -} - -static void -setup_color_cb (GtkWidget *button, gpointer userdata) -{ - GtkWidget *dialog; - PaletteColor *color; - GdkRGBA rgba; - setup_color_dialog_data *data; - - color = &colors[GPOINTER_TO_INT (userdata)]; - - dialog = gtk_color_chooser_dialog_new (_("Select color"), GTK_WINDOW (setup_window)); - setup_rgba_from_palette (color, &rgba); - gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (dialog), &rgba); - gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); - - data = g_new0 (setup_color_dialog_data, 1); - data->button = button; - data->color = color; - g_signal_connect (dialog, "response", G_CALLBACK (setup_color_response_cb), data); - gtk_widget_show (dialog); -} - -static void -setup_create_color_button (GtkWidget *table, int num, int row, int col) -{ - GtkWidget *but; - GtkWidget *label; - GtkWidget *box; - char buf[64]; - - if (num > 31) - strcpy (buf, "  "); - else if (num < 10) - sprintf (buf, " %d", num); - else - /* 12345678901 23456789 01 23456789 */ - sprintf (buf, "%d", num); - but = gtk_button_new (); - label = gtk_label_new (" "); - gtk_label_set_markup (GTK_LABEL (label), buf); - box = gtk_event_box_new (); - gtk_event_box_set_visible_window (GTK_EVENT_BOX (box), TRUE); - gtk_container_add (GTK_CONTAINER (box), label); - gtk_container_add (GTK_CONTAINER (but), box); - gtk_widget_set_halign (box, GTK_ALIGN_CENTER); - gtk_widget_set_valign (box, GTK_ALIGN_CENTER); - gtk_widget_show (label); - gtk_widget_show (box); - /* win32 build uses this to turn off themeing */ - g_object_set_data (G_OBJECT (but), "zoitechat-color", (gpointer)1); - g_object_set_data (G_OBJECT (but), "zoitechat-color-box", box); - setup_table_attach (table, but, col, col + 1, row, row + 1, FALSE, FALSE, - SETUP_ALIGN_CENTER, SETUP_ALIGN_CENTER, 0, 0); - g_signal_connect (G_OBJECT (but), "clicked", - G_CALLBACK (setup_color_cb), GINT_TO_POINTER (num)); - setup_color_button_apply (but, &colors[num]); - - /* Track all color selector widgets (used for dark mode UI behavior). */ - color_selector_widgets = g_slist_prepend (color_selector_widgets, but); -} - -static void -setup_create_other_colorR (char *text, int num, int row, GtkWidget *tab) -{ - GtkWidget *label; - - label = gtk_label_new (text); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - setup_table_attach (tab, label, 5, 9, row, row + 1, FALSE, FALSE, - SETUP_ALIGN_START, SETUP_ALIGN_CENTER, - LABEL_INDENT, 0); - setup_create_color_button (tab, num, row, 9); -} - -static void -setup_create_other_color (char *text, int num, int row, GtkWidget *tab) -{ - GtkWidget *label; - - label = gtk_label_new (text); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - setup_table_attach (tab, label, 2, 3, row, row + 1, FALSE, FALSE, - SETUP_ALIGN_START, SETUP_ALIGN_CENTER, - LABEL_INDENT, 0); - setup_create_color_button (tab, num, row, 3); -} - static GtkWidget * setup_create_color_page (void) { - color_selector_widgets = NULL; - - GtkWidget *tab, *box, *label; - int i; - - box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0); - gtk_container_set_border_width (GTK_CONTAINER (box), 6); - - tab = gtkutil_grid_new (9, 2, FALSE); - gtk_container_set_border_width (GTK_CONTAINER (tab), 6); - gtk_grid_set_row_spacing (GTK_GRID (tab), 2); - gtk_grid_set_column_spacing (GTK_GRID (tab), 3); - gtk_container_add (GTK_CONTAINER (box), tab); - - setup_create_header (tab, 0, N_("Text Colors")); - - label = gtk_label_new (_("mIRC colors:")); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - setup_table_attach (tab, label, 2, 3, 1, 2, FALSE, FALSE, - SETUP_ALIGN_START, SETUP_ALIGN_CENTER, - LABEL_INDENT, 0); - - for (i = 0; i < 16; i++) - setup_create_color_button (tab, i, 1, i+3); - - label = gtk_label_new (_("Local colors:")); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - setup_table_attach (tab, label, 2, 3, 2, 3, FALSE, FALSE, - SETUP_ALIGN_START, SETUP_ALIGN_CENTER, - LABEL_INDENT, 0); - - for (i = 16; i < 32; i++) - setup_create_color_button (tab, i, 2, (i+3) - 16); - - setup_create_other_color (_("Foreground:"), COL_FG, 3, tab); - setup_create_other_colorR (_("Background:"), COL_BG, 3, tab); - - setup_create_header (tab, 5, N_("Selected Text")); - - setup_create_other_color (_("Foreground:"), COL_MARK_FG, 6, tab); - setup_create_other_colorR (_("Background:"), COL_MARK_BG, 6, tab); - - setup_create_header (tab, 8, N_("Interface Colors")); - - setup_create_other_color (_("New data:"), COL_NEW_DATA, 9, tab); - setup_create_other_colorR (_("Marker line:"), COL_MARKER, 9, tab); - setup_create_other_color (_("New message:"), COL_NEW_MSG, 10, tab); - setup_create_other_colorR (_("Away user:"), COL_AWAY, 10, tab); - setup_create_other_color (_("Highlight:"), COL_HILIGHT, 11, tab); - setup_create_other_colorR (_("Spell checker:"), COL_SPELL, 11, tab); - setup_create_dark_mode_menu (tab, 13, &dark_mode_setting); - setup_color_selectors_set_sensitive (TRUE); - setup_create_header (tab, 15, N_("Color Stripping")); - - /* label = gtk_label_new (_("Strip colors from:")); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - setup_table_attach (tab, label, 2, 3, 16, 17, FALSE, FALSE, - SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, - LABEL_INDENT, 0); */ - - for (i = 0; i < 3; i++) - { - setup_create_toggleL (tab, i + 16, &color_settings[i]); - } - - return box; -} - -static void -setup_theme_show_message (GtkMessageType message_type, const char *primary) -{ - GtkWidget *dialog; - - dialog = gtk_message_dialog_new (GTK_WINDOW (setup_window), GTK_DIALOG_MODAL, - message_type, GTK_BUTTONS_CLOSE, "%s", primary); - gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_destroy (dialog); -} - -static void -setup_theme_populate (setup_theme_ui *ui) -{ - char *themes_dir; - GDir *dir; - const char *name; - GtkTreeModel *model; - GtkTreeIter iter; - int count; - - model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->combo)); - while (gtk_tree_model_get_iter_first (model, &iter)) - gtk_combo_box_text_remove (GTK_COMBO_BOX_TEXT (ui->combo), 0); - - themes_dir = g_build_filename (get_xdir (), "themes", NULL); - if (!g_file_test (themes_dir, G_FILE_TEST_IS_DIR)) - g_mkdir_with_parents (themes_dir, 0700); - - dir = g_dir_open (themes_dir, 0, NULL); - if (dir) - { - while ((name = g_dir_read_name (dir))) - { - char *path = g_build_filename (themes_dir, name, NULL); - if (g_file_test (path, G_FILE_TEST_IS_DIR)) - gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (ui->combo), name); - g_free (path); - } - g_dir_close (dir); - } - - count = gtk_tree_model_iter_n_children (gtk_combo_box_get_model (GTK_COMBO_BOX (ui->combo)), NULL); - if (count > 0) - gtk_combo_box_set_active (GTK_COMBO_BOX (ui->combo), 0); - - gtk_widget_set_sensitive (ui->apply_button, count > 0); - gtk_label_set_text (GTK_LABEL (ui->status_label), - count > 0 ? _("Select a theme to apply.") : _("No themes found.")); - - g_free (themes_dir); -} - -static void -setup_theme_refresh_cb (GtkWidget *button, gpointer user_data) -{ - setup_theme_ui *ui = user_data; - - setup_theme_populate (ui); -} - -static void -setup_theme_open_folder_cb (GtkWidget *button, gpointer user_data) -{ - char *themes_dir; - - themes_dir = g_build_filename (get_xdir (), "themes", NULL); - g_mkdir_with_parents (themes_dir, 0700); - fe_open_url (themes_dir); - g_free (themes_dir); -} - -static void -setup_theme_selection_changed (GtkComboBox *combo, gpointer user_data) -{ - setup_theme_ui *ui = user_data; - gboolean has_selection = gtk_combo_box_get_active (combo) >= 0; - - gtk_widget_set_sensitive (ui->apply_button, has_selection); -} - -static void -setup_theme_apply_cb (GtkWidget *button, gpointer user_data) -{ - setup_theme_ui *ui = user_data; - GtkWidget *dialog; - gint response; - char *theme; - GError *error = NULL; - - theme = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (ui->combo)); - if (!theme) - return; - - dialog = gtk_message_dialog_new (GTK_WINDOW (setup_window), GTK_DIALOG_MODAL, - GTK_MESSAGE_WARNING, GTK_BUTTONS_OK_CANCEL, - "%s", _("Applying a theme will overwrite your current colors and event settings.\nContinue?")); - response = gtk_dialog_run (GTK_DIALOG (dialog)); - gtk_widget_destroy (dialog); - - if (response != GTK_RESPONSE_OK) - { - g_free (theme); - return; - } - - if (!zoitechat_apply_theme (theme, &error)) - { - setup_theme_show_message (GTK_MESSAGE_ERROR, error ? error->message : _("Failed to apply theme.")); - g_clear_error (&error); - goto cleanup; - } - - palette_load (); - palette_apply_dark_mode (fe_dark_mode_is_enabled ()); - color_change = TRUE; - setup_apply_real (0, TRUE, FALSE, FALSE); - - setup_theme_show_message (GTK_MESSAGE_INFO, _("Theme applied. Some changes may require a restart to take full effect.")); - -cleanup: - g_free (theme); + return theme_preferences_create_color_page (GTK_WINDOW (setup_window), + &setup_prefs, + &color_change); } static GtkWidget * setup_create_theme_page (void) { - setup_theme_ui *ui; - GtkWidget *box; - GtkWidget *label; - GtkWidget *hbox; - GtkWidget *button_box; - char *themes_dir; - char *markup; - - ui = g_new0 (setup_theme_ui, 1); - - box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 6); - gtk_container_set_border_width (GTK_CONTAINER (box), 6); - - themes_dir = g_build_filename (get_xdir (), "themes", NULL); - markup = g_markup_printf_escaped (_("Theme files are loaded from %s."), themes_dir); - label = gtk_label_new (NULL); - gtk_label_set_markup (GTK_LABEL (label), markup); - gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); - gtk_widget_set_halign (label, GTK_ALIGN_START); - gtk_widget_set_valign (label, GTK_ALIGN_CENTER); - gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); - g_free (markup); - g_free (themes_dir); - - hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6); - gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0); - - ui->combo = gtk_combo_box_text_new (); - gtk_box_pack_start (GTK_BOX (hbox), ui->combo, TRUE, TRUE, 0); - g_signal_connect (G_OBJECT (ui->combo), "changed", - G_CALLBACK (setup_theme_selection_changed), ui); - - button_box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6); - gtk_box_pack_start (GTK_BOX (hbox), button_box, FALSE, FALSE, 0); - - ui->apply_button = gtk_button_new_with_mnemonic (_("_Apply Theme")); - gtk_box_pack_start (GTK_BOX (button_box), ui->apply_button, FALSE, FALSE, 0); - g_signal_connect (G_OBJECT (ui->apply_button), "clicked", - G_CALLBACK (setup_theme_apply_cb), ui); - - label = gtk_button_new_with_mnemonic (_("_Refresh")); - gtk_box_pack_start (GTK_BOX (button_box), label, FALSE, FALSE, 0); - g_signal_connect (G_OBJECT (label), "clicked", - G_CALLBACK (setup_theme_refresh_cb), ui); - - label = gtk_button_new_with_mnemonic (_("_Open Folder")); - gtk_box_pack_start (GTK_BOX (button_box), label, FALSE, FALSE, 0); - g_signal_connect (G_OBJECT (label), "clicked", - G_CALLBACK (setup_theme_open_folder_cb), ui); - - ui->status_label = gtk_label_new (NULL); - gtk_widget_set_halign (ui->status_label, GTK_ALIGN_START); - gtk_widget_set_valign (ui->status_label, GTK_ALIGN_CENTER); - gtk_box_pack_start (GTK_BOX (box), ui->status_label, FALSE, FALSE, 0); - - setup_theme_populate (ui); - - g_object_set_data_full (G_OBJECT (box), "setup-theme-ui", ui, g_free); - - return box; + return theme_preferences_create_page (GTK_WINDOW (setup_window), &color_change); } /* === GLOBALS for sound GUI === */ @@ -2405,67 +1913,12 @@ setup_create_tree (GtkWidget *box, GtkWidget *book) } } -static void -setup_apply_entry_style (GtkWidget *entry) -{ - gtkutil_apply_palette (entry, &colors[COL_BG], &colors[COL_FG], - input_style->font_desc); -} - static void setup_apply_to_sess (session_gui *gui) { mg_update_xtext (gui->xtext); - chanview_apply_theme ((chanview *) gui->chanview); - - { - const PaletteColor *bg = NULL; - const PaletteColor *fg = NULL; - const PangoFontDescription *font = NULL; - - if (prefs.hex_gui_ulist_style || fe_dark_mode_is_enabled ()) - bg = &colors[COL_BG]; - if (fe_dark_mode_is_enabled ()) - fg = &colors[COL_FG]; - if (input_style) - font = input_style->font_desc; - - gtkutil_apply_palette (gui->user_tree, bg, fg, font); - } - - if (prefs.hex_gui_input_style) - { - char buf[128]; - GtkCssProvider *provider = gtk_css_provider_new (); - GtkStyleContext *context; - char *color_string = gdk_rgba_to_string (&colors[COL_FG]); - - g_snprintf (buf, sizeof (buf), ".zoitechat-inputbox { caret-color: %s; }", - color_string); - gtk_css_provider_load_from_data (provider, buf, -1, NULL); - g_free (color_string); - - context = gtk_widget_get_style_context (gui->input_box); - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - context = gtk_widget_get_style_context (gui->limit_entry); - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - context = gtk_widget_get_style_context (gui->key_entry); - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - context = gtk_widget_get_style_context (gui->topic_entry); - gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - - g_object_unref (provider); - - setup_apply_entry_style (gui->input_box); - setup_apply_entry_style (gui->limit_entry); - setup_apply_entry_style (gui->key_entry); - setup_apply_entry_style (gui->topic_entry); - } + theme_preferences_apply_to_session (gui, input_style); if (prefs.hex_gui_ulist_buttons) gtk_widget_show (gui->button_box); @@ -2496,7 +1949,7 @@ unslash (char *dir) } void -setup_apply_real (int new_pix, int do_ulist, int do_layout, int do_identd) +setup_apply_real (const ThemeChangedEvent *event) { GSList *list; session *sess; @@ -2509,14 +1962,14 @@ setup_apply_real (int new_pix, int do_ulist, int do_layout, int do_identd) g_mkdir (prefs.hex_dcc_dir, 0700); g_mkdir (prefs.hex_dcc_completed_dir, 0700); - if (new_pix) + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PIXMAP)) { if (channelwin_pix) cairo_surface_destroy (channelwin_pix); channelwin_pix = pixmap_load_from_file (prefs.hex_text_background); } - input_style = create_input_style (input_style); + theme_manager_reload_input_style (); list = sess_list; while (list) @@ -2537,7 +1990,7 @@ setup_apply_real (int new_pix, int do_ulist, int do_layout, int do_identd) log_open_or_close (sess); - if (do_ulist) + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_USERLIST)) userlist_rehash (sess); list = list->next; @@ -2547,10 +2000,10 @@ setup_apply_real (int new_pix, int do_ulist, int do_layout, int do_identd) tray_apply_setup (); zoitechat_reinit_timers (); - if (do_layout) + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_LAYOUT)) menu_change_layout (); - if (do_identd) + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_IDENTD)) handle_command (current_sess, "IDENTD reload", FALSE); } @@ -2562,15 +2015,11 @@ setup_apply (struct zoitechatprefs *pr) PangoFontDescription *new_desc; char buffer[4 * FONTNAMELEN + 1]; #endif - int new_pix = FALSE; - int noapply = FALSE; - int do_ulist = FALSE; - int do_layout = FALSE; - int do_identd = FALSE; - int old_dark_mode = prefs.hex_gui_dark_mode; + int noapply = FALSE; + ThemeChangedEvent event; + struct zoitechatprefs old_prefs = prefs; + int old_dark_mode = prefs.hex_gui_dark_mode; - if (strcmp (pr->hex_text_background, prefs.hex_text_background) != 0) - new_pix = TRUE; #define DIFF(a) (pr->a != prefs.a) @@ -2611,17 +2060,6 @@ setup_apply (struct zoitechatprefs *pr) if (DIFF (hex_gui_input_style) && prefs.hex_gui_input_style == TRUE) noapply = TRUE; /* Requires restart to *disable* */ - if (DIFF (hex_gui_tab_dots)) - do_layout = TRUE; - if (DIFF (hex_gui_tab_layout)) - do_layout = TRUE; - - if (DIFF (hex_identd_server) || DIFF (hex_identd_port)) - do_identd = TRUE; - - if (color_change || (DIFF (hex_gui_ulist_color)) || (DIFF (hex_away_size_max)) || (DIFF (hex_away_track))) - do_ulist = TRUE; - if ((pr->hex_gui_tab_pos == 5 || pr->hex_gui_tab_pos == 6) && pr->hex_gui_tab_layout == 2 && pr->hex_gui_tab_pos != prefs.hex_gui_tab_pos) fe_message (_("You cannot place the tree on the top or bottom!\n" @@ -2638,23 +2076,7 @@ setup_apply (struct zoitechatprefs *pr) memcpy (&prefs, pr, sizeof (prefs)); - /* - * "Dark mode" applies ZoiteChat's built-in dark palette to the chat views. - * - * IMPORTANT: don't short-circuit this call. - * We MUST run palette_apply_dark_mode() when the setting changes, otherwise - * the preference flips but the palette stays the same (aka: "nothing happens"). - */ - { - gboolean pal_changed = FALSE; - - fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, &pal_changed); - if (prefs.hex_gui_dark_mode != old_dark_mode || pal_changed) - color_change = TRUE; - } - - if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO) - fe_set_auto_dark_mode_state (fe_dark_mode_is_enabled_for (ZOITECHAT_DARK_MODE_AUTO)); + event = theme_manager_on_preferences_changed (&old_prefs, &prefs, old_dark_mode, &color_change); #ifdef WIN32 /* merge hex_font_main and hex_font_alternative into hex_font_normal */ @@ -2678,7 +2100,7 @@ setup_apply (struct zoitechatprefs *pr) strcpy (prefs.hex_irc_real_name, "realname"); } - setup_apply_real (new_pix, do_ulist, do_layout, do_identd); + theme_manager_dispatch_setup_apply (&event); if (noapply) fe_message (_("Some settings were changed that require a" @@ -2704,7 +2126,7 @@ setup_ok_cb (GtkWidget *but, GtkWidget *win) gtk_widget_destroy (win); setup_apply (&setup_prefs); save_config (); - palette_save (); + theme_manager_save_preferences (); } static GtkWidget * @@ -2751,11 +2173,6 @@ setup_close_cb (GtkWidget *win, GtkWidget **swin) { *swin = NULL; - if (color_selector_widgets) - { - g_slist_free (color_selector_widgets); - color_selector_widgets = NULL; - } if (font_dialog) { diff --git a/src/fe-gtk/setup.h b/src/fe-gtk/setup.h index 3e3edfe2..c069fb61 100644 --- a/src/fe-gtk/setup.h +++ b/src/fe-gtk/setup.h @@ -20,6 +20,8 @@ #ifndef ZOITECHAT_SETUP_H #define ZOITECHAT_SETUP_H -void setup_apply_real (int new_pix, int do_ulist, int do_layout, int do_identd); +#include "theme/theme-manager.h" + +void setup_apply_real (const ThemeChangedEvent *event); #endif diff --git a/src/fe-gtk/sexy-spell-entry.c b/src/fe-gtk/sexy-spell-entry.c index 904fc7fd..5d57e496 100644 --- a/src/fe-gtk/sexy-spell-entry.c +++ b/src/fe-gtk/sexy-spell-entry.c @@ -47,7 +47,8 @@ #include "../common/cfgfiles.h" #include "../common/zoitechatc.h" -#include "palette.h" +#include "theme/theme-access.h" +#include "theme/theme-palette.h" #include "xtext.h" #include "gtkutil.h" @@ -56,6 +57,34 @@ #define ICON_REMOVE "zc-menu-remove" #define ICON_SPELL_CHECK "zc-menu-spell-check" +static void +color_to_rgb16 (const GdkRGBA *color, guint16 *red, guint16 *green, guint16 *blue) +{ + *red = (guint16) CLAMP (color->red * 65535.0 + 0.5, 0.0, 65535.0); + *green = (guint16) CLAMP (color->green * 65535.0 + 0.5, 0.0, 65535.0); + *blue = (guint16) CLAMP (color->blue * 65535.0 + 0.5, 0.0, 65535.0); +} + +static void +theme_token_color_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue) +{ + GdkRGBA color = { 0 }; + + if (!theme_get_color (token, &color)) + return; + color_to_rgb16 (&color, red, green, blue); +} + +static void +theme_mirc_color_rgb16 (int mirc_idx, guint16 *red, guint16 *green, guint16 *blue) +{ + GdkRGBA color = { 0 }; + + if (!theme_get_mirc_color ((unsigned int) mirc_idx, &color)) + return; + color_to_rgb16 (&color, red, green, blue); +} + /* * Bunch of poop to make enchant into a runtime dependency rather than a * compile-time dependency. This makes it so I don't have to hear the @@ -353,7 +382,7 @@ insert_underline_error (SexySpellEntry *entry, guint start, guint end) guint16 green; guint16 blue; - palette_color_get_rgb16 (&colors[COL_SPELL], &red, &green, &blue); + theme_token_color_rgb16 (THEME_TOKEN_SPELL, &red, &green, &blue); ucolor = pango_attr_underline_color_new (red, green, blue); unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR); @@ -421,27 +450,27 @@ insert_color (SexySpellEntry *entry, guint start, int fgcolor, int bgcolor) guint16 green; guint16 blue; - if (fgcolor < 0 || fgcolor > MAX_COL) + if (fgcolor < 0) { - palette_color_get_rgb16 (&colors[COL_FG], &red, &green, &blue); + theme_token_color_rgb16 (THEME_TOKEN_TEXT_FOREGROUND, &red, &green, &blue); fgattr = pango_attr_foreground_new (red, green, blue); ulattr = pango_attr_underline_color_new (red, green, blue); } else { - palette_color_get_rgb16 (&colors[fgcolor], &red, &green, &blue); + theme_mirc_color_rgb16 (fgcolor, &red, &green, &blue); fgattr = pango_attr_foreground_new (red, green, blue); ulattr = pango_attr_underline_color_new (red, green, blue); } - if (bgcolor < 0 || bgcolor > MAX_COL) + if (bgcolor < 0) { - palette_color_get_rgb16 (&colors[COL_BG], &red, &green, &blue); + theme_token_color_rgb16 (THEME_TOKEN_TEXT_BACKGROUND, &red, &green, &blue); bgattr = pango_attr_background_new (red, green, blue); } else { - palette_color_get_rgb16 (&colors[bgcolor], &red, &green, &blue); + theme_mirc_color_rgb16 (bgcolor, &red, &green, &blue); bgattr = pango_attr_background_new (red, green, blue); } @@ -1053,7 +1082,7 @@ check_attributes (SexySpellEntry *entry, const char *text, int len) case ATTR_REVERSE: insert_hiddenchar (entry, i, i + 1); - insert_color (entry, i, COL_BG, COL_FG); + insert_color (entry, i, THEME_TOKEN_TEXT_BACKGROUND, THEME_TOKEN_TEXT_FOREGROUND); goto check_color; case '\n': diff --git a/src/fe-gtk/textgui.c b/src/fe-gtk/textgui.c index 4ed759c2..3d8cb699 100644 --- a/src/fe-gtk/textgui.c +++ b/src/fe-gtk/textgui.c @@ -35,7 +35,8 @@ #include "gtkutil.h" #include "xtext.h" #include "maingui.h" -#include "palette.h" +#include "theme/theme-access.h" +#include "theme/theme-manager.h" #include "textgui.h" #define ICON_TEXTEVENT_SAVE_AS "document-save-as" @@ -49,6 +50,8 @@ extern char *pntevts[]; static GtkWidget *pevent_dialog = NULL, *pevent_dialog_twid, *pevent_dialog_list, *pevent_dialog_hlist; +#define PEVENT_THEME_LISTENER_ID_KEY "textgui.theme-listener-id" + enum { EVENT_COLUMN, @@ -151,6 +154,51 @@ pevent_dialog_close (GtkWidget *wid, gpointer arg) pevent_save (NULL); } +static void +pevent_dialog_theme_apply (GtkWidget *window) +{ + GtkWidget *xtext; + XTextColor xtext_palette[XTEXT_COLS]; + + if (!window) + return; + + xtext = g_object_get_data (G_OBJECT (window), "xtext"); + if (!xtext) + return; + + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); + gtk_xtext_set_palette (GTK_XTEXT (xtext), xtext_palette); +} + +static void +pevent_dialog_theme_changed (const ThemeChangedEvent *event, gpointer userdata) +{ + GtkWidget *window = userdata; + + if (!theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_THEME_PACK) && + !theme_changed_event_has_reason (event, THEME_CHANGED_REASON_MODE)) + return; + + pevent_dialog_theme_apply (window); +} + +static void +pevent_dialog_theme_destroy_cb (GtkWidget *widget, gpointer userdata) +{ + guint listener_id; + + (void) userdata; + + listener_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), PEVENT_THEME_LISTENER_ID_KEY)); + if (listener_id) + { + theme_listener_unregister (listener_id); + g_object_set_data (G_OBJECT (widget), PEVENT_THEME_LISTENER_ID_KEY, NULL); + } +} + static void pevent_edited (GtkCellRendererText *render, gchar *pathstr, gchar *new_text, gpointer data) { @@ -467,12 +515,16 @@ pevent_dialog_show () gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (wid), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS); gtk_box_pack_start (GTK_BOX (vbox), wid, FALSE, TRUE, 0); - palette_get_xtext_colors (xtext_palette, XTEXT_COLS); + theme_get_xtext_colors (xtext_palette, XTEXT_COLS); pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0); gtk_widget_set_sensitive (pevent_dialog_twid, FALSE); gtk_widget_set_size_request (pevent_dialog_twid, -1, 75); gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid); gtk_xtext_set_font (GTK_XTEXT (pevent_dialog_twid), prefs.hex_text_font); + g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid); + g_object_set_data (G_OBJECT (pevent_dialog), PEVENT_THEME_LISTENER_ID_KEY, + GUINT_TO_POINTER (theme_listener_register ("textgui.events", pevent_dialog_theme_changed, pevent_dialog))); + g_signal_connect (G_OBJECT (pevent_dialog), "destroy", G_CALLBACK (pevent_dialog_theme_destroy_cb), NULL); hbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL); gtk_button_box_set_layout (GTK_BUTTON_BOX (hbox), GTK_BUTTONBOX_SPREAD); diff --git a/src/fe-gtk/theme/tests/test-theme-access-routing.c b/src/fe-gtk/theme/tests/test-theme-access-routing.c new file mode 100644 index 00000000..479ff299 --- /dev/null +++ b/src/fe-gtk/theme/tests/test-theme-access-routing.c @@ -0,0 +1,236 @@ +#include +#include + +#include "../theme-access.h" +#include "../theme-manager.h" +#include "../theme-runtime.h" +#include "../../xtext-color.h" +#include "../../../common/zoitechat.h" + +struct session *current_sess; +struct session *current_tab; +struct session *lastact_sess; +struct zoitechatprefs prefs; + +static gboolean stub_dark_active; +static ThemeSemanticToken stub_last_color_token; +static int stub_runtime_get_color_calls; +static int stub_runtime_widget_calls; +static int stub_runtime_xtext_calls; +static size_t stub_runtime_xtext_last_len; + +static GdkRGBA stub_light_colors[THEME_TOKEN_COUNT]; +static GdkRGBA stub_dark_colors[THEME_TOKEN_COUNT]; + +void +setup_apply_real (const ThemeChangedEvent *event) +{ + (void) event; +} + +gboolean +fe_dark_mode_is_enabled_for (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +gboolean +theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active) +{ + if (mode == ZOITECHAT_DARK_MODE_DARK) + stub_dark_active = TRUE; + if (mode == ZOITECHAT_DARK_MODE_LIGHT) + stub_dark_active = FALSE; + if (dark_active) + *dark_active = stub_dark_active; + return TRUE; +} + +void +theme_runtime_load (void) +{ +} + +void +theme_runtime_save (void) +{ +} + +void +theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col) +{ + (void) token; + (void) col; +} + +void +theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col) +{ + (void) token; + (void) col; +} + +gboolean +theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) +{ + g_assert_nonnull (out_rgba); + stub_runtime_get_color_calls++; + stub_last_color_token = token; + *out_rgba = stub_dark_active ? stub_dark_colors[token] : stub_light_colors[token]; + return TRUE; +} + +void +theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values) +{ + stub_runtime_widget_calls++; + gdk_rgba_parse (&out_values->background, "#010203"); + gdk_rgba_parse (&out_values->foreground, "#fdfcfa"); +} + +void +theme_runtime_get_xtext_colors (XTextColor *palette, size_t palette_len) +{ + size_t i; + + stub_runtime_xtext_calls++; + stub_runtime_xtext_last_len = palette_len; + for (i = 0; i < palette_len; i++) + { + palette[i].red = (unsigned short) (i + 1); + palette[i].green = (unsigned short) (i + 2); + palette[i].blue = (unsigned short) (i + 3); + } +} + +gboolean +theme_runtime_is_dark_active (void) +{ + return stub_dark_active; +} + +static gboolean +rgba_equal (const GdkRGBA *a, const GdkRGBA *b) +{ + return a->red == b->red && a->green == b->green && a->blue == b->blue && a->alpha == b->alpha; +} + +static void +reset_stubs (void) +{ + size_t i; + char light[32]; + char dark[32]; + + stub_dark_active = FALSE; + stub_last_color_token = THEME_TOKEN_MIRC_0; + stub_runtime_get_color_calls = 0; + stub_runtime_widget_calls = 0; + stub_runtime_xtext_calls = 0; + stub_runtime_xtext_last_len = 0; + for (i = 0; i < THEME_TOKEN_COUNT; i++) + { + g_snprintf (light, sizeof (light), "#%02x%02x%02x", (unsigned int) (i + 1), 0x11, 0x22); + g_snprintf (dark, sizeof (dark), "#%02x%02x%02x", (unsigned int) (i + 1), 0xaa, 0xbb); + g_assert_true (gdk_rgba_parse (&stub_light_colors[i], light)); + g_assert_true (gdk_rgba_parse (&stub_dark_colors[i], dark)); + } +} + +static void +test_access_semantic_token_routes_directly (void) +{ + ThemeSemanticToken token; + GdkRGBA color; + size_t i; + + reset_stubs (); + for (i = 0; i < theme_palette_token_def_count (); i++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (i); + + g_assert_nonnull (def); + token = def->token; + g_assert_true (theme_get_color (token, &color)); + g_assert_cmpint (stub_last_color_token, ==, token); + g_assert_true (rgba_equal (&color, &stub_light_colors[token])); + } +} + +static void +test_access_token_routes_without_legacy_accessor (void) +{ + ThemeSemanticToken token = THEME_TOKEN_MIRC_0; + GdkRGBA color; + size_t i; + + reset_stubs (); + for (i = 0; i < theme_palette_token_def_count (); i++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (i); + + g_assert_nonnull (def); + g_assert_true (theme_palette_legacy_index_to_token (def->legacy_index, &token)); + g_assert_true (theme_get_color (token, &color)); + g_assert_cmpint (stub_last_color_token, ==, token); + g_assert_true (rgba_equal (&color, &stub_light_colors[token])); + } +} + +static void +test_access_xtext_palette_forwarding (void) +{ + XTextColor palette[4] = { 0 }; + + reset_stubs (); + theme_get_xtext_colors (palette, G_N_ELEMENTS (palette)); + g_assert_cmpint (stub_runtime_xtext_calls, ==, 1); + g_assert_cmpuint (stub_runtime_xtext_last_len, ==, G_N_ELEMENTS (palette)); + g_assert_cmpuint (palette[0].red, ==, 1); + g_assert_cmpuint (palette[1].green, ==, 3); + g_assert_cmpuint (palette[3].blue, ==, 6); +} + + +static void +test_access_widget_style_forwarding (void) +{ + ThemeWidgetStyleValues values; + + reset_stubs (); + theme_get_widget_style_values (&values); + g_assert_cmpint (stub_runtime_widget_calls, ==, 1); + g_assert_true (fabs (values.background.red - (0x01 / 255.0)) < 0.0001); + g_assert_true (fabs (values.foreground.green - (0xfc / 255.0)) < 0.0001); +} + +static void +test_access_dark_light_switch_affects_token_consumers (void) +{ + ThemeSemanticToken token; + GdkRGBA light; + GdkRGBA dark; + reset_stubs (); + token = THEME_TOKEN_TEXT_FOREGROUND; + g_assert_true (theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL)); + g_assert_true (theme_get_color (token, &light)); + g_assert_true (rgba_equal (&light, &stub_light_colors[token])); + + g_assert_true (theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_DARK, NULL)); + g_assert_true (theme_get_color (token, &dark)); + g_assert_true (rgba_equal (&dark, &stub_dark_colors[token])); + g_assert_false (rgba_equal (&light, &dark)); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/theme/access/semantic_token_routes_directly", test_access_semantic_token_routes_directly); + g_test_add_func ("/theme/access/token_routes_without_legacy_accessor", test_access_token_routes_without_legacy_accessor); + g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding); + g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding); + g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers", + test_access_dark_light_switch_affects_token_consumers); + return g_test_run (); +} 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 new file mode 100644 index 00000000..ccfe7255 --- /dev/null +++ b/src/fe-gtk/theme/tests/test-theme-application-input-style.c @@ -0,0 +1,122 @@ +#include + +#include "../theme-application.h" +#include "../../maingui.h" +#include "../../../common/zoitechat.h" +#include "../../../common/zoitechatc.h" + +struct session *current_sess; +struct session *current_tab; +struct session *lastact_sess; +struct zoitechatprefs prefs; +InputStyle *input_style; + +static gboolean css_enabled; +static PangoFontDescription *css_font_desc; +static int css_reload_calls; +static int message_calls; + +void +fe_message (char *msg, int flags) +{ + (void) msg; + (void) flags; + message_calls++; +} + +void +theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font_desc) +{ + css_enabled = enabled; + if (css_font_desc) + pango_font_description_free (css_font_desc); + css_font_desc = font_desc ? pango_font_description_copy (font_desc) : NULL; + css_reload_calls++; +} + +void +theme_runtime_load (void) +{ +} + +gboolean +theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + (void) mode; + if (palette_changed) + *palette_changed = FALSE; + return FALSE; +} + +static void +reset_state (void) +{ + if (css_font_desc) + { + pango_font_description_free (css_font_desc); + css_font_desc = NULL; + } + if (input_style) + { + if (input_style->font_desc) + pango_font_description_free (input_style->font_desc); + g_free (input_style); + input_style = NULL; + } + css_enabled = FALSE; + css_reload_calls = 0; + message_calls = 0; +} + +static void +test_invalid_font_falls_back_to_sans_11 (void) +{ + InputStyle *style; + + reset_state (); + g_strlcpy (prefs.hex_text_font, "Sans", sizeof (prefs.hex_text_font)); + prefs.hex_gui_input_style = TRUE; + + style = theme_application_update_input_style (NULL); + g_assert_nonnull (style); + g_assert_nonnull (style->font_desc); + g_assert_cmpstr (pango_font_description_get_family (style->font_desc), ==, "sans"); + 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); + g_assert_nonnull (css_font_desc); + + if (style->font_desc) + pango_font_description_free (style->font_desc); + g_free (style); +} + +static void +test_style_toggle_routes_enabled_flag (void) +{ + reset_state (); + g_strlcpy (prefs.hex_text_font, "sans 13", sizeof (prefs.hex_text_font)); + prefs.hex_gui_input_style = FALSE; + input_style = NULL; + + theme_application_reload_input_style (); + g_assert_nonnull (input_style); + g_assert_cmpint (css_reload_calls, ==, 1); + g_assert_false (css_enabled); + g_assert_nonnull (css_font_desc); + g_assert_cmpstr (pango_font_description_get_family (css_font_desc), ==, "sans"); + g_assert_cmpint (pango_font_description_get_size (css_font_desc), ==, 13 * PANGO_SCALE); + g_assert_cmpint (message_calls, ==, 0); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/theme/application/input_style/invalid_font_fallback", + test_invalid_font_falls_back_to_sans_11); + g_test_add_func ("/theme/application/input_style/style_toggle_routes_enabled_flag", + test_style_toggle_routes_enabled_flag); + return g_test_run (); +} 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 new file mode 100644 index 00000000..686e0d0c --- /dev/null +++ b/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c @@ -0,0 +1,229 @@ +#include + +#include "../theme-manager.h" +#include "../../fe-gtk.h" +#include "../../../common/zoitechat.h" +#include "../../../common/zoitechatc.h" + +struct session *current_sess; +struct session *current_tab; +struct session *lastact_sess; +struct zoitechatprefs prefs; + +static gboolean stub_apply_mode_palette_changed; +static gboolean stub_system_prefers_dark; +static int auto_state_calls; +static gboolean last_auto_state; +static int listener_calls; +static ThemeChangedEvent last_event; +static int idle_add_calls; +static guint next_idle_source_id = 33; + +void setup_apply_real (const ThemeChangedEvent *event) +{ + (void) event; +} + +gboolean fe_dark_mode_is_enabled_for (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +void fe_set_auto_dark_mode_state (gboolean enabled) +{ + auto_state_calls++; + last_auto_state = enabled; +} + +gboolean fe_win32_high_contrast_is_enabled (void) +{ + return FALSE; +} + +gboolean fe_win32_try_get_system_dark (gboolean *enabled) +{ + (void) enabled; + return FALSE; +} + +void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback) +{ + (void) callback; +} + +gboolean theme_policy_is_dark_mode_active (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +gboolean theme_policy_system_prefers_dark (void) +{ + return stub_system_prefers_dark; +} + +gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + (void) mode; + if (palette_changed) + *palette_changed = stub_apply_mode_palette_changed; + return TRUE; +} + +void theme_application_reload_input_style (void) +{ +} + +void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ + (void) token; + (void) color; +} + +void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ + (void) token; + (void) color; +} + +gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active) +{ + (void) mode; + (void) dark_active; + return FALSE; +} + +void theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font_desc) +{ + (void) enabled; + (void) font_desc; +} + +void theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc) +{ + (void) widget; + (void) bg; + (void) fg; + (void) font_desc; +} + +void theme_runtime_load (void) +{ +} + +void theme_runtime_save (void) +{ +} + +gboolean theme_runtime_is_dark_active (void) +{ + return FALSE; +} + +void gtkutil_apply_palette (GtkWidget *widget, const GdkRGBA *background, const GdkRGBA *foreground, + const PangoFontDescription *font_desc) +{ + (void) widget; + (void) background; + (void) foreground; + (void) font_desc; +} + +void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values) +{ + gdk_rgba_parse (&out_values->background, "#101010"); + gdk_rgba_parse (&out_values->foreground, "#f0f0f0"); +} + +void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark) +{ + (void) window; + (void) dark; +} + +static void +auto_listener (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + listener_calls++; + last_event = *event; +} + +static guint +immediate_idle_add (GSourceFunc function, gpointer data) +{ + idle_add_calls++; + function (data); + return next_idle_source_id++; +} + +static void +reset_state (void) +{ + stub_apply_mode_palette_changed = FALSE; + stub_system_prefers_dark = FALSE; + auto_state_calls = 0; + last_auto_state = FALSE; + listener_calls = 0; + idle_add_calls = 0; + next_idle_source_id = 33; +} + +static void +test_auto_refresh_dispatches_mode_palette_and_style_reasons (void) +{ + guint listener_id; + + reset_state (); + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_AUTO; + stub_apply_mode_palette_changed = TRUE; + stub_system_prefers_dark = TRUE; + listener_id = theme_listener_register ("auto.refresh", auto_listener, NULL); + theme_manager_set_idle_add_func (immediate_idle_add); + + theme_manager_refresh_auto_mode (); + + g_assert_cmpint (idle_add_calls, ==, 1); + g_assert_cmpint (auto_state_calls, ==, 2); + g_assert_true (last_auto_state); + 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)); + + theme_manager_set_idle_add_func (NULL); + theme_listener_unregister (listener_id); +} + +static void +test_auto_refresh_ignores_non_auto_mode (void) +{ + guint listener_id; + + reset_state (); + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + 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); + + theme_manager_refresh_auto_mode (); + + g_assert_cmpint (idle_add_calls, ==, 1); + g_assert_cmpint (auto_state_calls, ==, 0); + g_assert_cmpint (listener_calls, ==, 0); + + theme_manager_set_idle_add_func (NULL); + theme_listener_unregister (listener_id); +} + +int +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); + 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 new file mode 100644 index 00000000..21ad04e5 --- /dev/null +++ b/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c @@ -0,0 +1,273 @@ +#include +#include + +#include "../theme-manager.h" +#include "../../fe-gtk.h" +#include "../../../common/zoitechat.h" +#include "../../../common/zoitechatc.h" + +struct session *current_sess; +struct session *current_tab; +struct session *lastact_sess; +struct zoitechatprefs prefs; + +static int window_refresh_calls; +static int widget_style_calls; +static int palette_reapply_calls; +static int unmatched_listener_calls; + +void setup_apply_real (const ThemeChangedEvent *event) +{ + (void) event; +} + +gboolean fe_dark_mode_is_enabled_for (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +void fe_set_auto_dark_mode_state (gboolean enabled) +{ + (void) enabled; +} + +gboolean fe_win32_high_contrast_is_enabled (void) +{ + return FALSE; +} + +gboolean fe_win32_try_get_system_dark (gboolean *enabled) +{ + (void) enabled; + return FALSE; +} + +void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback) +{ + (void) callback; +} + +gboolean theme_policy_is_dark_mode_active (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +gboolean theme_policy_system_prefers_dark (void) +{ + return FALSE; +} + +gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + (void) mode; + if (palette_changed) + *palette_changed = FALSE; + return TRUE; +} + +void theme_application_reload_input_style (void) +{ +} + +void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ + (void) token; + (void) color; +} + +void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ + (void) token; + (void) color; +} + +gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active) +{ + (void) mode; + (void) dark_active; + return FALSE; +} + +void theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font_desc) +{ + (void) enabled; + (void) font_desc; +} + +void theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc) +{ + (void) widget; + (void) bg; + (void) fg; + (void) font_desc; +} + +void theme_runtime_load (void) +{ +} + +void theme_runtime_save (void) +{ +} + +gboolean theme_runtime_is_dark_active (void) +{ + return FALSE; +} + +void gtkutil_apply_palette (GtkWidget *widget, const GdkRGBA *background, const GdkRGBA *foreground, + const PangoFontDescription *font_desc) +{ + (void) widget; + (void) background; + (void) foreground; + (void) font_desc; +} + +void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values) +{ + gdk_rgba_parse (&out_values->background, "#101010"); + gdk_rgba_parse (&out_values->foreground, "#f0f0f0"); +} + +void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark) +{ + (void) window; + (void) dark; +} + +static void +window_refresh_listener (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE)) + palette_reapply_calls++; + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_PALETTE) || + theme_changed_event_has_reason (event, THEME_CHANGED_REASON_WIDGET_STYLE)) + window_refresh_calls++; +} + +static void +widget_style_listener (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_WIDGET_STYLE)) + widget_style_calls++; +} + +static void +unmatched_reason_listener (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + if (theme_changed_event_has_reason (event, THEME_CHANGED_REASON_IDENTD)) + unmatched_listener_calls++; +} + +static void +reset_counters (void) +{ + window_refresh_calls = 0; + widget_style_calls = 0; + palette_reapply_calls = 0; + unmatched_listener_calls = 0; +} + +static void +test_dispatch_filters_reasons_across_multiple_subscribers (void) +{ + guint listener_window; + guint listener_widget; + guint listener_unmatched; + + reset_counters (); + listener_window = theme_listener_register ("refresh.window", window_refresh_listener, NULL); + listener_widget = theme_listener_register ("refresh.widget", widget_style_listener, NULL); + listener_unmatched = theme_listener_register ("refresh.unmatched", unmatched_reason_listener, NULL); + + theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE); + g_assert_cmpint (window_refresh_calls, ==, 1); + g_assert_cmpint (palette_reapply_calls, ==, 1); + g_assert_cmpint (widget_style_calls, ==, 0); + g_assert_cmpint (unmatched_listener_calls, ==, 0); + + theme_manager_dispatch_changed (THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST); + g_assert_cmpint (window_refresh_calls, ==, 2); + g_assert_cmpint (palette_reapply_calls, ==, 1); + g_assert_cmpint (widget_style_calls, ==, 1); + g_assert_cmpint (unmatched_listener_calls, ==, 0); + + theme_manager_dispatch_changed (THEME_CHANGED_REASON_LAYOUT); + g_assert_cmpint (window_refresh_calls, ==, 2); + g_assert_cmpint (palette_reapply_calls, ==, 1); + g_assert_cmpint (widget_style_calls, ==, 1); + g_assert_cmpint (unmatched_listener_calls, ==, 0); + + theme_listener_unregister (listener_unmatched); + theme_listener_unregister (listener_widget); + theme_listener_unregister (listener_window); +} + + +static void +test_preferences_change_synthesizes_theme_reasons (void) +{ + struct zoitechatprefs old_prefs = { 0 }; + struct zoitechatprefs new_prefs = { 0 }; + ThemeChangedEvent event; + gboolean color_change = TRUE; + + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + old_prefs.hex_gui_dark_mode = prefs.hex_gui_dark_mode; + new_prefs.hex_gui_dark_mode = prefs.hex_gui_dark_mode; + strcpy (old_prefs.hex_text_background, "old.png"); + strcpy (new_prefs.hex_text_background, "new.png"); + old_prefs.hex_gui_tab_dots = 0; + new_prefs.hex_gui_tab_dots = 1; + old_prefs.hex_identd_port = 113; + new_prefs.hex_identd_port = 114; + old_prefs.hex_gui_ulist_color = 0; + new_prefs.hex_gui_ulist_color = 1; + + event = theme_manager_on_preferences_changed (&old_prefs, &new_prefs, prefs.hex_gui_dark_mode, &color_change); + + g_assert_true (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_PIXMAP)); + g_assert_true (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_LAYOUT)); + g_assert_true (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_IDENTD)); + g_assert_true (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_USERLIST)); + g_assert_true (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_WIDGET_STYLE)); +} + +static void +test_preferences_change_omits_reasons_without_differences (void) +{ + struct zoitechatprefs old_prefs = { 0 }; + struct zoitechatprefs new_prefs = { 0 }; + ThemeChangedEvent event; + gboolean color_change = FALSE; + + prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; + old_prefs.hex_gui_dark_mode = prefs.hex_gui_dark_mode; + new_prefs.hex_gui_dark_mode = prefs.hex_gui_dark_mode; + + event = theme_manager_on_preferences_changed (&old_prefs, &new_prefs, prefs.hex_gui_dark_mode, &color_change); + + g_assert_false (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_PIXMAP)); + g_assert_false (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_LAYOUT)); + g_assert_false (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_IDENTD)); + g_assert_false (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_USERLIST)); + g_assert_false (theme_changed_event_has_reason (&event, THEME_CHANGED_REASON_WIDGET_STYLE)); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/theme/manager/dispatch_filters_reasons_across_multiple_subscribers", + test_dispatch_filters_reasons_across_multiple_subscribers); + g_test_add_func ("/theme/manager/preferences_change_synthesizes_theme_reasons", + test_preferences_change_synthesizes_theme_reasons); + g_test_add_func ("/theme/manager/preferences_change_omits_reasons_without_differences", + test_preferences_change_omits_reasons_without_differences); + return g_test_run (); +} diff --git a/src/fe-gtk/theme/tests/test-theme-manager-policy.c b/src/fe-gtk/theme/tests/test-theme-manager-policy.c new file mode 100644 index 00000000..f64078bc --- /dev/null +++ b/src/fe-gtk/theme/tests/test-theme-manager-policy.c @@ -0,0 +1,368 @@ +#include + +#include "../theme-palette.h" +#include "../theme-manager.h" +#include "../../fe-gtk.h" +#include "../../../common/zoitechat.h" +#include "../../../common/zoitechatc.h" + +struct session *current_sess; +struct session *current_tab; +struct session *lastact_sess; +struct zoitechatprefs prefs; + +static gboolean stub_policy_dark; +static unsigned int stub_policy_mode; +static gboolean stub_apply_mode_result; +static gboolean stub_apply_mode_palette_changed; +static int stub_dark_set_calls; +static int stub_user_set_calls; +static int stub_apply_mode_calls; +static int stub_reload_style_calls; +static ThemeSemanticToken stub_last_dark_token; +static ThemeSemanticToken stub_last_user_token; + +static int listener_a_calls; +static int listener_b_calls; +static ThemeChangedEvent listener_last_event; + +void setup_apply_real (const ThemeChangedEvent *event) +{ + (void) event; +} + +gboolean fe_dark_mode_is_enabled_for (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +void fe_set_auto_dark_mode_state (gboolean enabled) +{ + (void) enabled; +} + +gboolean fe_win32_high_contrast_is_enabled (void) +{ + return FALSE; +} + +gboolean fe_win32_try_get_system_dark (gboolean *enabled) +{ + (void) enabled; + return FALSE; +} + +void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback) +{ + (void) callback; +} + +gboolean theme_policy_is_dark_mode_active (unsigned int mode) +{ + stub_policy_mode = mode; + return stub_policy_dark; +} + +gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + (void) mode; + if (palette_changed) + *palette_changed = stub_apply_mode_palette_changed; + return stub_apply_mode_result; +} + +void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ + (void) color; + stub_dark_set_calls++; + stub_last_dark_token = token; +} + +void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *color) +{ + (void) color; + stub_user_set_calls++; + stub_last_user_token = token; +} + +gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active) +{ + (void) mode; + (void) dark_active; + stub_apply_mode_calls++; + return TRUE; +} + +void theme_application_reload_input_style (void) +{ + stub_reload_style_calls++; +} + + +void theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font_desc) +{ + (void) enabled; + (void) font_desc; +} + +void theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc) +{ + (void) widget; + (void) bg; + (void) fg; + (void) font_desc; +} + +void theme_runtime_load (void) +{ +} + +void theme_runtime_save (void) +{ +} + +gboolean theme_runtime_is_dark_active (void) +{ + return FALSE; +} + +gboolean theme_policy_system_prefers_dark (void) +{ + return FALSE; +} + +void gtkutil_apply_palette (GtkWidget *widget, const GdkRGBA *background, const GdkRGBA *foreground, + const PangoFontDescription *font_desc) +{ + (void) widget; + (void) background; + (void) foreground; + (void) font_desc; +} + +void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values) +{ + gdk_rgba_parse (&out_values->background, "#101010"); + gdk_rgba_parse (&out_values->foreground, "#f0f0f0"); +} + +void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark) +{ + (void) window; + (void) dark; +} + +static void +listener_a (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + listener_a_calls++; + listener_last_event = *event; +} + +static void +listener_b (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + (void) event; + listener_b_calls++; +} + +static void +reset_manager_stubs (void) +{ + stub_policy_dark = FALSE; + stub_policy_mode = 999; + stub_apply_mode_result = TRUE; + stub_apply_mode_palette_changed = FALSE; + stub_dark_set_calls = 0; + stub_user_set_calls = 0; + stub_apply_mode_calls = 0; + stub_reload_style_calls = 0; + stub_last_dark_token = -1; + stub_last_user_token = -1; + listener_a_calls = 0; + listener_b_calls = 0; +} + +static void +test_token_roundtrip (void) +{ + size_t i; + + for (i = 0; i < theme_palette_token_def_count (); i++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (i); + int legacy_idx = -1; + ThemeSemanticToken token = THEME_TOKEN_MIRC_0; + + g_assert_nonnull (def); + g_assert_true (theme_palette_token_to_legacy_index (def->token, &legacy_idx)); + g_assert_cmpint (legacy_idx, ==, def->legacy_index); + g_assert_true (theme_palette_legacy_index_to_token (legacy_idx, &token)); + g_assert_cmpint (token, ==, def->token); + } +} + +static void +test_policy_mode_resolution (void) +{ + g_assert_false (theme_policy_is_dark_mode_active (ZOITECHAT_DARK_MODE_LIGHT)); + g_assert_true (theme_policy_is_dark_mode_active (ZOITECHAT_DARK_MODE_DARK)); +} + +static void +test_manager_set_token_color_routes_by_mode (void) +{ + GdkRGBA color = { 0.1, 0.2, 0.3, 1.0 }; + gboolean palette_changed = FALSE; + + 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); + g_assert_cmpint (stub_reload_style_calls, ==, 1); + g_assert_true (palette_changed); + + 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); + + 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); +} + + +static void +test_manager_set_token_color_routes_setup_indexes (void) +{ + GdkRGBA color = { 0.7, 0.3, 0.2, 1.0 }; + gboolean palette_changed = FALSE; + size_t i; + + for (i = 0; i < theme_palette_token_def_count (); i++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (i); + + g_assert_nonnull (def); + reset_manager_stubs (); + stub_policy_dark = FALSE; + palette_changed = FALSE; + theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, def->token, &color, &palette_changed); + 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); + + reset_manager_stubs (); + 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_true (palette_changed); + } +} + +static void +test_manager_listener_registration_dispatch_and_unregister (void) +{ + guint id_a; + guint id_b; + + reset_manager_stubs (); + id_a = theme_listener_register ("test.a", listener_a, NULL); + id_b = theme_listener_register ("test.b", listener_b, NULL); + g_assert_cmpuint (id_a, >, 0); + g_assert_cmpuint (id_b, >, 0); + + theme_manager_dispatch_changed (THEME_CHANGED_REASON_PIXMAP | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_IDENTD | THEME_CHANGED_REASON_WIDGET_STYLE); + g_assert_cmpint (listener_a_calls, ==, 1); + g_assert_cmpint (listener_b_calls, ==, 1); + g_assert_true (theme_changed_event_has_reason (&listener_last_event, THEME_CHANGED_REASON_PIXMAP)); + g_assert_true (theme_changed_event_has_reason (&listener_last_event, THEME_CHANGED_REASON_USERLIST)); + g_assert_false (theme_changed_event_has_reason (&listener_last_event, THEME_CHANGED_REASON_LAYOUT)); + g_assert_true (theme_changed_event_has_reason (&listener_last_event, THEME_CHANGED_REASON_IDENTD)); + g_assert_true (theme_changed_event_has_reason (&listener_last_event, THEME_CHANGED_REASON_WIDGET_STYLE)); + + theme_listener_unregister (id_a); + theme_manager_dispatch_changed (THEME_CHANGED_REASON_PIXMAP | THEME_CHANGED_REASON_LAYOUT | THEME_CHANGED_REASON_WIDGET_STYLE); + g_assert_cmpint (listener_a_calls, ==, 1); + g_assert_cmpint (listener_b_calls, ==, 2); + + theme_listener_unregister (id_b); +} + +static void +test_manager_window_attach_detach_idempotence (void) +{ + GtkWidget *window; + gulong *first_handler_ptr; + gulong first_handler_id; + gulong *second_handler_ptr; + gulong second_handler_id; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + g_assert_nonnull (window); + + theme_manager_attach_window (window); + first_handler_ptr = g_object_get_data (G_OBJECT (window), "theme-manager-window-destroy-handler"); + g_assert_nonnull (first_handler_ptr); + first_handler_id = *first_handler_ptr; + g_assert_cmpuint (first_handler_id, >, 0); + g_assert_true (g_signal_handler_is_connected (G_OBJECT (window), first_handler_id)); + + theme_manager_attach_window (window); + second_handler_ptr = g_object_get_data (G_OBJECT (window), "theme-manager-window-destroy-handler"); + g_assert_nonnull (second_handler_ptr); + g_assert_true (first_handler_ptr == second_handler_ptr); + g_assert_cmpuint (*second_handler_ptr, ==, first_handler_id); + + theme_manager_detach_window (window); + g_assert_null (g_object_get_data (G_OBJECT (window), "theme-manager-window-destroy-handler")); + g_assert_false (g_signal_handler_is_connected (G_OBJECT (window), first_handler_id)); + + theme_manager_detach_window (window); + g_assert_null (g_object_get_data (G_OBJECT (window), "theme-manager-window-destroy-handler")); + + theme_manager_attach_window (window); + second_handler_ptr = g_object_get_data (G_OBJECT (window), "theme-manager-window-destroy-handler"); + g_assert_nonnull (second_handler_ptr); + second_handler_id = *second_handler_ptr; + g_assert_cmpuint (second_handler_id, >, 0); + g_assert_true (g_signal_handler_is_connected (G_OBJECT (window), second_handler_id)); + + theme_manager_detach_window (window); + g_assert_null (g_object_get_data (G_OBJECT (window), "theme-manager-window-destroy-handler")); + g_assert_false (g_signal_handler_is_connected (G_OBJECT (window), second_handler_id)); + + gtk_widget_destroy (window); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/theme/palette/token_roundtrip", test_token_roundtrip); + g_test_add_func ("/theme/policy/mode_resolution", test_policy_mode_resolution); + g_test_add_func ("/theme/manager/set_token_color_routes_by_mode", test_manager_set_token_color_routes_by_mode); + g_test_add_func ("/theme/manager/set_token_color_routes_setup_indexes", + test_manager_set_token_color_routes_setup_indexes); + g_test_add_func ("/theme/manager/listener_registration_dispatch_and_unregister", + test_manager_listener_registration_dispatch_and_unregister); + g_test_add_func ("/theme/manager/window_attach_detach_idempotence", + test_manager_window_attach_detach_idempotence); + return g_test_run (); +} diff --git a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c new file mode 100644 index 00000000..25ed1083 --- /dev/null +++ b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c @@ -0,0 +1,281 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../theme-runtime.h" +#include "../../../common/zoitechat.h" +#include "../../../common/zoitechatc.h" + +struct session *current_sess; +struct session *current_tab; +struct session *lastact_sess; +struct zoitechatprefs prefs; + +static char *test_home_dir; + +static gboolean +read_line_value (const char *cfg, const char *key, char *out, gsize out_len) +{ + char *pattern; + char *pos; + char *line_end; + gsize value_len; + + pattern = g_strdup_printf ("%s = ", key); + pos = g_strstr_len (cfg, -1, pattern); + g_free (pattern); + if (!pos) + return FALSE; + + pos = strchr (pos, '='); + if (!pos) + return FALSE; + pos++; + while (*pos == ' ') + pos++; + + line_end = strchr (pos, '\n'); + if (!line_end) + line_end = pos + strlen (pos); + value_len = (gsize) (line_end - pos); + if (value_len + 1 > out_len) + return FALSE; + memcpy (out, pos, value_len); + out[value_len] = '\0'; + return TRUE; +} + +int +cfg_get_color (char *cfg, char *var, guint16 *r, guint16 *g, guint16 *b) +{ + char value[128]; + + if (!read_line_value (cfg, var, value, sizeof (value))) + return 0; + if (sscanf (value, "%04hx %04hx %04hx", r, g, b) != 3) + return 0; + return 1; +} + +int +cfg_get_int (char *cfg, char *var) +{ + char value[128]; + + if (!read_line_value (cfg, var, value, sizeof (value))) + return 0; + return atoi (value); +} + +int +cfg_put_color (int fh, guint16 r, guint16 g, guint16 b, char *var) +{ + char line[256]; + int len; + + len = g_snprintf (line, sizeof line, "%s = %04hx %04hx %04hx\n", var, r, g, b); + if (len < 0) + return 0; + return write (fh, line, (size_t) len) == len; +} + +int +cfg_put_int (int fh, int value, char *var) +{ + char line[128]; + int len; + + len = g_snprintf (line, sizeof line, "%s = %d\n", var, value); + if (len < 0) + return 0; + return write (fh, line, (size_t) len) == len; +} + +int +zoitechat_open_file (const char *file, int flags, int mode, int xof_flags) +{ + char *path; + int fd; + + (void) xof_flags; + path = g_build_filename (test_home_dir, file, NULL); + fd = g_open (path, flags, mode); + g_free (path); + return fd; +} + +gboolean +fe_dark_mode_is_enabled_for (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +gboolean +theme_policy_system_prefers_dark (void) +{ + return FALSE; +} + +gboolean +theme_policy_is_dark_mode_active (unsigned int mode) +{ + return mode == ZOITECHAT_DARK_MODE_DARK; +} + +static void +setup_temp_home (void) +{ + if (test_home_dir) + return; + test_home_dir = g_dir_make_tmp ("zoitechat-theme-tests-XXXXXX", NULL); + g_assert_nonnull (test_home_dir); +} + +static char * +read_colors_conf (void) +{ + char *path; + char *content = NULL; + gsize length = 0; + gboolean ok; + + path = g_build_filename (test_home_dir, "colors.conf", NULL); + ok = g_file_get_contents (path, &content, &length, NULL); + g_free (path); + g_assert_true (ok); + g_assert_cmpuint (length, >, 0); + return content; +} + +static gboolean +colors_equal (const GdkRGBA *a, const GdkRGBA *b) +{ + return a->red == b->red && a->green == b->green && a->blue == b->blue; +} + +static void +apply_ui_color_edit (unsigned int mode, ThemeSemanticToken token, const char *hex) +{ + GdkRGBA color; + + g_assert_true (gdk_rgba_parse (&color, hex)); + if (theme_policy_is_dark_mode_active (mode)) + theme_runtime_dark_set_color (token, &color); + else + theme_runtime_user_set_color (token, &color); + theme_runtime_apply_mode (mode, NULL); +} + +static void +test_persistence_roundtrip_light_and_dark (void) +{ + GdkRGBA light_color; + GdkRGBA dark_color; + GdkRGBA loaded; + char *cfg; + + setup_temp_home (); + theme_runtime_load (); + + gdk_rgba_parse (&light_color, "#123456"); + theme_runtime_user_set_color (THEME_TOKEN_MIRC_0, &light_color); + theme_runtime_apply_dark_mode (FALSE); + + theme_runtime_apply_dark_mode (TRUE); + gdk_rgba_parse (&dark_color, "#abcdef"); + theme_runtime_dark_set_color (THEME_TOKEN_MIRC_0, &dark_color); + + theme_runtime_save (); + cfg = read_colors_conf (); + g_assert_nonnull (g_strstr_len (cfg, -1, "theme.mode.light.token.mirc_0")); + g_assert_nonnull (g_strstr_len (cfg, -1, "theme.mode.dark.token.mirc_0")); + g_assert_null (g_strstr_len (cfg, -1, "color_0 = ")); + g_assert_null (g_strstr_len (cfg, -1, "dark_color_0 = ")); + g_free (cfg); + + theme_runtime_load (); + theme_runtime_apply_dark_mode (FALSE); + g_assert_true (theme_runtime_get_color (THEME_TOKEN_MIRC_0, &loaded)); + g_assert_true (colors_equal (&light_color, &loaded)); + + theme_runtime_apply_dark_mode (TRUE); + g_assert_true (theme_runtime_get_color (THEME_TOKEN_MIRC_0, &loaded)); + g_assert_true (colors_equal (&dark_color, &loaded)); +} + +static void +test_loads_legacy_color_keys_via_migration_loader (void) +{ + char *path; + const char *legacy_cfg = + "color_0 = 1111 2222 3333\n" + "dark_color_0 = aaaa bbbb cccc\n"; + GdkRGBA loaded; + GdkRGBA light_expected; + GdkRGBA dark_expected; + gboolean ok; + + setup_temp_home (); + path = g_build_filename (test_home_dir, "colors.conf", NULL); + ok = g_file_set_contents (path, legacy_cfg, -1, NULL); + g_free (path); + g_assert_true (ok); + + theme_runtime_load (); + + gdk_rgba_parse (&light_expected, "#111122223333"); + gdk_rgba_parse (&dark_expected, "#aaaabbbbcccc"); + + theme_runtime_apply_dark_mode (FALSE); + g_assert_true (theme_runtime_get_color (THEME_TOKEN_MIRC_0, &loaded)); + g_assert_true (colors_equal (&loaded, &light_expected)); + + theme_runtime_apply_dark_mode (TRUE); + g_assert_true (theme_runtime_get_color (THEME_TOKEN_MIRC_0, &loaded)); + g_assert_true (colors_equal (&loaded, &dark_expected)); +} + + +static void +test_ui_edits_persist_without_legacy_array_mutation (void) +{ + GdkRGBA light_loaded; + GdkRGBA dark_loaded; + GdkRGBA light_expected; + GdkRGBA dark_expected; + + setup_temp_home (); + theme_runtime_load (); + + apply_ui_color_edit (ZOITECHAT_DARK_MODE_LIGHT, THEME_TOKEN_SELECTION_FOREGROUND, "#224466"); + apply_ui_color_edit (ZOITECHAT_DARK_MODE_DARK, THEME_TOKEN_SELECTION_FOREGROUND, "#88aacc"); + theme_runtime_save (); + + theme_runtime_load (); + theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL); + 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); + 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)); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/theme/runtime/persistence_roundtrip_light_and_dark", + test_persistence_roundtrip_light_and_dark); + g_test_add_func ("/theme/runtime/loads_legacy_color_keys_via_migration_loader", + test_loads_legacy_color_keys_via_migration_loader); + g_test_add_func ("/theme/runtime/ui_edits_persist_without_legacy_array_mutation", + test_ui_edits_persist_without_legacy_array_mutation); + return g_test_run (); +} diff --git a/src/fe-gtk/theme/theme-access.c b/src/fe-gtk/theme/theme-access.c new file mode 100644 index 00000000..621649c1 --- /dev/null +++ b/src/fe-gtk/theme/theme-access.c @@ -0,0 +1,73 @@ +#include "theme-access.h" + +#include "theme-runtime.h" + + +static gboolean +theme_token_to_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue) +{ + GdkRGBA color = { 0 }; + + g_return_val_if_fail (red != NULL, FALSE); + g_return_val_if_fail (green != NULL, FALSE); + g_return_val_if_fail (blue != NULL, FALSE); + if (!theme_runtime_get_color (token, &color)) + return FALSE; + theme_palette_color_get_rgb16 (&color, red, green, blue); + return TRUE; +} + +gboolean +theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) +{ + return theme_runtime_get_color (token, out_rgba); +} + +gboolean +theme_get_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba) +{ + ThemeSemanticToken token = (ThemeSemanticToken) (THEME_TOKEN_MIRC_0 + (int) mirc_index); + + if (mirc_index >= 32) + return FALSE; + return theme_runtime_get_color (token, out_rgba); +} + +gboolean +theme_get_color_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue) +{ + return theme_token_to_rgb16 (token, red, green, blue); +} + +gboolean +theme_get_mirc_color_rgb16 (unsigned int mirc_index, guint16 *red, guint16 *green, guint16 *blue) +{ + ThemeSemanticToken token = (ThemeSemanticToken) (THEME_TOKEN_MIRC_0 + (int) mirc_index); + + if (mirc_index >= 32) + return FALSE; + return theme_token_to_rgb16 (token, red, green, blue); +} + +gboolean +theme_get_legacy_color (int legacy_idx, GdkRGBA *out_rgba) +{ + ThemeSemanticToken token; + + g_return_val_if_fail (out_rgba != NULL, FALSE); + if (!theme_palette_legacy_index_to_token (legacy_idx, &token)) + return FALSE; + return theme_runtime_get_color (token, out_rgba); +} + +void +theme_get_widget_style_values (ThemeWidgetStyleValues *out_values) +{ + theme_runtime_get_widget_style_values (out_values); +} + +void +theme_get_xtext_colors (XTextColor *palette, size_t palette_len) +{ + theme_runtime_get_xtext_colors (palette, palette_len); +} diff --git a/src/fe-gtk/theme/theme-access.h b/src/fe-gtk/theme/theme-access.h new file mode 100644 index 00000000..9db6ddb8 --- /dev/null +++ b/src/fe-gtk/theme/theme-access.h @@ -0,0 +1,20 @@ +#ifndef ZOITECHAT_THEME_ACCESS_H +#define ZOITECHAT_THEME_ACCESS_H + +#include + +#include + +#include "theme-palette.h" +#include "../xtext-color.h" + +gboolean theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba); +gboolean theme_get_mirc_color (unsigned int mirc_index, GdkRGBA *out_rgba); +gboolean theme_get_color_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue); +gboolean theme_get_mirc_color_rgb16 (unsigned int mirc_index, guint16 *red, guint16 *green, guint16 *blue); +G_DEPRECATED_FOR (theme_get_color) +gboolean theme_get_legacy_color (int legacy_idx, GdkRGBA *out_rgba); +void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values); +void theme_get_xtext_colors (XTextColor *palette, size_t palette_len); + +#endif diff --git a/src/fe-gtk/theme/theme-application.c b/src/fe-gtk/theme/theme-application.c new file mode 100644 index 00000000..fd8e2c5c --- /dev/null +++ b/src/fe-gtk/theme/theme-application.c @@ -0,0 +1,83 @@ +#include "theme-application.h" + +#include "../../common/fe.h" +#include "../../common/zoitechatc.h" +#include "theme-css.h" +#include "theme-runtime.h" +#include "../maingui.h" + +#ifdef G_OS_WIN32 +#include + +static void +theme_application_apply_windows_theme (gboolean dark) +{ + GtkSettings *settings = gtk_settings_get_default (); + + if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), + "gtk-application-prefer-dark-theme")) + { + g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL); + } + + { + static GtkCssProvider *win_theme_provider = NULL; + char *css = theme_css_build_toplevel_classes (); + + if (!win_theme_provider) + win_theme_provider = gtk_css_provider_new (); + + gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL); + theme_css_apply_app_provider (GTK_STYLE_PROVIDER (win_theme_provider)); + g_free (css); + } +} +#endif + +gboolean +theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + gboolean dark; + + theme_runtime_load (); + dark = theme_runtime_apply_mode (mode, palette_changed); + +#ifdef G_OS_WIN32 + theme_application_apply_windows_theme (dark); +#endif + + theme_application_reload_input_style (); + + return dark; +} + +void +theme_application_reload_input_style (void) +{ + input_style = theme_application_update_input_style (input_style); +} + +InputStyle * +theme_application_update_input_style (InputStyle *style) +{ + char buf[256]; + + if (!style) + style = g_new0 (InputStyle, 1); + + if (style->font_desc) + pango_font_description_free (style->font_desc); + style->font_desc = pango_font_description_from_string (prefs.hex_text_font); + + if (pango_font_description_get_size (style->font_desc) == 0) + { + g_snprintf (buf, sizeof (buf), _("Failed to open font:\n\n%s"), prefs.hex_text_font); + fe_message (buf, FE_MSG_ERROR); + pango_font_description_free (style->font_desc); + style->font_desc = pango_font_description_from_string ("sans 11"); + } + + theme_css_reload_input_style (prefs.hex_gui_input_style, style->font_desc); + + return style; +} diff --git a/src/fe-gtk/theme/theme-application.h b/src/fe-gtk/theme/theme-application.h new file mode 100644 index 00000000..99f770c9 --- /dev/null +++ b/src/fe-gtk/theme/theme-application.h @@ -0,0 +1,11 @@ +#ifndef ZOITECHAT_THEME_APPLICATION_H +#define ZOITECHAT_THEME_APPLICATION_H + +#include +#include "../fe-gtk.h" + +gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed); +void theme_application_reload_input_style (void); +InputStyle *theme_application_update_input_style (InputStyle *style); + +#endif diff --git a/src/fe-gtk/theme/theme-css.c b/src/fe-gtk/theme/theme-css.c new file mode 100644 index 00000000..b7a2ee59 --- /dev/null +++ b/src/fe-gtk/theme/theme-css.c @@ -0,0 +1,298 @@ +#include "theme-css.h" + +#include "theme-runtime.h" +#include "../gtkutil.h" +#include + +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_APPLICATION; + +typedef struct +{ + char *theme_name; + char *font; + gboolean enabled; + gboolean dark; + gboolean colors_set; + guint16 fg_red; + guint16 fg_green; + guint16 fg_blue; + guint16 bg_red; + guint16 bg_green; + guint16 bg_blue; +} ThemeCssInputFingerprint; + +static GtkCssProvider *theme_css_input_provider; +static ThemeCssInputFingerprint theme_css_input_fp; + +void +theme_css_apply_app_provider (GtkStyleProvider *provider) +{ + GdkScreen *screen; + + if (!provider) + return; + + screen = gdk_screen_get_default (); + if (!screen) + return; + + gtk_style_context_add_provider_for_screen (screen, provider, theme_css_provider_priority); +} + +void +theme_css_remove_app_provider (GtkStyleProvider *provider) +{ + GdkScreen *screen; + + if (!provider) + return; + + screen = gdk_screen_get_default (); + if (!screen) + return; + + gtk_style_context_remove_provider_for_screen (screen, provider); +} + +void +theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider) +{ + GtkStyleContext *context; + + if (!widget || !provider) + return; + + context = gtk_widget_get_style_context (widget); + if (!context) + return; + + gtk_style_context_add_provider (context, provider, theme_css_provider_priority); +} + +static gboolean +theme_css_input_fingerprint_matches (const ThemeCssInputFingerprint *next) +{ + if (theme_css_input_fp.enabled != next->enabled) + return FALSE; + if (theme_css_input_fp.dark != next->dark) + return FALSE; + if (theme_css_input_fp.colors_set != next->colors_set) + return FALSE; + if (theme_css_input_fp.fg_red != next->fg_red || theme_css_input_fp.fg_green != next->fg_green + || theme_css_input_fp.fg_blue != next->fg_blue) + return FALSE; + if (theme_css_input_fp.bg_red != next->bg_red || theme_css_input_fp.bg_green != next->bg_green + || theme_css_input_fp.bg_blue != next->bg_blue) + return FALSE; + if (g_strcmp0 (theme_css_input_fp.theme_name, next->theme_name) != 0) + return FALSE; + if (g_strcmp0 (theme_css_input_fp.font, next->font) != 0) + return FALSE; + return TRUE; +} + +static void +theme_css_input_fingerprint_replace (ThemeCssInputFingerprint *next) +{ + g_free (theme_css_input_fp.theme_name); + g_free (theme_css_input_fp.font); + theme_css_input_fp = *next; + next->theme_name = NULL; + next->font = NULL; +} + +static void +theme_css_input_fingerprint_clear (void) +{ + g_free (theme_css_input_fp.theme_name); + g_free (theme_css_input_fp.font); + memset (&theme_css_input_fp, 0, sizeof (theme_css_input_fp)); +} + +static char * +theme_css_build_input (const char *theme_name, guint16 fg_red, guint16 fg_green, guint16 fg_blue, + guint16 bg_red, guint16 bg_green, guint16 bg_blue) +{ + GString *css = g_string_new (""); + + if (g_str_has_prefix (theme_name, "Adwaita") || g_str_has_prefix (theme_name, "Yaru")) + { + g_string_append_printf (css, "%s { background-image: none; }", theme_css_selector_input); + } + + g_string_append_printf (css, + "%s {" + "background-color: #%02x%02x%02x;" + "color: #%02x%02x%02x;" + "caret-color: #%02x%02x%02x;" + "}" + "%s {" + "color: #%02x%02x%02x;" + "caret-color: #%02x%02x%02x;" + "}", + theme_css_selector_input, + (bg_red >> 8), (bg_green >> 8), (bg_blue >> 8), + (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), + (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), + theme_css_selector_input_text, + (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8), + (fg_red >> 8), (fg_green >> 8), (fg_blue >> 8)); + + return g_string_free (css, FALSE); +} + +void +theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font_desc) +{ + ThemeCssInputFingerprint next = {0}; + + next.enabled = enabled; + next.dark = theme_runtime_is_dark_active (); + next.theme_name = NULL; + next.font = font_desc ? pango_font_description_to_string (font_desc) : NULL; + + if (enabled) + { + GtkSettings *settings = gtk_settings_get_default (); + char *theme_name = NULL; + char *css; + + if (settings) + g_object_get (settings, "gtk-theme-name", &theme_name, NULL); + + next.theme_name = g_strdup (theme_name); + { + GdkRGBA color; + + if (theme_runtime_get_color (THEME_TOKEN_TEXT_FOREGROUND, &color)) + { + theme_palette_color_get_rgb16 (&color, &next.fg_red, &next.fg_green, &next.fg_blue); + next.colors_set = TRUE; + } + if (theme_runtime_get_color (THEME_TOKEN_TEXT_BACKGROUND, &color)) + { + theme_palette_color_get_rgb16 (&color, &next.bg_red, &next.bg_green, &next.bg_blue); + next.colors_set = TRUE; + } + } + + if (theme_css_input_fingerprint_matches (&next)) + { + g_free (theme_name); + g_free (next.theme_name); + g_free (next.font); + return; + } + + if (!theme_css_input_provider) + theme_css_input_provider = gtk_css_provider_new (); + + css = theme_css_build_input (theme_name ? theme_name : "", + next.fg_red, next.fg_green, next.fg_blue, + next.bg_red, next.bg_green, next.bg_blue); + gtk_css_provider_load_from_data (theme_css_input_provider, css, -1, NULL); + g_free (css); + theme_css_apply_app_provider (GTK_STYLE_PROVIDER (theme_css_input_provider)); + + g_free (theme_name); + theme_css_input_fingerprint_replace (&next); + return; + } + + if (theme_css_input_provider) + theme_css_remove_app_provider (GTK_STYLE_PROVIDER (theme_css_input_provider)); + g_clear_object (&theme_css_input_provider); + theme_css_input_fingerprint_clear (); + g_free (next.theme_name); + g_free (next.font); +} + +void +theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc) +{ + GtkCssProvider *provider; + gboolean new_provider = FALSE; + GString *css; + gchar *bg_color = NULL; + gchar *fg_color = NULL; + + if (!widget) + return; + + provider = g_object_get_data (G_OBJECT (widget), theme_css_palette_provider_key); + + if (!bg && !fg && !font_desc) + { + gtk_style_context_remove_class (gtk_widget_get_style_context (widget), theme_css_selector_palette_class); + if (provider) + { + gtk_style_context_remove_provider (gtk_widget_get_style_context (widget), GTK_STYLE_PROVIDER (provider)); + g_object_set_data (G_OBJECT (widget), theme_css_palette_provider_key, NULL); + } + return; + } + + if (!provider) + { + provider = gtk_css_provider_new (); + g_object_set_data_full (G_OBJECT (widget), theme_css_palette_provider_key, + provider, g_object_unref); + new_provider = TRUE; + } + + css = g_string_new ("."); + g_string_append (css, theme_css_selector_palette_class); + g_string_append (css, " {"); + if (bg) + { + bg_color = gdk_rgba_to_string (bg); + g_string_append_printf (css, " background-color: %s;", bg_color); + } + if (fg) + { + fg_color = gdk_rgba_to_string (fg); + g_string_append_printf (css, " color: %s;", fg_color); + } + gtkutil_append_font_css (css, font_desc); + g_string_append (css, " }"); + g_string_append_printf (css, ".%s *:selected {", theme_css_selector_palette_class); + if (bg) + g_string_append (css, " background-color: @theme_selected_bg_color;"); + if (fg) + g_string_append (css, " color: @theme_selected_fg_color;"); + g_string_append (css, " }"); + + gtk_css_provider_load_from_data (provider, css->str, -1, NULL); + if (new_provider) + theme_css_apply_widget_provider (widget, GTK_STYLE_PROVIDER (provider)); + gtk_style_context_add_class (gtk_widget_get_style_context (widget), theme_css_selector_palette_class); + + g_string_free (css, TRUE); + g_free (bg_color); + g_free (fg_color); +} + +char * +theme_css_build_toplevel_classes (void) +{ + return g_strdup_printf ( + "window.%s, .%s {" + "background-color: #202020;" + "color: #f0f0f0;" + "}" + "window.%s, .%s {" + "background-color: #f6f6f6;" + "color: #101010;" + "}", + theme_css_selector_dark_class, + theme_css_selector_dark_class, + theme_css_selector_light_class, + theme_css_selector_light_class); +} diff --git a/src/fe-gtk/theme/theme-css.h b/src/fe-gtk/theme/theme-css.h new file mode 100644 index 00000000..5c5e5069 --- /dev/null +++ b/src/fe-gtk/theme/theme-css.h @@ -0,0 +1,21 @@ +#ifndef ZOITECHAT_THEME_CSS_H +#define ZOITECHAT_THEME_CSS_H + +#include "../fe-gtk.h" + +/** + * theme_css_apply_app_provider/theme_css_remove_app_provider: + * Use for CSS providers that should apply to the entire application screen. + * + * theme_css_apply_widget_provider: + * Use for widget-local CSS providers attached to a specific widget context. + */ +void theme_css_apply_app_provider (GtkStyleProvider *provider); +void theme_css_remove_app_provider (GtkStyleProvider *provider); +void theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider); +void theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font_desc); +void theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc); +char *theme_css_build_toplevel_classes (void); + +#endif diff --git a/src/fe-gtk/theme/theme-gtk.h b/src/fe-gtk/theme/theme-gtk.h new file mode 100644 index 00000000..883ae35b --- /dev/null +++ b/src/fe-gtk/theme/theme-gtk.h @@ -0,0 +1,9 @@ +#ifndef ZOITECHAT_THEME_GTK_H +#define ZOITECHAT_THEME_GTK_H + +#include + +#define THEME_GTK_COLOR_TYPE GDK_TYPE_RGBA +#define THEME_GTK_FOREGROUND_PROPERTY "foreground-rgba" + +#endif diff --git a/src/fe-gtk/theme/theme-manager.c b/src/fe-gtk/theme/theme-manager.c new file mode 100644 index 00000000..f7d318bd --- /dev/null +++ b/src/fe-gtk/theme/theme-manager.c @@ -0,0 +1,497 @@ +#include "../fe-gtk.h" +#include "theme-manager.h" + +#include + +#include "theme-application.h" +#include "theme-policy.h" +#include "theme-runtime.h" +#include "theme-access.h" +#include "theme-css.h" +#include "../gtkutil.h" +#include "../maingui.h" +#include "../setup.h" +#include "../../common/zoitechat.h" +#include "../../common/zoitechatc.h" + +typedef struct +{ + guint id; + char *component_id; + ThemeChangedCallback callback; + gpointer userdata; +} ThemeListener; + +static GHashTable *theme_manager_listeners; +static guint theme_manager_next_listener_id = 1; +static guint theme_manager_setup_listener_id; +static const char theme_manager_window_destroy_handler_key[] = "theme-manager-window-destroy-handler"; + +static void +theme_listener_free (gpointer data) +{ + ThemeListener *listener = data; + + if (!listener) + return; + + g_free (listener->component_id); + g_free (listener); +} + +static void +theme_manager_setup_apply_listener (const ThemeChangedEvent *event, gpointer userdata) +{ + (void) userdata; + theme_manager_dispatch_setup_apply (event); +} + +static ThemeChangedReason +theme_manager_synthesize_preference_reasons (const struct zoitechatprefs *old_prefs, + const struct zoitechatprefs *new_prefs, + gboolean color_change) +{ + ThemeChangedReason reasons = THEME_CHANGED_REASON_NONE; + + if (!old_prefs || !new_prefs) + return reasons; + + if (strcmp (old_prefs->hex_text_background, new_prefs->hex_text_background) != 0) + reasons |= THEME_CHANGED_REASON_PIXMAP; + if (old_prefs->hex_gui_tab_dots != new_prefs->hex_gui_tab_dots || + old_prefs->hex_gui_tab_layout != new_prefs->hex_gui_tab_layout) + reasons |= THEME_CHANGED_REASON_LAYOUT; + if (old_prefs->hex_identd_server != new_prefs->hex_identd_server || + old_prefs->hex_identd_port != new_prefs->hex_identd_port) + reasons |= THEME_CHANGED_REASON_IDENTD; + if (color_change || + old_prefs->hex_gui_ulist_color != new_prefs->hex_gui_ulist_color || + old_prefs->hex_away_size_max != new_prefs->hex_away_size_max || + old_prefs->hex_away_track != new_prefs->hex_away_track) + reasons |= THEME_CHANGED_REASON_USERLIST; + + if (reasons != THEME_CHANGED_REASON_NONE) + reasons |= THEME_CHANGED_REASON_WIDGET_STYLE; + + return reasons; +} + +static void +theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer data) +{ + gboolean color_change = FALSE; + static gboolean in_handler = FALSE; + + (void) settings; + (void) pspec; + (void) data; + + if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO) + return; + if (in_handler) + return; + + in_handler = TRUE; + + fe_set_auto_dark_mode_state (theme_policy_system_prefers_dark ()); + 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); + + in_handler = FALSE; +} + +static guint theme_manager_auto_refresh_source = 0; +static ThemeManagerIdleAddFunc theme_manager_idle_add_func = g_idle_add; + +static gboolean +theme_manager_run_auto_refresh (gpointer data) +{ + theme_manager_auto_refresh_source = 0; + theme_manager_auto_dark_mode_changed (NULL, NULL, data); + return G_SOURCE_REMOVE; +} + +static void +theme_manager_queue_auto_refresh (GtkSettings *settings, GParamSpec *pspec, gpointer data) +{ + (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); +} + +void +theme_manager_init (void) +{ + GtkSettings *settings; + + if (!theme_manager_listeners) + theme_manager_listeners = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, + theme_listener_free); + + if (!theme_manager_setup_listener_id) + theme_manager_setup_listener_id = theme_listener_register ("setup.apply", theme_manager_setup_apply_listener, NULL); + + settings = gtk_settings_get_default (); + if (settings) + fe_set_auto_dark_mode_state (theme_policy_system_prefers_dark ()); + + theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL); + zoitechat_set_theme_post_apply_callback (theme_manager_handle_theme_applied); + + if (settings) + { + g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme", + G_CALLBACK (theme_manager_queue_auto_refresh), NULL); + g_signal_connect (settings, "notify::gtk-theme-name", + G_CALLBACK (theme_manager_queue_auto_refresh), NULL); + } +} + +gboolean +theme_manager_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + return theme_application_apply_mode (mode, palette_changed); +} + +void +theme_manager_set_mode (unsigned int mode, gboolean *palette_changed) +{ + theme_application_apply_mode (mode, palette_changed); +} + +void +theme_manager_set_token_color (unsigned int mode, ThemeSemanticToken token, const GdkRGBA *color, gboolean *palette_changed) +{ + gboolean dark; + gboolean changed = FALSE; + + if (!color) + return; + + dark = theme_policy_is_dark_mode_active (mode); + if (dark) + theme_runtime_dark_set_color (token, color); + else + theme_runtime_user_set_color (token, color); + + changed = theme_runtime_apply_mode (mode, NULL); + if (palette_changed) + *palette_changed = changed; + + theme_application_reload_input_style (); +} + +void +theme_manager_commit_preferences (unsigned int old_mode, gboolean *color_change) +{ + gboolean palette_changed = FALSE; + + theme_application_apply_mode (prefs.hex_gui_dark_mode, &palette_changed); + if (color_change && (prefs.hex_gui_dark_mode != old_mode || palette_changed)) + *color_change = TRUE; + + if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO) + fe_set_auto_dark_mode_state (theme_policy_is_dark_mode_active (ZOITECHAT_DARK_MODE_AUTO)); +} + +void +theme_manager_save_preferences (void) +{ + theme_runtime_save (); +} + +gboolean +theme_changed_event_has_reason (const ThemeChangedEvent *event, ThemeChangedReason reason) +{ + if (!event) + return FALSE; + + return (event->reasons & reason) != 0; +} + +void +theme_manager_apply_and_dispatch (unsigned int mode, ThemeChangedReason reasons, gboolean *palette_changed) +{ + theme_application_apply_mode (mode, palette_changed); + theme_manager_dispatch_changed (reasons); +} + +void +theme_manager_dispatch_changed (ThemeChangedReason reasons) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + ThemeChangedEvent event; + + event.reasons = reasons; + + if (!theme_manager_listeners) + return; + + g_hash_table_iter_init (&iter, theme_manager_listeners); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ThemeListener *listener = value; + + if (listener->callback) + listener->callback (&event, listener->userdata); + } +} + +guint +theme_listener_register (const char *component_id, ThemeChangedCallback callback, gpointer userdata) +{ + ThemeListener *listener; + guint id; + + if (!callback) + return 0; + + if (!theme_manager_listeners) + theme_manager_listeners = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, + theme_listener_free); + + id = theme_manager_next_listener_id++; + if (theme_manager_next_listener_id == 0) + theme_manager_next_listener_id = 1; + + listener = g_new0 (ThemeListener, 1); + listener->id = id; + listener->component_id = g_strdup (component_id ? component_id : "theme.listener"); + listener->callback = callback; + listener->userdata = userdata; + + g_hash_table_insert (theme_manager_listeners, GUINT_TO_POINTER (id), listener); + + return id; +} + +void +theme_listener_unregister (guint listener_id) +{ + if (!theme_manager_listeners || listener_id == 0) + return; + + g_hash_table_remove (theme_manager_listeners, GUINT_TO_POINTER (listener_id)); +} + +void +theme_manager_handle_theme_applied (void) +{ + theme_runtime_load (); + theme_runtime_apply_mode (prefs.hex_gui_dark_mode, NULL); + theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK | THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE); +} + +static void +theme_manager_apply_platform_window_theme (GtkWidget *window) +{ +#ifdef G_OS_WIN32 + GtkStyleContext *context; + gboolean dark; + + if (!window) + return; + + context = gtk_widget_get_style_context (window); + dark = theme_runtime_is_dark_active (); + if (context) + { + gtk_style_context_remove_class (context, "zoitechat-dark"); + gtk_style_context_remove_class (context, "zoitechat-light"); + gtk_style_context_add_class (context, dark ? "zoitechat-dark" : "zoitechat-light"); + } + fe_win32_apply_native_titlebar (window, dark); +#else + (void) window; +#endif +} + +static void +theme_manager_window_destroy_cb (GtkWidget *window, gpointer userdata) +{ + (void) userdata; + g_object_set_data (G_OBJECT (window), theme_manager_window_destroy_handler_key, NULL); +} + +void +theme_manager_apply_to_window (GtkWidget *window) +{ + if (!window) + return; + + theme_manager_apply_platform_window_theme (window); +} + +void +theme_manager_attach_window (GtkWidget *window) +{ + gulong *handler_id; + + if (!window) + return; + + handler_id = g_object_get_data (G_OBJECT (window), theme_manager_window_destroy_handler_key); + if (!handler_id) + { + handler_id = g_new (gulong, 1); + *handler_id = g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (theme_manager_window_destroy_cb), NULL); + g_object_set_data_full (G_OBJECT (window), theme_manager_window_destroy_handler_key, handler_id, g_free); + } + + theme_manager_apply_to_window (window); +} + +void +theme_manager_detach_window (GtkWidget *window) +{ + gulong *handler_id; + + if (!window) + return; + + handler_id = g_object_get_data (G_OBJECT (window), theme_manager_window_destroy_handler_key); + if (handler_id) + { + g_signal_handler_disconnect (G_OBJECT (window), *handler_id); + g_object_set_data (G_OBJECT (window), theme_manager_window_destroy_handler_key, NULL); + } +} + +void +theme_manager_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc) +{ + theme_css_apply_palette_widget (widget, bg, fg, font_desc); +} + +void +theme_manager_apply_entry_palette (GtkWidget *widget, const PangoFontDescription *font_desc) +{ + ThemeWidgetStyleValues style_values; + + if (!widget || !font_desc) + return; + + theme_get_widget_style_values (&style_values); + gtkutil_apply_palette (widget, &style_values.background, &style_values.foreground, font_desc); +} + +ThemePaletteBehavior +theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_desc) +{ + ThemePaletteBehavior behavior; + gboolean dark_mode_active = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode); + + behavior.font_desc = font_desc; + behavior.apply_background = prefs.hex_gui_ulist_style || dark_mode_active; + behavior.apply_foreground = dark_mode_active; + + return behavior; +} + +ThemePaletteBehavior +theme_manager_get_channel_tree_palette_behavior (const PangoFontDescription *font_desc) +{ + ThemePaletteBehavior behavior; + gboolean dark_mode_active = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode); + + behavior.font_desc = font_desc; + behavior.apply_background = dark_mode_active || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_LIGHT; + behavior.apply_foreground = dark_mode_active || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_LIGHT; + + return behavior; +} + +void +theme_manager_apply_userlist_palette (GtkWidget *widget, const PangoFontDescription *font_desc, + gboolean prefer_background, gboolean prefer_foreground) +{ + ThemePaletteBehavior behavior; + + behavior.font_desc = font_desc; + behavior.apply_background = prefer_background; + behavior.apply_foreground = prefer_foreground; + theme_manager_apply_userlist_style (widget, behavior); +} + +void +theme_manager_apply_userlist_style (GtkWidget *widget, ThemePaletteBehavior behavior) +{ + ThemeWidgetStyleValues style_values; + const GdkRGBA *background = NULL; + const GdkRGBA *foreground = NULL; + + if (!widget) + return; + + theme_get_widget_style_values (&style_values); + if (behavior.apply_background) + background = &style_values.background; + if (behavior.apply_foreground) + foreground = &style_values.foreground; + + gtkutil_apply_palette (widget, background, foreground, behavior.font_desc); +} + +void +theme_manager_apply_channel_tree_style (GtkWidget *widget, ThemePaletteBehavior behavior) +{ + theme_manager_apply_userlist_style (widget, behavior); +} + +void +theme_manager_apply_input_style (gboolean enabled, const PangoFontDescription *font_desc) +{ + theme_css_reload_input_style (enabled, font_desc); +} + +void +theme_manager_reload_input_style (void) +{ + theme_application_reload_input_style (); +} + +void +theme_manager_refresh_auto_mode (void) +{ + theme_manager_queue_auto_refresh (NULL, NULL, NULL); +} + +ThemeChangedEvent +theme_manager_on_preferences_changed (const struct zoitechatprefs *old_prefs, + const struct zoitechatprefs *new_prefs, + unsigned int old_mode, + gboolean *color_change) +{ + ThemeChangedEvent event; + gboolean had_color_change = color_change && *color_change; + + theme_manager_commit_preferences (old_mode, color_change); + event.reasons = theme_manager_synthesize_preference_reasons (old_prefs, new_prefs, + had_color_change || (color_change && *color_change)); + + return event; +} + +void +theme_manager_dispatch_setup_apply (const ThemeChangedEvent *event) +{ + if (!event) + return; + + setup_apply_real (event); +} + +void +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; +} diff --git a/src/fe-gtk/theme/theme-manager.h b/src/fe-gtk/theme/theme-manager.h new file mode 100644 index 00000000..0d94b614 --- /dev/null +++ b/src/fe-gtk/theme/theme-manager.h @@ -0,0 +1,74 @@ +#ifndef ZOITECHAT_THEME_MANAGER_H +#define ZOITECHAT_THEME_MANAGER_H + +#include +#include + +#include "theme-palette.h" + +typedef struct _GtkWidget GtkWidget; +struct zoitechatprefs; + +typedef enum +{ + THEME_CHANGED_REASON_NONE = 0, + THEME_CHANGED_REASON_PALETTE = 1 << 0, + THEME_CHANGED_REASON_WIDGET_STYLE = 1 << 1, + THEME_CHANGED_REASON_MODE = 1 << 2, + THEME_CHANGED_REASON_THEME_PACK = 1 << 3, + THEME_CHANGED_REASON_PIXMAP = 1 << 4, + THEME_CHANGED_REASON_USERLIST = 1 << 5, + THEME_CHANGED_REASON_LAYOUT = 1 << 6, + THEME_CHANGED_REASON_IDENTD = 1 << 7 +} ThemeChangedReason; + +typedef struct +{ + ThemeChangedReason reasons; +} ThemeChangedEvent; + +typedef struct +{ + const PangoFontDescription *font_desc; + gboolean apply_background; + gboolean apply_foreground; +} ThemePaletteBehavior; + +typedef void (*ThemeChangedCallback) (const ThemeChangedEvent *event, gpointer userdata); +typedef guint (*ThemeManagerIdleAddFunc) (GSourceFunc function, gpointer data); + +void theme_manager_init (void); +gboolean theme_manager_apply_mode (unsigned int mode, gboolean *palette_changed); +void theme_manager_set_mode (unsigned int mode, gboolean *palette_changed); +void theme_manager_set_token_color (unsigned int mode, ThemeSemanticToken token, const GdkRGBA *color, gboolean *palette_changed); +void theme_manager_commit_preferences (unsigned int old_mode, gboolean *color_change); +void theme_manager_save_preferences (void); +gboolean theme_changed_event_has_reason (const ThemeChangedEvent *event, ThemeChangedReason reason); +void theme_manager_apply_and_dispatch (unsigned int mode, ThemeChangedReason reasons, gboolean *palette_changed); +void theme_manager_dispatch_changed (ThemeChangedReason reasons); +guint theme_listener_register (const char *component_id, ThemeChangedCallback callback, gpointer userdata); +void theme_listener_unregister (guint listener_id); +void theme_manager_handle_theme_applied (void); +void theme_manager_apply_to_window (GtkWidget *window); +void theme_manager_attach_window (GtkWidget *window); +void theme_manager_detach_window (GtkWidget *window); +void theme_manager_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkRGBA *fg, + const PangoFontDescription *font_desc); +void theme_manager_apply_entry_palette (GtkWidget *widget, const PangoFontDescription *font_desc); +ThemePaletteBehavior theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_desc); +ThemePaletteBehavior theme_manager_get_channel_tree_palette_behavior (const PangoFontDescription *font_desc); +void theme_manager_apply_userlist_palette (GtkWidget *widget, const PangoFontDescription *font_desc, + gboolean prefer_background, gboolean prefer_foreground); +void theme_manager_apply_userlist_style (GtkWidget *widget, ThemePaletteBehavior behavior); +void theme_manager_apply_channel_tree_style (GtkWidget *widget, ThemePaletteBehavior behavior); +void theme_manager_apply_input_style (gboolean enabled, const PangoFontDescription *font_desc); +void theme_manager_reload_input_style (void); +void theme_manager_refresh_auto_mode (void); +ThemeChangedEvent theme_manager_on_preferences_changed (const struct zoitechatprefs *old_prefs, + const struct zoitechatprefs *new_prefs, + unsigned int old_mode, + gboolean *color_change); +void theme_manager_dispatch_setup_apply (const ThemeChangedEvent *event); +void theme_manager_set_idle_add_func (ThemeManagerIdleAddFunc idle_add_func); + +#endif diff --git a/src/fe-gtk/theme/theme-palette.c b/src/fe-gtk/theme/theme-palette.c new file mode 100644 index 00000000..c0858c05 --- /dev/null +++ b/src/fe-gtk/theme/theme-palette.c @@ -0,0 +1,286 @@ +#include + +#include "theme-palette.h" + +static const ThemePaletteTokenDef theme_palette_token_defs[] = { + { THEME_TOKEN_MIRC_0, 0, "mirc_0" }, + { THEME_TOKEN_MIRC_1, 1, "mirc_1" }, + { THEME_TOKEN_MIRC_2, 2, "mirc_2" }, + { THEME_TOKEN_MIRC_3, 3, "mirc_3" }, + { THEME_TOKEN_MIRC_4, 4, "mirc_4" }, + { THEME_TOKEN_MIRC_5, 5, "mirc_5" }, + { THEME_TOKEN_MIRC_6, 6, "mirc_6" }, + { THEME_TOKEN_MIRC_7, 7, "mirc_7" }, + { THEME_TOKEN_MIRC_8, 8, "mirc_8" }, + { THEME_TOKEN_MIRC_9, 9, "mirc_9" }, + { THEME_TOKEN_MIRC_10, 10, "mirc_10" }, + { THEME_TOKEN_MIRC_11, 11, "mirc_11" }, + { THEME_TOKEN_MIRC_12, 12, "mirc_12" }, + { THEME_TOKEN_MIRC_13, 13, "mirc_13" }, + { THEME_TOKEN_MIRC_14, 14, "mirc_14" }, + { THEME_TOKEN_MIRC_15, 15, "mirc_15" }, + { THEME_TOKEN_MIRC_16, 16, "mirc_16" }, + { THEME_TOKEN_MIRC_17, 17, "mirc_17" }, + { THEME_TOKEN_MIRC_18, 18, "mirc_18" }, + { THEME_TOKEN_MIRC_19, 19, "mirc_19" }, + { THEME_TOKEN_MIRC_20, 20, "mirc_20" }, + { THEME_TOKEN_MIRC_21, 21, "mirc_21" }, + { THEME_TOKEN_MIRC_22, 22, "mirc_22" }, + { THEME_TOKEN_MIRC_23, 23, "mirc_23" }, + { THEME_TOKEN_MIRC_24, 24, "mirc_24" }, + { THEME_TOKEN_MIRC_25, 25, "mirc_25" }, + { THEME_TOKEN_MIRC_26, 26, "mirc_26" }, + { THEME_TOKEN_MIRC_27, 27, "mirc_27" }, + { THEME_TOKEN_MIRC_28, 28, "mirc_28" }, + { THEME_TOKEN_MIRC_29, 29, "mirc_29" }, + { THEME_TOKEN_MIRC_30, 30, "mirc_30" }, + { THEME_TOKEN_MIRC_31, 31, "mirc_31" }, + { THEME_TOKEN_SELECTION_FOREGROUND, 32, "selection_foreground" }, + { THEME_TOKEN_SELECTION_BACKGROUND, 33, "selection_background" }, + { THEME_TOKEN_TEXT_FOREGROUND, 34, "text_foreground" }, + { THEME_TOKEN_TEXT_BACKGROUND, 35, "text_background" }, + { THEME_TOKEN_MARKER, 36, "marker" }, + { THEME_TOKEN_TAB_NEW_DATA, 37, "tab_new_data" }, + { THEME_TOKEN_TAB_HIGHLIGHT, 38, "tab_highlight" }, + { THEME_TOKEN_TAB_NEW_MESSAGE, 39, "tab_new_message" }, + { THEME_TOKEN_TAB_AWAY, 40, "tab_away" }, + { THEME_TOKEN_SPELL, 41, "spell" }, +}; + +static const ThemePaletteTokenDef * +theme_palette_lookup_token_def (ThemeSemanticToken token) +{ + size_t i; + + for (i = 0; i < G_N_ELEMENTS (theme_palette_token_defs); i++) + { + if (theme_palette_token_defs[i].token == token) + return &theme_palette_token_defs[i]; + } + + return NULL; +} + +static const ThemePaletteTokenDef * +theme_palette_lookup_legacy_def (int legacy_idx) +{ + size_t i; + + for (i = 0; i < G_N_ELEMENTS (theme_palette_token_defs); i++) + { + if (theme_palette_token_defs[i].legacy_index == legacy_idx) + return &theme_palette_token_defs[i]; + } + + return NULL; +} + +size_t +theme_palette_token_count (void) +{ + return THEME_TOKEN_COUNT; +} + +size_t +theme_palette_token_def_count (void) +{ + return G_N_ELEMENTS (theme_palette_token_defs); +} + +const ThemePaletteTokenDef * +theme_palette_token_def_at (size_t index) +{ + if (index >= G_N_ELEMENTS (theme_palette_token_defs)) + return NULL; + + return &theme_palette_token_defs[index]; +} + +const ThemePaletteTokenDef * +theme_palette_token_def_for_token (ThemeSemanticToken token) +{ + if (token < 0 || token >= THEME_TOKEN_COUNT) + return NULL; + + return theme_palette_lookup_token_def (token); +} + +const char * +theme_palette_token_name (ThemeSemanticToken token) +{ + const ThemePaletteTokenDef *def = theme_palette_token_def_for_token (token); + + if (def == NULL) + return NULL; + + return def->name; +} + +gboolean +theme_palette_token_to_legacy_index (ThemeSemanticToken token, int *legacy_idx) +{ + const ThemePaletteTokenDef *def; + + if (legacy_idx == NULL) + return FALSE; + + def = theme_palette_token_def_for_token (token); + if (def == NULL) + return FALSE; + + *legacy_idx = def->legacy_index; + return TRUE; +} + +gboolean +theme_palette_legacy_index_to_token (int legacy_idx, ThemeSemanticToken *token) +{ + const ThemePaletteTokenDef *def; + + if (token == NULL) + return FALSE; + + def = theme_palette_lookup_legacy_def (legacy_idx); + if (def == NULL) + return FALSE; + + *token = def->token; + return TRUE; +} + +gboolean +theme_palette_set_color (ThemePalette *palette, ThemeSemanticToken token, const GdkRGBA *color) +{ + if (palette == NULL || color == NULL) + return FALSE; + if (token < 0 || token >= THEME_TOKEN_COUNT) + return FALSE; + if (theme_palette_token_def_for_token (token) == NULL) + return FALSE; + + palette->colors[token] = *color; + return TRUE; +} + +gboolean +theme_palette_get_color (const ThemePalette *palette, ThemeSemanticToken token, GdkRGBA *color) +{ + if (palette == NULL || color == NULL) + return FALSE; + if (token < 0 || token >= THEME_TOKEN_COUNT) + return FALSE; + if (theme_palette_token_def_for_token (token) == NULL) + return FALSE; + + *color = palette->colors[token]; + return TRUE; +} + +void +theme_palette_from_legacy_colors (ThemePalette *palette, const GdkRGBA *legacy_colors, size_t legacy_len) +{ + size_t i; + + g_return_if_fail (palette != NULL); + g_return_if_fail (legacy_colors != NULL); + + for (i = 0; i < G_N_ELEMENTS (theme_palette_token_defs); i++) + { + int legacy_idx = theme_palette_token_defs[i].legacy_index; + ThemeSemanticToken token = theme_palette_token_defs[i].token; + + g_return_if_fail (legacy_idx >= 0); + g_return_if_fail ((size_t) legacy_idx < legacy_len); + palette->colors[token] = legacy_colors[legacy_idx]; + } +} + +void +theme_palette_to_legacy_colors (const ThemePalette *palette, GdkRGBA *legacy_colors, size_t legacy_len) +{ + size_t i; + + g_return_if_fail (palette != NULL); + g_return_if_fail (legacy_colors != NULL); + + for (i = 0; i < G_N_ELEMENTS (theme_palette_token_defs); i++) + { + int legacy_idx = theme_palette_token_defs[i].legacy_index; + ThemeSemanticToken token = theme_palette_token_defs[i].token; + + g_return_if_fail (legacy_idx >= 0); + g_return_if_fail ((size_t) legacy_idx < legacy_len); + legacy_colors[legacy_idx] = palette->colors[token]; + } +} + +void +theme_palette_to_xtext_colors (const ThemePalette *palette, XTextColor *xtext_colors, size_t xtext_len) +{ + size_t i; + + g_return_if_fail (palette != NULL); + g_return_if_fail (xtext_colors != NULL); + + for (i = 0; i < G_N_ELEMENTS (theme_palette_token_defs); i++) + { + int legacy_idx = theme_palette_token_defs[i].legacy_index; + ThemeSemanticToken token = theme_palette_token_defs[i].token; + + if ((size_t) legacy_idx >= xtext_len) + continue; + xtext_colors[legacy_idx].red = palette->colors[token].red; + xtext_colors[legacy_idx].green = palette->colors[token].green; + xtext_colors[legacy_idx].blue = palette->colors[token].blue; + xtext_colors[legacy_idx].alpha = palette->colors[token].alpha; + } +} + +void +theme_palette_to_widget_style_values (const ThemePalette *palette, ThemeWidgetStyleValues *style_values) +{ + g_return_if_fail (palette != NULL); + g_return_if_fail (style_values != NULL); + + style_values->foreground = palette->colors[THEME_TOKEN_TEXT_FOREGROUND]; + style_values->background = palette->colors[THEME_TOKEN_TEXT_BACKGROUND]; + style_values->selection_foreground = palette->colors[THEME_TOKEN_SELECTION_FOREGROUND]; + style_values->selection_background = palette->colors[THEME_TOKEN_SELECTION_BACKGROUND]; + g_snprintf (style_values->foreground_css, sizeof (style_values->foreground_css), + "rgba(%u,%u,%u,%.3f)", + (guint) CLAMP (style_values->foreground.red * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->foreground.green * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->foreground.blue * 255.0 + 0.5, 0.0, 255.0), + CLAMP (style_values->foreground.alpha, 0.0, 1.0)); + g_snprintf (style_values->background_css, sizeof (style_values->background_css), + "rgba(%u,%u,%u,%.3f)", + (guint) CLAMP (style_values->background.red * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->background.green * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->background.blue * 255.0 + 0.5, 0.0, 255.0), + CLAMP (style_values->background.alpha, 0.0, 1.0)); + g_snprintf (style_values->selection_foreground_css, sizeof (style_values->selection_foreground_css), + "rgba(%u,%u,%u,%.3f)", + (guint) CLAMP (style_values->selection_foreground.red * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->selection_foreground.green * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->selection_foreground.blue * 255.0 + 0.5, 0.0, 255.0), + CLAMP (style_values->selection_foreground.alpha, 0.0, 1.0)); + g_snprintf (style_values->selection_background_css, sizeof (style_values->selection_background_css), + "rgba(%u,%u,%u,%.3f)", + (guint) CLAMP (style_values->selection_background.red * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->selection_background.green * 255.0 + 0.5, 0.0, 255.0), + (guint) CLAMP (style_values->selection_background.blue * 255.0 + 0.5, 0.0, 255.0), + CLAMP (style_values->selection_background.alpha, 0.0, 1.0)); +} + +void +theme_palette_color_get_rgb16 (const GdkRGBA *color, guint16 *red, guint16 *green, guint16 *blue) +{ + g_return_if_fail (color != NULL); + g_return_if_fail (red != NULL); + g_return_if_fail (green != NULL); + g_return_if_fail (blue != NULL); + + *red = (guint16) CLAMP (color->red * 65535.0 + 0.5, 0.0, 65535.0); + *green = (guint16) CLAMP (color->green * 65535.0 + 0.5, 0.0, 65535.0); + *blue = (guint16) CLAMP (color->blue * 65535.0 + 0.5, 0.0, 65535.0); +} diff --git a/src/fe-gtk/theme/theme-palette.h b/src/fe-gtk/theme/theme-palette.h new file mode 100644 index 00000000..86668965 --- /dev/null +++ b/src/fe-gtk/theme/theme-palette.h @@ -0,0 +1,143 @@ +#ifndef ZOITECHAT_THEME_PALETTE_H +#define ZOITECHAT_THEME_PALETTE_H + +#include + +#include + +#include "../xtext-color.h" + +typedef enum +{ + THEME_LEGACY_MIRC_0 = 0, + THEME_LEGACY_MIRC_1, + THEME_LEGACY_MIRC_2, + THEME_LEGACY_MIRC_3, + THEME_LEGACY_MIRC_4, + THEME_LEGACY_MIRC_5, + THEME_LEGACY_MIRC_6, + THEME_LEGACY_MIRC_7, + THEME_LEGACY_MIRC_8, + THEME_LEGACY_MIRC_9, + THEME_LEGACY_MIRC_10, + THEME_LEGACY_MIRC_11, + THEME_LEGACY_MIRC_12, + THEME_LEGACY_MIRC_13, + THEME_LEGACY_MIRC_14, + THEME_LEGACY_MIRC_15, + THEME_LEGACY_MIRC_16, + THEME_LEGACY_MIRC_17, + THEME_LEGACY_MIRC_18, + THEME_LEGACY_MIRC_19, + THEME_LEGACY_MIRC_20, + THEME_LEGACY_MIRC_21, + THEME_LEGACY_MIRC_22, + THEME_LEGACY_MIRC_23, + THEME_LEGACY_MIRC_24, + THEME_LEGACY_MIRC_25, + THEME_LEGACY_MIRC_26, + THEME_LEGACY_MIRC_27, + THEME_LEGACY_MIRC_28, + THEME_LEGACY_MIRC_29, + THEME_LEGACY_MIRC_30, + THEME_LEGACY_MIRC_31, + THEME_LEGACY_SELECTION_FOREGROUND, + THEME_LEGACY_SELECTION_BACKGROUND, + THEME_LEGACY_TEXT_FOREGROUND, + THEME_LEGACY_TEXT_BACKGROUND, + THEME_LEGACY_MARKER, + THEME_LEGACY_TAB_NEW_DATA, + THEME_LEGACY_TAB_HIGHLIGHT, + THEME_LEGACY_TAB_NEW_MESSAGE, + THEME_LEGACY_TAB_AWAY, + THEME_LEGACY_SPELL, + THEME_LEGACY_MAX = THEME_LEGACY_SPELL +} ThemeLegacyColorIndex; + +typedef enum +{ + THEME_TOKEN_MIRC_0 = 0, + THEME_TOKEN_MIRC_1, + THEME_TOKEN_MIRC_2, + THEME_TOKEN_MIRC_3, + THEME_TOKEN_MIRC_4, + THEME_TOKEN_MIRC_5, + THEME_TOKEN_MIRC_6, + THEME_TOKEN_MIRC_7, + THEME_TOKEN_MIRC_8, + THEME_TOKEN_MIRC_9, + THEME_TOKEN_MIRC_10, + THEME_TOKEN_MIRC_11, + THEME_TOKEN_MIRC_12, + THEME_TOKEN_MIRC_13, + THEME_TOKEN_MIRC_14, + THEME_TOKEN_MIRC_15, + THEME_TOKEN_MIRC_16, + THEME_TOKEN_MIRC_17, + THEME_TOKEN_MIRC_18, + THEME_TOKEN_MIRC_19, + THEME_TOKEN_MIRC_20, + THEME_TOKEN_MIRC_21, + THEME_TOKEN_MIRC_22, + THEME_TOKEN_MIRC_23, + THEME_TOKEN_MIRC_24, + THEME_TOKEN_MIRC_25, + THEME_TOKEN_MIRC_26, + THEME_TOKEN_MIRC_27, + THEME_TOKEN_MIRC_28, + THEME_TOKEN_MIRC_29, + THEME_TOKEN_MIRC_30, + THEME_TOKEN_MIRC_31, + THEME_TOKEN_SELECTION_FOREGROUND, + THEME_TOKEN_SELECTION_BACKGROUND, + THEME_TOKEN_TEXT_FOREGROUND, + THEME_TOKEN_TEXT_BACKGROUND, + THEME_TOKEN_MARKER, + THEME_TOKEN_TAB_NEW_DATA, + THEME_TOKEN_TAB_HIGHLIGHT, + THEME_TOKEN_TAB_NEW_MESSAGE, + THEME_TOKEN_TAB_AWAY, + THEME_TOKEN_SPELL, + THEME_TOKEN_COUNT +} ThemeSemanticToken; + +typedef struct +{ + ThemeSemanticToken token; + int legacy_index; + const char *name; +} ThemePaletteTokenDef; + +typedef struct +{ + GdkRGBA colors[THEME_TOKEN_COUNT]; +} ThemePalette; + +typedef struct +{ + GdkRGBA foreground; + GdkRGBA background; + GdkRGBA selection_foreground; + GdkRGBA selection_background; + char foreground_css[32]; + char background_css[32]; + char selection_foreground_css[32]; + char selection_background_css[32]; +} ThemeWidgetStyleValues; + +size_t theme_palette_token_count (void); +size_t theme_palette_token_def_count (void); +const ThemePaletteTokenDef *theme_palette_token_def_at (size_t index); +const ThemePaletteTokenDef *theme_palette_token_def_for_token (ThemeSemanticToken token); +const char *theme_palette_token_name (ThemeSemanticToken token); +gboolean theme_palette_token_to_legacy_index (ThemeSemanticToken token, int *legacy_idx); +gboolean theme_palette_legacy_index_to_token (int legacy_idx, ThemeSemanticToken *token); +gboolean theme_palette_set_color (ThemePalette *palette, ThemeSemanticToken token, const GdkRGBA *color); +gboolean theme_palette_get_color (const ThemePalette *palette, ThemeSemanticToken token, GdkRGBA *color); +void theme_palette_from_legacy_colors (ThemePalette *palette, const GdkRGBA *legacy_colors, size_t legacy_len); +void theme_palette_to_legacy_colors (const ThemePalette *palette, GdkRGBA *legacy_colors, size_t legacy_len); +void theme_palette_to_xtext_colors (const ThemePalette *palette, XTextColor *xtext_colors, size_t xtext_len); +void theme_palette_to_widget_style_values (const ThemePalette *palette, ThemeWidgetStyleValues *style_values); +void theme_palette_color_get_rgb16 (const GdkRGBA *color, guint16 *red, guint16 *green, guint16 *blue); + +#endif diff --git a/src/fe-gtk/theme/theme-policy.c b/src/fe-gtk/theme/theme-policy.c new file mode 100644 index 00000000..14df8bf9 --- /dev/null +++ b/src/fe-gtk/theme/theme-policy.c @@ -0,0 +1,59 @@ +#include "theme-policy.h" + +#include + +#include "../fe-gtk.h" +#include "../../common/zoitechat.h" +#include "../../common/zoitechatc.h" + +gboolean +theme_policy_system_prefers_dark (void) +{ + GtkSettings *settings = gtk_settings_get_default (); + gboolean prefer_dark = FALSE; + char *theme_name = NULL; +#ifdef G_OS_WIN32 + gboolean have_win_pref = FALSE; + + if (fe_win32_high_contrast_is_enabled ()) + return FALSE; + + have_win_pref = fe_win32_try_get_system_dark (&prefer_dark); + if (!have_win_pref) +#endif + if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), + "gtk-application-prefer-dark-theme")) + { + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark, NULL); + } + + if (settings && !prefer_dark) + { + g_object_get (settings, "gtk-theme-name", &theme_name, NULL); + if (theme_name) + { + char *lower = g_ascii_strdown (theme_name, -1); + if (g_str_has_suffix (lower, "-dark") || g_strrstr (lower, "dark")) + prefer_dark = TRUE; + g_free (lower); + g_free (theme_name); + } + } + + return prefer_dark; +} + +gboolean +theme_policy_is_dark_mode_active (unsigned int mode) +{ + if (mode == ZOITECHAT_DARK_MODE_AUTO) + return theme_policy_system_prefers_dark (); + + return fe_dark_mode_is_enabled_for (mode); +} + +gboolean +theme_policy_is_app_dark_mode_active (void) +{ + return theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode); +} diff --git a/src/fe-gtk/theme/theme-policy.h b/src/fe-gtk/theme/theme-policy.h new file mode 100644 index 00000000..8e40653e --- /dev/null +++ b/src/fe-gtk/theme/theme-policy.h @@ -0,0 +1,10 @@ +#ifndef ZOITECHAT_THEME_POLICY_H +#define ZOITECHAT_THEME_POLICY_H + +#include + +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); + +#endif diff --git a/src/fe-gtk/theme/theme-preferences.c b/src/fe-gtk/theme/theme-preferences.c new file mode 100644 index 00000000..3d508505 --- /dev/null +++ b/src/fe-gtk/theme/theme-preferences.c @@ -0,0 +1,580 @@ +#include +#include + +#include +#include + +#include "../gtkutil.h" +#include "../../common/fe.h" +#include "../../common/util.h" +#include "../../common/cfgfiles.h" +#include "../../common/zoitechat.h" +#include "../../common/theme-service.h" +#include "../../common/zoitechatc.h" +#include "theme-css.h" +#include "theme-manager.h" +#include "theme-preferences.h" + +typedef struct +{ + GtkWidget *combo; + GtkWidget *apply_button; + GtkWidget *status_label; + GtkWindow *parent; + gboolean *color_change_flag; +} theme_preferences_ui; + +typedef struct +{ + GtkWidget *button; + ThemeSemanticToken token; + gboolean *color_change_flag; +} theme_color_dialog_data; + +typedef struct +{ + struct zoitechatprefs *setup_prefs; +} theme_preferences_dark_mode_data; + +#define LABEL_INDENT 12 + +static void +theme_preferences_show_message (theme_preferences_ui *ui, GtkMessageType message_type, const char *primary) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new (ui->parent, + GTK_DIALOG_MODAL, + message_type, + GTK_BUTTONS_OK, + "%s", + primary); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static void +theme_preferences_color_button_apply (GtkWidget *button, const GdkRGBA *color) +{ + GtkWidget *target = g_object_get_data (G_OBJECT (button), "zoitechat-color-box"); + GtkWidget *apply_widget = GTK_IS_WIDGET (target) ? target : button; + + gtkutil_apply_palette (apply_widget, color, NULL, NULL); + + if (apply_widget != button) + gtkutil_apply_palette (button, color, NULL, NULL); + + gtk_widget_queue_draw (button); +} + +static void +theme_preferences_color_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data) +{ + theme_color_dialog_data *data = user_data; + + if (response_id == GTK_RESPONSE_OK) + { + GdkRGBA rgba; + gboolean changed = FALSE; + + gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (dialog), &rgba); + theme_manager_set_token_color (prefs.hex_gui_dark_mode, + data->token, + &rgba, + &changed); + if (data->color_change_flag) + *data->color_change_flag = *data->color_change_flag || changed; + theme_preferences_color_button_apply (data->button, &rgba); + } + + gtk_widget_destroy (GTK_WIDGET (dialog)); + g_free (data); +} + +static void +theme_preferences_color_cb (GtkWidget *button, gpointer userdata) +{ + GtkWidget *dialog; + ThemeSemanticToken token; + GdkRGBA rgba; + theme_color_dialog_data *data; + + token = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "zoitechat-theme-token")); + + if (!theme_get_color (token, &rgba)) + return; + dialog = gtk_color_chooser_dialog_new (_("Select color"), GTK_WINDOW (userdata)); + gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (dialog), &rgba); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + + data = g_new0 (theme_color_dialog_data, 1); + data->button = button; + data->token = token; + data->color_change_flag = g_object_get_data (G_OBJECT (button), "zoitechat-theme-color-change"); + g_signal_connect (dialog, "response", G_CALLBACK (theme_preferences_color_response_cb), data); + gtk_widget_show (dialog); +} + +static void +theme_preferences_create_color_button (GtkWidget *table, + ThemeSemanticToken token, + int row, + int col, + GtkWindow *parent, + gboolean *color_change_flag) +{ + GtkWidget *but; + GtkWidget *label; + GtkWidget *box; + char buf[64]; + GdkRGBA color; + + if (token > THEME_TOKEN_MIRC_31) + strcpy (buf, "  "); + else if (token < 10) + sprintf (buf, " %d", token); + else + sprintf (buf, "%d", token); + + but = gtk_button_new (); + label = gtk_label_new (" "); + gtk_label_set_markup (GTK_LABEL (label), buf); + box = gtk_event_box_new (); + gtk_event_box_set_visible_window (GTK_EVENT_BOX (box), TRUE); + gtk_container_add (GTK_CONTAINER (box), label); + gtk_container_add (GTK_CONTAINER (but), box); + gtk_widget_set_halign (box, GTK_ALIGN_CENTER); + gtk_widget_set_valign (box, GTK_ALIGN_CENTER); + gtk_widget_show (label); + gtk_widget_show (box); + g_object_set_data (G_OBJECT (but), "zoitechat-color", (gpointer)1); + g_object_set_data (G_OBJECT (but), "zoitechat-color-box", box); + g_object_set_data (G_OBJECT (but), "zoitechat-theme-token", GINT_TO_POINTER (token)); + g_object_set_data (G_OBJECT (but), "zoitechat-theme-color-change", color_change_flag); + gtk_grid_attach (GTK_GRID (table), but, col, row, 1, 1); + g_signal_connect (G_OBJECT (but), "clicked", G_CALLBACK (theme_preferences_color_cb), parent); + if (theme_get_color (token, &color)) + theme_preferences_color_button_apply (but, &color); +} + +static void +theme_preferences_create_header (GtkWidget *table, int row, const char *labeltext) +{ + GtkWidget *label; + char buf[128]; + + if (row == 0) + g_snprintf (buf, sizeof (buf), "%s", _(labeltext)); + else + g_snprintf (buf, sizeof (buf), "\n%s", _(labeltext)); + + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), buf); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_grid_attach (GTK_GRID (table), label, 0, row, 4, 1); + gtk_widget_set_margin_bottom (label, 5); +} + +static void +theme_preferences_create_other_color_l (GtkWidget *tab, + const char *text, + ThemeSemanticToken token, + int row, + GtkWindow *parent, + gboolean *color_change_flag) +{ + GtkWidget *label; + + label = gtk_label_new (text); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start (label, LABEL_INDENT); + gtk_grid_attach (GTK_GRID (tab), label, 2, row, 1, 1); + theme_preferences_create_color_button (tab, token, row, 3, parent, color_change_flag); +} + +static void +theme_preferences_create_other_color_r (GtkWidget *tab, + const char *text, + ThemeSemanticToken token, + int row, + GtkWindow *parent, + gboolean *color_change_flag) +{ + GtkWidget *label; + + label = gtk_label_new (text); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start (label, LABEL_INDENT); + gtk_grid_attach (GTK_GRID (tab), label, 5, row, 4, 1); + theme_preferences_create_color_button (tab, token, row, 9, parent, color_change_flag); +} + +static void +theme_preferences_strip_toggle_cb (GtkToggleButton *toggle, gpointer user_data) +{ + int *field = user_data; + + *field = gtk_toggle_button_get_active (toggle); +} + +static void +theme_preferences_create_strip_toggle (GtkWidget *tab, + int row, + const char *text, + int *field) +{ + GtkWidget *toggle; + + toggle = gtk_check_button_new_with_label (text); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), *field); + g_signal_connect (G_OBJECT (toggle), "toggled", + G_CALLBACK (theme_preferences_strip_toggle_cb), field); + gtk_grid_attach (GTK_GRID (tab), toggle, 2, row, 1, 1); +} + +static void +theme_preferences_dark_mode_changed_cb (GtkComboBox *combo, gpointer user_data) +{ + theme_preferences_dark_mode_data *data = user_data; + + data->setup_prefs->hex_gui_dark_mode = gtk_combo_box_get_active (combo); +} + +static void +theme_preferences_create_dark_mode_menu (GtkWidget *tab, + int row, + struct zoitechatprefs *setup_prefs) +{ + static const char *const dark_mode_modes[] = + { + N_("Auto (system)"), + N_("Dark"), + N_("Light"), + NULL + }; + GtkWidget *label; + GtkWidget *combo; + GtkWidget *box; + theme_preferences_dark_mode_data *data; + int i; + + label = gtk_label_new (_("Dark mode:")); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start (label, LABEL_INDENT); + gtk_widget_set_tooltip_text (label, + _("Choose how ZoiteChat selects its color palette for the chat buffer, channel list, and user list.\n" + "This includes message colors, selection colors, and interface highlights.\n")); + gtk_grid_attach (GTK_GRID (tab), label, 2, row, 1, 1); + + combo = gtk_combo_box_text_new (); + for (i = 0; dark_mode_modes[i] != NULL; i++) + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), _(dark_mode_modes[i])); + gtk_combo_box_set_active (GTK_COMBO_BOX (combo), setup_prefs->hex_gui_dark_mode); + gtk_widget_set_tooltip_text (combo, + _("Choose how ZoiteChat selects its color palette for the chat buffer, channel list, and user list.\n" + "This includes message colors, selection colors, and interface highlights.\n")); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (box), combo, FALSE, FALSE, 0); + gtk_grid_attach (GTK_GRID (tab), box, 3, row, 1, 1); + + data = g_new0 (theme_preferences_dark_mode_data, 1); + data->setup_prefs = setup_prefs; + g_signal_connect (G_OBJECT (combo), "changed", + G_CALLBACK (theme_preferences_dark_mode_changed_cb), data); + g_object_set_data_full (G_OBJECT (combo), "zoitechat-dark-mode-data", data, g_free); +} + +GtkWidget * +theme_preferences_create_color_page (GtkWindow *parent, + struct zoitechatprefs *setup_prefs, + gboolean *color_change_flag) +{ + GtkWidget *tab; + GtkWidget *box; + GtkWidget *label; + int i; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_container_set_border_width (GTK_CONTAINER (box), 6); + + tab = gtk_grid_new (); + gtk_container_set_border_width (GTK_CONTAINER (tab), 6); + gtk_grid_set_row_spacing (GTK_GRID (tab), 2); + gtk_grid_set_column_spacing (GTK_GRID (tab), 3); + gtk_container_add (GTK_CONTAINER (box), tab); + + theme_preferences_create_header (tab, 0, N_("Text Colors")); + + label = gtk_label_new (_("mIRC colors:")); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start (label, LABEL_INDENT); + gtk_grid_attach (GTK_GRID (tab), label, 2, 1, 1, 1); + + for (i = 0; i < 16; i++) + theme_preferences_create_color_button (tab, + THEME_TOKEN_MIRC_0 + i, + 1, + i + 3, + parent, + color_change_flag); + + label = gtk_label_new (_("Local colors:")); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start (label, LABEL_INDENT); + gtk_grid_attach (GTK_GRID (tab), label, 2, 2, 1, 1); + + for (i = 16; i < 32; i++) + theme_preferences_create_color_button (tab, + THEME_TOKEN_MIRC_0 + i, + 2, + (i + 3) - 16, + parent, + color_change_flag); + + theme_preferences_create_other_color_l (tab, _("Foreground:"), THEME_TOKEN_TEXT_FOREGROUND, 3, + parent, color_change_flag); + theme_preferences_create_other_color_r (tab, _("Background:"), THEME_TOKEN_TEXT_BACKGROUND, 3, + parent, color_change_flag); + + theme_preferences_create_header (tab, 5, N_("Selected Text")); + theme_preferences_create_other_color_l (tab, _("Foreground:"), THEME_TOKEN_SELECTION_FOREGROUND, 6, + parent, color_change_flag); + theme_preferences_create_other_color_r (tab, _("Background:"), THEME_TOKEN_SELECTION_BACKGROUND, 6, + parent, color_change_flag); + + theme_preferences_create_header (tab, 8, N_("Interface Colors")); + theme_preferences_create_other_color_l (tab, _("New data:"), THEME_TOKEN_TAB_NEW_DATA, 9, + parent, color_change_flag); + theme_preferences_create_other_color_r (tab, _("Marker line:"), THEME_TOKEN_MARKER, 9, + parent, color_change_flag); + theme_preferences_create_other_color_l (tab, _("New message:"), THEME_TOKEN_TAB_NEW_MESSAGE, 10, + parent, color_change_flag); + theme_preferences_create_other_color_r (tab, _("Away user:"), THEME_TOKEN_TAB_AWAY, 10, + parent, color_change_flag); + theme_preferences_create_other_color_l (tab, _("Highlight:"), THEME_TOKEN_TAB_HIGHLIGHT, 11, + parent, color_change_flag); + theme_preferences_create_other_color_r (tab, _("Spell checker:"), THEME_TOKEN_SPELL, 11, + parent, color_change_flag); + theme_preferences_create_dark_mode_menu (tab, 13, setup_prefs); + + theme_preferences_create_header (tab, 15, N_("Color Stripping")); + theme_preferences_create_strip_toggle (tab, 16, _("Messages"), &setup_prefs->hex_text_stripcolor_msg); + theme_preferences_create_strip_toggle (tab, 17, _("Scrollback"), &setup_prefs->hex_text_stripcolor_replay); + theme_preferences_create_strip_toggle (tab, 18, _("Topic"), &setup_prefs->hex_text_stripcolor_topic); + + return box; +} + +static void +theme_preferences_populate (theme_preferences_ui *ui) +{ + GStrv themes; + int count = 0; + guint i; + + gtk_combo_box_text_remove_all (GTK_COMBO_BOX_TEXT (ui->combo)); + + themes = zoitechat_theme_service_discover_themes (); + for (i = 0; themes[i] != NULL; i++) + { + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (ui->combo), themes[i]); + count++; + } + g_strfreev (themes); + + gtk_widget_set_sensitive (ui->apply_button, count > 0); + gtk_label_set_text (GTK_LABEL (ui->status_label), + count > 0 ? _("Select a theme to apply.") : _("No themes found.")); +} + +static void +theme_preferences_refresh_cb (GtkWidget *button, gpointer user_data) +{ + theme_preferences_ui *ui = user_data; + + (void)button; + theme_preferences_populate (ui); +} + +static void +theme_preferences_open_folder_cb (GtkWidget *button, gpointer user_data) +{ + theme_preferences_ui *ui = user_data; + GAppInfo *handler; + char *themes_dir; + + (void)button; + themes_dir = zoitechat_theme_service_get_themes_dir (); + g_mkdir_with_parents (themes_dir, 0700); + + handler = g_app_info_get_default_for_uri_scheme ("file"); + if (!handler) + { + theme_preferences_show_message (ui, + GTK_MESSAGE_ERROR, + _("No application is configured to open folders.")); + g_free (themes_dir); + return; + } + + g_object_unref (handler); + fe_open_url (themes_dir); + g_free (themes_dir); +} + +static void +theme_preferences_selection_changed (GtkComboBox *combo, gpointer user_data) +{ + theme_preferences_ui *ui = user_data; + gboolean has_selection = gtk_combo_box_get_active (combo) >= 0; + + gtk_widget_set_sensitive (ui->apply_button, has_selection); +} + +static void +theme_preferences_apply_cb (GtkWidget *button, gpointer user_data) +{ + theme_preferences_ui *ui = user_data; + GtkWidget *dialog; + gint response; + char *theme; + GError *error = NULL; + + (void)button; + theme = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (ui->combo)); + if (!theme) + return; + + dialog = gtk_message_dialog_new (ui->parent, GTK_DIALOG_MODAL, + GTK_MESSAGE_WARNING, GTK_BUTTONS_OK_CANCEL, + "%s", _("Applying a theme will overwrite your current colors and event settings.\nContinue?")); + response = gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + + if (response != GTK_RESPONSE_OK) + { + g_free (theme); + return; + } + + if (!zoitechat_apply_theme (theme, &error)) + { + theme_preferences_show_message (ui, GTK_MESSAGE_ERROR, + error ? error->message : _("Failed to apply theme.")); + g_clear_error (&error); + goto cleanup; + } + + if (ui->color_change_flag) + *ui->color_change_flag = TRUE; + + theme_preferences_show_message (ui, + GTK_MESSAGE_INFO, + _("Theme applied. Some changes may require a restart to take full effect.")); + +cleanup: + g_free (theme); +} + +GtkWidget * +theme_preferences_create_page (GtkWindow *parent, gboolean *color_change_flag) +{ + theme_preferences_ui *ui; + GtkWidget *box; + GtkWidget *label; + GtkWidget *hbox; + GtkWidget *button_box; + char *themes_dir; + char *markup; + + ui = g_new0 (theme_preferences_ui, 1); + ui->parent = parent; + ui->color_change_flag = color_change_flag; + + box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (box), 6); + + themes_dir = zoitechat_theme_service_get_themes_dir (); + markup = g_markup_printf_escaped (_("Theme files are loaded from %s."), themes_dir); + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), markup); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); + g_free (markup); + g_free (themes_dir); + + hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6); + gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0); + + ui->combo = gtk_combo_box_text_new (); + gtk_box_pack_start (GTK_BOX (hbox), ui->combo, TRUE, TRUE, 0); + g_signal_connect (G_OBJECT (ui->combo), "changed", + G_CALLBACK (theme_preferences_selection_changed), ui); + + button_box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6); + gtk_box_pack_start (GTK_BOX (hbox), button_box, FALSE, FALSE, 0); + + ui->apply_button = gtk_button_new_with_mnemonic (_("_Apply Theme")); + gtk_box_pack_start (GTK_BOX (button_box), ui->apply_button, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (ui->apply_button), "clicked", + G_CALLBACK (theme_preferences_apply_cb), ui); + + label = gtk_button_new_with_mnemonic (_("_Refresh")); + gtk_box_pack_start (GTK_BOX (button_box), label, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (label), "clicked", + G_CALLBACK (theme_preferences_refresh_cb), ui); + + label = gtk_button_new_with_mnemonic (_("_Open Folder")); + gtk_box_pack_start (GTK_BOX (button_box), label, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (label), "clicked", + G_CALLBACK (theme_preferences_open_folder_cb), ui); + + ui->status_label = gtk_label_new (NULL); + gtk_widget_set_halign (ui->status_label, GTK_ALIGN_START); + gtk_widget_set_valign (ui->status_label, GTK_ALIGN_CENTER); + gtk_box_pack_start (GTK_BOX (box), ui->status_label, FALSE, FALSE, 0); + + theme_preferences_populate (ui); + + g_object_set_data_full (G_OBJECT (box), "theme-preferences-ui", ui, g_free); + + return box; +} + +static void +theme_preferences_apply_entry_style (GtkWidget *entry, InputStyle *input_style) +{ + ThemeWidgetStyleValues style_values; + + theme_get_widget_style_values (&style_values); + gtkutil_apply_palette (entry, &style_values.background, &style_values.foreground, + input_style ? input_style->font_desc : NULL); +} + +void +theme_preferences_apply_to_session (session_gui *gui, InputStyle *input_style) +{ + if (prefs.hex_gui_input_style) + { + theme_css_reload_input_style (TRUE, input_style ? input_style->font_desc : NULL); + theme_preferences_apply_entry_style (gui->input_box, input_style); + theme_preferences_apply_entry_style (gui->limit_entry, input_style); + theme_preferences_apply_entry_style (gui->key_entry, input_style); + theme_preferences_apply_entry_style (gui->topic_entry, input_style); + } + + if (gui->user_tree) + { + theme_manager_apply_userlist_style (gui->user_tree, + theme_manager_get_userlist_palette_behavior (input_style ? input_style->font_desc : NULL)); + } +} diff --git a/src/fe-gtk/theme/theme-preferences.h b/src/fe-gtk/theme/theme-preferences.h new file mode 100644 index 00000000..ee55fbdf --- /dev/null +++ b/src/fe-gtk/theme/theme-preferences.h @@ -0,0 +1,16 @@ +#ifndef ZOITECHAT_THEME_PREFERENCES_H +#define ZOITECHAT_THEME_PREFERENCES_H + +#include + +#include "theme-access.h" +#include "../fe-gtk.h" +#include "../../common/zoitechat.h" + +GtkWidget *theme_preferences_create_page (GtkWindow *parent, gboolean *color_change_flag); +GtkWidget *theme_preferences_create_color_page (GtkWindow *parent, + struct zoitechatprefs *setup_prefs, + gboolean *color_change_flag); +void theme_preferences_apply_to_session (session_gui *gui, InputStyle *input_style); + +#endif diff --git a/src/fe-gtk/theme/theme-runtime.c b/src/fe-gtk/theme/theme-runtime.c new file mode 100644 index 00000000..b23c0b10 --- /dev/null +++ b/src/fe-gtk/theme/theme-runtime.c @@ -0,0 +1,466 @@ +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#else +#include +#endif + +#include "theme-runtime.h" +#include "theme-policy.h" + +#include "../../common/zoitechat.h" +#include "../../common/zoitechatc.h" +#include "../../common/util.h" +#include "../../common/cfgfiles.h" +#include "../../common/typedef.h" + +#define PALETTE_COLOR_INIT(r, g, b) { (r) / 65535.0, (g) / 65535.0, (b) / 65535.0, 1.0 } + +static const GdkRGBA legacy_light_defaults[THEME_LEGACY_MAX + 1] = { + PALETTE_COLOR_INIT (0xd3d3, 0xd7d7, 0xcfcf), + PALETTE_COLOR_INIT (0x2e2e, 0x3434, 0x3636), + PALETTE_COLOR_INIT (0x3434, 0x6565, 0xa4a4), + PALETTE_COLOR_INIT (0x4e4e, 0x9a9a, 0x0606), + PALETTE_COLOR_INIT (0xcccc, 0x0000, 0x0000), + PALETTE_COLOR_INIT (0x8f8f, 0x3939, 0x0202), + PALETTE_COLOR_INIT (0x5c5c, 0x3535, 0x6666), + PALETTE_COLOR_INIT (0xcece, 0x5c5c, 0x0000), + PALETTE_COLOR_INIT (0xc4c4, 0xa0a0, 0x0000), + PALETTE_COLOR_INIT (0x7373, 0xd2d2, 0x1616), + PALETTE_COLOR_INIT (0x1111, 0xa8a8, 0x7979), + PALETTE_COLOR_INIT (0x5858, 0xa1a1, 0x9d9d), + PALETTE_COLOR_INIT (0x5757, 0x7979, 0x9e9e), + PALETTE_COLOR_INIT (0xa0d0, 0x42d4, 0x6562), + PALETTE_COLOR_INIT (0x5555, 0x5757, 0x5353), + PALETTE_COLOR_INIT (0x8888, 0x8a8a, 0x8585), + PALETTE_COLOR_INIT (0xd3d3, 0xd7d7, 0xcfcf), + PALETTE_COLOR_INIT (0x2e2e, 0x3434, 0x3636), + PALETTE_COLOR_INIT (0x3434, 0x6565, 0xa4a4), + PALETTE_COLOR_INIT (0x4e4e, 0x9a9a, 0x0606), + PALETTE_COLOR_INIT (0xcccc, 0x0000, 0x0000), + PALETTE_COLOR_INIT (0x8f8f, 0x3939, 0x0202), + PALETTE_COLOR_INIT (0x5c5c, 0x3535, 0x6666), + PALETTE_COLOR_INIT (0xcece, 0x5c5c, 0x0000), + PALETTE_COLOR_INIT (0xc4c4, 0xa0a0, 0x0000), + PALETTE_COLOR_INIT (0x7373, 0xd2d2, 0x1616), + PALETTE_COLOR_INIT (0x1111, 0xa8a8, 0x7979), + PALETTE_COLOR_INIT (0x5858, 0xa1a1, 0x9d9d), + PALETTE_COLOR_INIT (0x5757, 0x7979, 0x9e9e), + PALETTE_COLOR_INIT (0xa0d0, 0x42d4, 0x6562), + PALETTE_COLOR_INIT (0x5555, 0x5757, 0x5353), + PALETTE_COLOR_INIT (0x8888, 0x8a8a, 0x8585), + PALETTE_COLOR_INIT (0xd3d3, 0xd7d7, 0xcfcf), + PALETTE_COLOR_INIT (0x2020, 0x4a4a, 0x8787), + PALETTE_COLOR_INIT (0x2512, 0x29e8, 0x2b85), + PALETTE_COLOR_INIT (0xfae0, 0xfae0, 0xf8c4), + PALETTE_COLOR_INIT (0x8f8f, 0x3939, 0x0202), + PALETTE_COLOR_INIT (0x3434, 0x6565, 0xa4a4), + PALETTE_COLOR_INIT (0x4e4e, 0x9a9a, 0x0606), + PALETTE_COLOR_INIT (0xcece, 0x5c5c, 0x0000), + PALETTE_COLOR_INIT (0x8888, 0x8a8a, 0x8585), + PALETTE_COLOR_INIT (0xa4a4, 0x0000, 0x0000), +}; + +static const GdkRGBA legacy_dark_defaults[THEME_LEGACY_MAX + 1] = { + PALETTE_COLOR_INIT (0xe5e5, 0xe5e5, 0xe5e5), PALETTE_COLOR_INIT (0x3c3c, 0x3c3c, 0x3c3c), + PALETTE_COLOR_INIT (0x5656, 0x9c9c, 0xd6d6), PALETTE_COLOR_INIT (0x0d0d, 0xbcbc, 0x7979), + PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), PALETTE_COLOR_INIT (0xcece, 0x9191, 0x7878), + PALETTE_COLOR_INIT (0xc5c5, 0x8686, 0xc0c0), PALETTE_COLOR_INIT (0xd7d7, 0xbaba, 0x7d7d), + PALETTE_COLOR_INIT (0xdcdc, 0xdcdc, 0xaaaa), PALETTE_COLOR_INIT (0xb5b5, 0xcece, 0xa8a8), + PALETTE_COLOR_INIT (0x4e4e, 0xc9c9, 0xb0b0), PALETTE_COLOR_INIT (0x9c9c, 0xdcdc, 0xfefe), + PALETTE_COLOR_INIT (0x3737, 0x9494, 0xffff), PALETTE_COLOR_INIT (0xd6d6, 0x7070, 0xd6d6), + PALETTE_COLOR_INIT (0x8080, 0x8080, 0x8080), PALETTE_COLOR_INIT (0xc0c0, 0xc0c0, 0xc0c0), + PALETTE_COLOR_INIT (0xe5e5, 0xe5e5, 0xe5e5), PALETTE_COLOR_INIT (0x3c3c, 0x3c3c, 0x3c3c), + PALETTE_COLOR_INIT (0x5656, 0x9c9c, 0xd6d6), PALETTE_COLOR_INIT (0x0d0d, 0xbcbc, 0x7979), + PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), PALETTE_COLOR_INIT (0xcece, 0x9191, 0x7878), + PALETTE_COLOR_INIT (0xc5c5, 0x8686, 0xc0c0), PALETTE_COLOR_INIT (0xd7d7, 0xbaba, 0x7d7d), + PALETTE_COLOR_INIT (0xdcdc, 0xdcdc, 0xaaaa), PALETTE_COLOR_INIT (0xb5b5, 0xcece, 0xa8a8), + PALETTE_COLOR_INIT (0x4e4e, 0xc9c9, 0xb0b0), PALETTE_COLOR_INIT (0x9c9c, 0xdcdc, 0xfefe), + PALETTE_COLOR_INIT (0x3737, 0x9494, 0xffff), PALETTE_COLOR_INIT (0xd6d6, 0x7070, 0xd6d6), + PALETTE_COLOR_INIT (0x8080, 0x8080, 0x8080), PALETTE_COLOR_INIT (0xc0c0, 0xc0c0, 0xc0c0), + PALETTE_COLOR_INIT (0xffff, 0xffff, 0xffff), PALETTE_COLOR_INIT (0x2626, 0x4f4f, 0x7878), + PALETTE_COLOR_INIT (0xd4d4, 0xd4d4, 0xd4d4), PALETTE_COLOR_INIT (0x1e1e, 0x1e1e, 0x1e1e), + PALETTE_COLOR_INIT (0x4040, 0x4040, 0x4040), PALETTE_COLOR_INIT (0x3737, 0x9494, 0xffff), + PALETTE_COLOR_INIT (0xd7d7, 0xbaba, 0x7d7d), PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), + PALETTE_COLOR_INIT (0x8080, 0x8080, 0x8080), PALETTE_COLOR_INIT (0xf4f4, 0x4747, 0x4747), +}; + +static ThemePalette light_palette; +static ThemePalette dark_palette; +static ThemePalette active_palette; +static gboolean user_colors_valid = FALSE; +static gboolean dark_user_colors_valid = FALSE; +static gboolean dark_mode_active = FALSE; + +#define THEME_PALETTE_MIGRATION_MARKER_KEY "theme.palette.semantic_migrated" +#define THEME_PALETTE_MIGRATION_MARKER_VALUE 1 + +typedef struct +{ + const char *mode_name; + const char *legacy_prefix; + ThemePalette *palette; + gboolean *mode_valid; +} ThemePalettePersistenceMode; + +static void +palette_color_set_rgb16 (GdkRGBA *color, guint16 red, guint16 green, guint16 blue) +{ + char color_string[16]; + GdkRGBA parsed = { 0 }; + gboolean parsed_ok; + + g_snprintf (color_string, sizeof (color_string), "#%04x%04x%04x", red, green, blue); + parsed_ok = gdk_rgba_parse (&parsed, color_string); + if (!parsed_ok) + { + parsed.red = red / 65535.0; + parsed.green = green / 65535.0; + parsed.blue = blue / 65535.0; + parsed.alpha = 1.0; + } + *color = parsed; +} + +static void +palette_init_defaults (void) +{ + theme_palette_from_legacy_colors (&light_palette, legacy_light_defaults, G_N_ELEMENTS (legacy_light_defaults)); + theme_palette_from_legacy_colors (&dark_palette, legacy_dark_defaults, G_N_ELEMENTS (legacy_dark_defaults)); + active_palette = light_palette; + dark_mode_active = FALSE; +} + +static int +palette_legacy_index_to_cfg_key (int legacy_idx) +{ + g_return_val_if_fail (legacy_idx >= 0 && legacy_idx <= THEME_LEGACY_MAX, -1); + if (legacy_idx < 32) + return legacy_idx; + return (legacy_idx - 32) + 256; +} + +static gboolean +palette_read_token_color (char *cfg, const char *mode_name, const ThemePaletteTokenDef *def, GdkRGBA *out_color) +{ + char prefname[256]; + guint16 red; + guint16 green; + guint16 blue; + + g_return_val_if_fail (cfg != NULL, FALSE); + g_return_val_if_fail (mode_name != NULL, FALSE); + g_return_val_if_fail (def != NULL, FALSE); + g_return_val_if_fail (out_color != NULL, FALSE); + + g_snprintf (prefname, sizeof prefname, "theme.mode.%s.token.%s", mode_name, def->name); + if (!cfg_get_color (cfg, prefname, &red, &green, &blue)) + return FALSE; + + palette_color_set_rgb16 (out_color, red, green, blue); + return TRUE; +} + +static gboolean +palette_read_legacy_color (char *cfg, const char *legacy_prefix, int legacy_index, GdkRGBA *out_color) +{ + char prefname[256]; + guint16 red; + guint16 green; + guint16 blue; + int legacy_key; + + g_return_val_if_fail (cfg != NULL, FALSE); + g_return_val_if_fail (legacy_prefix != NULL, FALSE); + g_return_val_if_fail (out_color != NULL, FALSE); + + legacy_key = palette_legacy_index_to_cfg_key (legacy_index); + g_return_val_if_fail (legacy_key >= 0, FALSE); + + g_snprintf (prefname, sizeof prefname, "%s_%d", legacy_prefix, legacy_key); + if (!cfg_get_color (cfg, prefname, &red, &green, &blue)) + return FALSE; + + palette_color_set_rgb16 (out_color, red, green, blue); + return TRUE; +} + +static gboolean +theme_runtime_load_migrated_legacy_color (char *cfg, + const ThemePalettePersistenceMode *mode, + const ThemePaletteTokenDef *def, + GdkRGBA *out_color) +{ + g_return_val_if_fail (cfg != NULL, FALSE); + g_return_val_if_fail (mode != NULL, FALSE); + g_return_val_if_fail (def != NULL, FALSE); + g_return_val_if_fail (out_color != NULL, FALSE); + + return palette_read_legacy_color (cfg, mode->legacy_prefix, def->legacy_index, out_color); +} + +static void +palette_write_token_color (int fh, const char *mode_name, const ThemePaletteTokenDef *def, const GdkRGBA *color) +{ + char prefname[256]; + guint16 red; + guint16 green; + guint16 blue; + + g_return_if_fail (mode_name != NULL); + g_return_if_fail (def != NULL); + g_return_if_fail (color != NULL); + + g_snprintf (prefname, sizeof prefname, "theme.mode.%s.token.%s", mode_name, def->name); + theme_palette_color_get_rgb16 (color, &red, &green, &blue); + cfg_put_color (fh, red, green, blue, prefname); +} + + + +gboolean +theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba) +{ + g_return_val_if_fail (out_rgba != NULL, FALSE); + return theme_palette_get_color (&active_palette, token, out_rgba); +} + +void +theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values) +{ + g_return_if_fail (out_values != NULL); + theme_palette_to_widget_style_values (&active_palette, out_values); +} + +void +theme_runtime_get_xtext_colors (XTextColor *palette, size_t palette_len) +{ + g_return_if_fail (palette != NULL); + theme_palette_to_xtext_colors (&active_palette, palette, palette_len); +} + +void +theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col) +{ + if (!col) + return; + if (token < 0 || token >= THEME_TOKEN_COUNT) + return; + if (!user_colors_valid) + light_palette = active_palette; + + g_assert (theme_palette_set_color (&light_palette, token, col)); + user_colors_valid = TRUE; +} + +void +theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col) +{ + if (!col) + return; + if (token < 0 || token >= THEME_TOKEN_COUNT) + return; + if (!dark_user_colors_valid) + dark_palette = active_palette; + + g_assert (theme_palette_set_color (&dark_palette, token, col)); + dark_user_colors_valid = TRUE; +} + +void +theme_runtime_load (void) +{ + size_t i; + int fh; + struct stat st; + char *cfg; + ThemePalettePersistenceMode modes[] = { + { "light", "color", &light_palette, &user_colors_valid }, + { "dark", "dark_color", &dark_palette, &dark_user_colors_valid }, + }; + const size_t mode_count = G_N_ELEMENTS (modes); + + palette_init_defaults (); + + fh = zoitechat_open_file ("colors.conf", O_RDONLY, 0, 0); + if (fh != -1) + { + fstat (fh, &st); + cfg = g_malloc0 (st.st_size + 1); + read (fh, cfg, st.st_size); + for (i = 0; i < mode_count; i++) + { + size_t j; + gboolean mode_found = FALSE; + + for (j = 0; j < theme_palette_token_def_count (); j++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (j); + GdkRGBA color; + gboolean found; + + g_assert (def != NULL); + g_assert (theme_palette_get_color (modes[i].palette, def->token, &color)); + found = palette_read_token_color (cfg, modes[i].mode_name, def, &color); + if (!found) + found = theme_runtime_load_migrated_legacy_color (cfg, &modes[i], def, &color); + if (found) + { + g_assert (theme_palette_set_color (modes[i].palette, def->token, &color)); + mode_found = TRUE; + } + } + + *modes[i].mode_valid = mode_found; + } + + g_free (cfg); + close (fh); + } + + active_palette = light_palette; + dark_mode_active = FALSE; + user_colors_valid = TRUE; +} + +void +theme_runtime_save (void) +{ + size_t i; + size_t j; + int fh; + ThemePalettePersistenceMode modes[] = { + { "light", "color", &light_palette, &user_colors_valid }, + { "dark", "dark_color", &dark_palette, &dark_user_colors_valid }, + }; + const size_t mode_count = G_N_ELEMENTS (modes); + + if (dark_mode_active && !user_colors_valid) + light_palette = active_palette; + + if (!dark_mode_active) + light_palette = active_palette; + + if (dark_mode_active) + { + if (!dark_user_colors_valid) + dark_palette = active_palette; + dark_user_colors_valid = TRUE; + } + + user_colors_valid = TRUE; + modes[0].palette = &light_palette; + modes[0].mode_valid = &user_colors_valid; + + if (dark_user_colors_valid) + modes[1].palette = &dark_palette; + else if (dark_mode_active) + modes[1].palette = &active_palette; + else + modes[1].palette = &dark_palette; + + fh = zoitechat_open_file ("colors.conf", O_TRUNC | O_WRONLY | O_CREAT, 0600, XOF_DOMODE); + if (fh == -1) + return; + + cfg_put_int (fh, THEME_PALETTE_MIGRATION_MARKER_VALUE, (char *) THEME_PALETTE_MIGRATION_MARKER_KEY); + + for (i = 0; i < mode_count; i++) + { + if (!*modes[i].mode_valid) + continue; + + for (j = 0; j < theme_palette_token_def_count (); j++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (j); + GdkRGBA color; + + g_assert (def != NULL); + g_assert (theme_palette_get_color (modes[i].palette, def->token, &color)); + palette_write_token_color (fh, modes[i].mode_name, def, &color); + } + } + + close (fh); +} + +static gboolean +palette_color_eq (const GdkRGBA *a, const GdkRGBA *b) +{ + guint16 red_a; + guint16 green_a; + guint16 blue_a; + guint16 red_b; + guint16 green_b; + guint16 blue_b; + + theme_palette_color_get_rgb16 (a, &red_a, &green_a, &blue_a); + theme_palette_color_get_rgb16 (b, &red_b, &green_b, &blue_b); + + return red_a == red_b && green_a == green_b && blue_a == blue_b; +} + +gboolean +theme_runtime_apply_dark_mode (gboolean enable) +{ + ThemePalette previous_palette; + size_t i; + gboolean changed = FALSE; + + previous_palette = active_palette; + + if (!user_colors_valid) + { + light_palette = active_palette; + user_colors_valid = TRUE; + } + + if (enable) + active_palette = dark_palette; + else + active_palette = light_palette; + + dark_mode_active = enable ? TRUE : FALSE; + + for (i = 0; i < theme_palette_token_def_count (); i++) + { + const ThemePaletteTokenDef *def = theme_palette_token_def_at (i); + GdkRGBA old_color; + GdkRGBA new_color; + + g_assert (def != NULL); + g_assert (theme_palette_get_color (&previous_palette, def->token, &old_color)); + g_assert (theme_palette_get_color (&active_palette, def->token, &new_color)); + if (!palette_color_eq (&old_color, &new_color)) + { + changed = TRUE; + break; + } + } + + return changed; +} + +gboolean +theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed) +{ + gboolean dark = theme_policy_is_dark_mode_active (mode); + gboolean changed = theme_runtime_apply_dark_mode (dark); + + if (palette_changed) + *palette_changed = changed; + + return dark; +} + +gboolean +theme_runtime_is_dark_active (void) +{ + return dark_mode_active; +} diff --git a/src/fe-gtk/theme/theme-runtime.h b/src/fe-gtk/theme/theme-runtime.h new file mode 100644 index 00000000..332ab3a4 --- /dev/null +++ b/src/fe-gtk/theme/theme-runtime.h @@ -0,0 +1,21 @@ +#ifndef ZOITECHAT_THEME_RUNTIME_H +#define ZOITECHAT_THEME_RUNTIME_H + +#include + +#include + +#include "theme-palette.h" + +void theme_runtime_load (void); +void theme_runtime_save (void); +gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed); +gboolean theme_runtime_apply_dark_mode (gboolean enable); +void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col); +void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col); +gboolean theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba); +void theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values); +void theme_runtime_get_xtext_colors (XTextColor *palette, size_t palette_len); +gboolean theme_runtime_is_dark_active (void); + +#endif diff --git a/src/fe-gtk/userlistgui.c b/src/fe-gtk/userlistgui.c index 06198d4e..d49512c0 100644 --- a/src/fe-gtk/userlistgui.c +++ b/src/fe-gtk/userlistgui.c @@ -33,10 +33,11 @@ #include "../common/zoitechatc.h" #include "../common/fe.h" #include "gtkutil.h" -#include "palette.h" +#include "theme/theme-gtk.h" #include "maingui.h" #include "menu.h" #include "pixmaps.h" +#include "theme/theme-access.h" #include "userlistgui.h" #include "fkeys.h" @@ -46,10 +47,10 @@ enum COL_NICK=1, /* char * */ COL_HOST=2, /* char * */ COL_USER=3, /* struct User * */ - COL_GDKCOLOR=4 /* PaletteColor */ + COL_GDKCOLOR=4 /* GdkRGBA */ }; -static void userlist_store_color (GtkListStore *store, GtkTreeIter *iter, int color_index); +static void userlist_store_color (GtkListStore *store, GtkTreeIter *iter, ThemeSemanticToken token, gboolean has_token); GdkPixbuf * get_user_icon (server *serv, struct User *user) @@ -130,6 +131,81 @@ scroll_to_iter (GtkTreeIter *iter, GtkTreeView *treeview, GtkTreeModel *model) } } +static GHashTable * +userlist_row_map_ensure (session *sess) +{ + if (!sess->res->user_row_refs) + sess->res->user_row_refs = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) gtk_tree_row_reference_free); + + return sess->res->user_row_refs; +} + +static void +userlist_row_map_remove (session *sess, struct User *user) +{ + if (!sess->res->user_row_refs) + return; + + g_hash_table_remove (sess->res->user_row_refs, user); +} + +static void +userlist_row_map_set (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter) +{ + GtkTreePath *path; + GtkTreeRowReference *ref; + + path = gtk_tree_model_get_path (model, iter); + if (!path) + return; + + ref = gtk_tree_row_reference_new (model, path); + gtk_tree_path_free (path); + if (!ref) + return; + + g_hash_table_replace (userlist_row_map_ensure (sess), user, ref); +} + +static gboolean +userlist_row_map_get_iter (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter) +{ + GtkTreeRowReference *ref; + GtkTreePath *path; + struct User *row_user; + + if (!sess->res->user_row_refs) + return FALSE; + + ref = g_hash_table_lookup (sess->res->user_row_refs, user); + if (!ref) + return FALSE; + + path = gtk_tree_row_reference_get_path (ref); + if (!path) + { + g_hash_table_remove (sess->res->user_row_refs, user); + return FALSE; + } + + if (!gtk_tree_model_get_iter (model, iter, path)) + { + gtk_tree_path_free (path); + g_hash_table_remove (sess->res->user_row_refs, user); + return FALSE; + } + gtk_tree_path_free (path); + + gtk_tree_model_get (model, iter, COL_USER, &row_user, -1); + if (row_user != user) + { + g_hash_table_remove (sess->res->user_row_refs, user); + return FALSE; + } + + return TRUE; +} + /* select a row in the userlist by nick-name */ void @@ -139,8 +215,20 @@ userlist_select (session *sess, char *name) GtkTreeView *treeview = GTK_TREE_VIEW (sess->gui->user_tree); GtkTreeModel *model = gtk_tree_view_get_model (treeview); GtkTreeSelection *selection = gtk_tree_view_get_selection (treeview); + struct User *user = userlist_find (sess, name); struct User *row_user; + if (user && userlist_row_map_get_iter (sess, model, user, &iter)) + { + if (gtk_tree_selection_iter_is_selected (selection, &iter)) + gtk_tree_selection_unselect_iter (selection, &iter); + else + gtk_tree_selection_select_iter (selection, &iter); + + scroll_to_iter (&iter, treeview, model); + return; + } + if (gtk_tree_model_get_iter_first (model, &iter)) { do @@ -148,6 +236,7 @@ userlist_select (session *sess, char *name) gtk_tree_model_get (model, &iter, COL_USER, &row_user, -1); if (sess->server->p_cmp (row_user->nick, name) == 0) { + userlist_row_map_set (sess, model, row_user, &iter); if (gtk_tree_selection_iter_is_selected (selection, &iter)) gtk_tree_selection_unselect_iter (selection, &iter); else @@ -237,13 +326,23 @@ fe_userlist_set_selected (struct session *sess) } static GtkTreeIter * -find_row (GtkTreeView *treeview, GtkTreeModel *model, struct User *user, +find_row (session *sess, GtkTreeView *treeview, GtkTreeModel *model, struct User *user, int *selected) { static GtkTreeIter iter; struct User *row_user; *selected = FALSE; + if (userlist_row_map_get_iter (sess, model, user, &iter)) + { + if (gtk_tree_view_get_model (treeview) == model) + { + if (gtk_tree_selection_iter_is_selected (gtk_tree_view_get_selection (treeview), &iter)) + *selected = TRUE; + } + return &iter; + } + if (gtk_tree_model_get_iter_first (model, &iter)) { do @@ -251,6 +350,7 @@ find_row (GtkTreeView *treeview, GtkTreeModel *model, struct User *user, gtk_tree_model_get (model, &iter, COL_USER, &row_user, -1); if (row_user == user) { + userlist_row_map_set (sess, model, row_user, &iter); if (gtk_tree_view_get_model (treeview) == model) { if (gtk_tree_selection_iter_is_selected (gtk_tree_view_get_selection (treeview), &iter)) @@ -286,10 +386,11 @@ fe_userlist_remove (session *sess, struct User *user) gfloat val, end;*/ int sel; - iter = find_row (GTK_TREE_VIEW (sess->gui->user_tree), - GTK_TREE_MODEL(sess->res->user_model), user, &sel); + iter = find_row (sess, GTK_TREE_VIEW (sess->gui->user_tree), + GTK_TREE_MODEL(sess->res->user_model), user, &sel); if (!iter) return 0; + userlist_row_map_remove (sess, user); /* adj = gtk_tree_view_get_vadjustment (GTK_TREE_VIEW (sess->gui->user_tree)); val = adj->value;*/ @@ -314,22 +415,35 @@ fe_userlist_rehash (session *sess, struct User *user) { GtkTreeIter *iter; int sel; - int nick_color = 0; + ThemeSemanticToken nick_token = THEME_TOKEN_TEXT_FOREGROUND; + gboolean have_nick_token = FALSE; - iter = find_row (GTK_TREE_VIEW (sess->gui->user_tree), - GTK_TREE_MODEL(sess->res->user_model), user, &sel); + iter = find_row (sess, GTK_TREE_VIEW (sess->gui->user_tree), + GTK_TREE_MODEL(sess->res->user_model), user, &sel); if (!iter) return; + userlist_row_map_set (sess, GTK_TREE_MODEL (sess->res->user_model), user, iter); if (prefs.hex_away_track && user->away) - nick_color = COL_AWAY; + { + nick_token = THEME_TOKEN_TAB_AWAY; + have_nick_token = TRUE; + } else if (prefs.hex_gui_ulist_color) - nick_color = text_color_of(user->nick); + { + int mirc_index = text_color_of (user->nick); + + if (mirc_index >= 0 && mirc_index < 32) + { + nick_token = (ThemeSemanticToken) (THEME_TOKEN_MIRC_0 + mirc_index); + have_nick_token = TRUE; + } + } gtk_list_store_set (GTK_LIST_STORE (sess->res->user_model), iter, COL_HOST, user->hostname, -1); - userlist_store_color (GTK_LIST_STORE (sess->res->user_model), iter, nick_color); + userlist_store_color (GTK_LIST_STORE (sess->res->user_model), iter, nick_token, have_nick_token); } void @@ -339,12 +453,24 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel) GdkPixbuf *pix = get_user_icon (sess->server, newuser); GtkTreeIter iter; char *nick; - int nick_color = 0; + ThemeSemanticToken nick_token = THEME_TOKEN_TEXT_FOREGROUND; + gboolean have_nick_token = FALSE; if (prefs.hex_away_track && newuser->away) - nick_color = COL_AWAY; + { + nick_token = THEME_TOKEN_TAB_AWAY; + have_nick_token = TRUE; + } else if (prefs.hex_gui_ulist_color) - nick_color = text_color_of(newuser->nick); + { + int mirc_index = text_color_of (newuser->nick); + + if (mirc_index >= 0 && mirc_index < 32) + { + nick_token = (ThemeSemanticToken) (THEME_TOKEN_MIRC_0 + mirc_index); + have_nick_token = TRUE; + } + } nick = newuser->nick; if (!prefs.hex_gui_ulist_icons) @@ -364,13 +490,15 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel) COL_HOST, newuser->hostname, COL_USER, newuser, -1); - userlist_store_color (GTK_LIST_STORE (model), &iter, nick_color); + userlist_store_color (GTK_LIST_STORE (model), &iter, nick_token, have_nick_token); if (!prefs.hex_gui_ulist_icons) { g_free (nick); } + userlist_row_map_set (sess, model, newuser, &iter); + /* is it me? */ if (newuser->me && sess->gui->nick_box) { @@ -391,6 +519,8 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel) void fe_userlist_clear (session *sess) { + if (sess->res->user_row_refs) + g_hash_table_remove_all (sess->res->user_row_refs); gtk_list_store_clear (sess->res->user_model); } @@ -469,14 +599,17 @@ userlist_ops_cmp (GtkTreeModel *model, GtkTreeIter *iter_a, GtkTreeIter *iter_b, } static void -userlist_store_color (GtkListStore *store, GtkTreeIter *iter, int color_index) +userlist_store_color (GtkListStore *store, GtkTreeIter *iter, ThemeSemanticToken token, gboolean has_token) { - const PaletteColor *color = color_index ? &colors[color_index] : NULL; + GdkRGBA rgba; + const GdkRGBA *color = NULL; + + if (has_token && theme_get_color (token, &rgba)) + color = &rgba; if (color) { - GdkRGBA rgba = *color; - gtk_list_store_set (store, iter, COL_GDKCOLOR, &rgba, -1); + gtk_list_store_set (store, iter, COL_GDKCOLOR, color, -1); } else { @@ -492,7 +625,7 @@ userlist_create_model (session *sess) GtkSortType sort_type; store = gtk_list_store_new (5, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, - G_TYPE_POINTER, PALETTE_GDK_TYPE); + G_TYPE_POINTER, THEME_GTK_COLOR_TYPE); switch (prefs.hex_gui_ulist_sort) { @@ -545,7 +678,7 @@ userlist_add_columns (GtkTreeView * treeview) gtk_cell_renderer_text_set_fixed_height_from_font (GTK_CELL_RENDERER_TEXT (renderer), 1); gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview), -1, NULL, renderer, - "text", 1, PALETTE_FOREGROUND_PROPERTY, 4, NULL); + "text", 1, THEME_GTK_FOREGROUND_PROPERTY, 4, NULL); if (prefs.hex_gui_ulist_show_hosts) { @@ -734,38 +867,48 @@ userlist_show (session *sess) void fe_uselect (session *sess, char *word[], int do_clear, int scroll_to) { - int thisname; char *name; + int thisname; GtkTreeIter iter; GtkTreeView *treeview = GTK_TREE_VIEW (sess->gui->user_tree); GtkTreeModel *model = gtk_tree_view_get_model (treeview); GtkTreeSelection *selection = gtk_tree_view_get_selection (treeview); + struct User *user; struct User *row_user; - if (gtk_tree_model_get_iter_first (model, &iter)) - { - if (do_clear) - gtk_tree_selection_unselect_all (selection); + if (do_clear) + gtk_tree_selection_unselect_all (selection); - do + thisname = 0; + while (*(name = word[thisname++])) + { + user = userlist_find (sess, name); + if (!user) + continue; + + if (userlist_row_map_get_iter (sess, model, user, &iter)) { - if (*word[0]) + gtk_tree_selection_select_iter (selection, &iter); + if (scroll_to) + scroll_to_iter (&iter, treeview, model); + continue; + } + + if (gtk_tree_model_get_iter_first (model, &iter)) + { + do { gtk_tree_model_get (model, &iter, COL_USER, &row_user, -1); - thisname = 0; - while ( *(name = word[thisname++]) ) + if (row_user == user) { - if (sess->server->p_cmp (row_user->nick, name) == 0) - { - gtk_tree_selection_select_iter (selection, &iter); - if (scroll_to) - scroll_to_iter (&iter, treeview, model); - break; - } + userlist_row_map_set (sess, model, row_user, &iter); + gtk_tree_selection_select_iter (selection, &iter); + if (scroll_to) + scroll_to_iter (&iter, treeview, model); + break; } } - + while (gtk_tree_model_iter_next (model, &iter)); } - while (gtk_tree_model_iter_next (model, &iter)); } } diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 6b64e999..ebf634ac 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -86,9 +86,10 @@ struct textentry gint16 left_len; GSList *slp; GSList *sublines; + int subline_count; guchar tag; guchar pad1; - guchar pad2; /* 32-bit align : 44 bytes total */ + guchar pad2; GList *marks; /* List of found strings */ }; @@ -703,7 +704,7 @@ gtk_xtext_adjustment_set (xtext_buffer *buf, int fire_signal) if (fire_signal) { - gtk_adjustment_value_changed (adj); + g_signal_emit_by_name (adj, "value-changed"); } } } @@ -1327,7 +1328,7 @@ gtk_xtext_draw_marker (GtkXText * xtext, textentry * ent, int y) } else if (xtext->buffer->marker_pos == ent->next && ent->next != NULL) { - render_y = y + xtext->font->descent + xtext->fontsize * g_slist_length (ent->sublines); + render_y = y + xtext->font->descent + xtext->fontsize * ent->subline_count; } else return; @@ -1800,7 +1801,7 @@ gtk_xtext_scrolldown_timeout (GtkXText * xtext) xtext->select_start_y -= xtext->fontsize; xtext->select_start_adj++; xtext_adj_set_value (adj, xtext_adj_get_value (adj) + 1); - gtk_adjustment_value_changed (adj); + g_signal_emit_by_name (adj, "value-changed"); gtk_xtext_selection_draw (xtext, NULL, TRUE); gtk_xtext_render_ents (xtext, buf->pagetop_ent->next, buf->last_ent_end); xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y - win_height), @@ -1844,7 +1845,7 @@ gtk_xtext_scrollup_timeout (GtkXText * xtext) } xtext->select_start_y += delta_y; xtext->select_start_adj = xtext_adj_get_value (adj); - gtk_adjustment_value_changed (adj); + g_signal_emit_by_name (adj, "value-changed"); gtk_xtext_selection_draw (xtext, NULL, TRUE); gtk_xtext_render_ents (xtext, buf->pagetop_ent->prev, buf->last_ent_end); xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y), @@ -2969,19 +2970,17 @@ gtk_xtext_text_width (GtkXText *xtext, unsigned char *text, int len) static int gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str, - int len, int *emphasis) + int len, int *emphasis, int str_width) { - int str_width, dofill; - cairo_surface_t *surface = NULL; - int dest_x = 0, dest_y = 0; + int dofill; int tile_x = xtext->ts_x; int tile_y = xtext->ts_y; - GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (xtext)); if (xtext->dont_render || len < 1 || xtext->hidden) return 0; - str_width = backend_get_text_width_emph (xtext, str, len, *emphasis); + if (str_width < 0) + str_width = backend_get_text_width_emph (xtext, str, len, *emphasis); if (xtext->dont_render2) return str_width; @@ -3000,22 +2999,6 @@ gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str, goto dounder; } - if (!window) - return str_width; - surface = gdk_window_create_similar_surface (window, - CAIRO_CONTENT_COLOR_ALPHA, str_width, xtext->fontsize); - if (surface) - { - dest_x = x; - dest_y = y - xtext->font->ascent; - tile_x = xtext->ts_x - x; - tile_y = xtext->ts_y - dest_y; - - x = 0; - y = xtext->font->ascent; - xtext->draw_surface = surface; - } - dofill = TRUE; /* backcolor is always handled by XDrawImageString */ @@ -3029,45 +3012,13 @@ gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str, backend_draw_text_emph (xtext, dofill, x, y, str, len, str_width, *emphasis); - if (surface) - { - GdkRectangle clip; - GdkRectangle dest; - cairo_t *cr; - - xtext->draw_surface = NULL; - clip.x = xtext->clip_x; - clip.y = xtext->clip_y; - clip.width = xtext->clip_x2 - xtext->clip_x; - clip.height = xtext->clip_y2 - xtext->clip_y; - - dest.x = dest_x; - dest.y = dest_y; - dest.width = str_width; - dest.height = xtext->fontsize; - - if (gdk_rectangle_intersect (&clip, &dest, &dest)) - /* dump the DB to window, but only within the clip_x/x2/y/y2 */ - { - cr = xtext_create_context (xtext); - cairo_save (cr); - cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); - cairo_set_source_surface (cr, surface, dest_x, dest_y); - cairo_rectangle (cr, dest.x, dest.y, dest.width, dest.height); - cairo_fill (cr); - cairo_restore (cr); - cairo_destroy (cr); - } - cairo_surface_destroy (surface); - } - if (xtext->strikethrough) { cairo_t *cr; - /* pango_attr_strikethrough_new does not render in the custom widget so we need to reinvent the wheel */ - y = dest_y + (xtext->fontsize / 2); + int strike_y = y - xtext->font->ascent + (xtext->fontsize / 2); + cr = xtext_create_context (xtext); - xtext_draw_line (xtext, cr, &xtext->fgc, dest_x, y, dest_x + str_width - 1, y); + xtext_draw_line (xtext, cr, &xtext->fgc, x, strike_y, x + str_width - 1, strike_y); cairo_destroy (cr); } @@ -3076,17 +3027,10 @@ gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str, dounder: { cairo_t *cr; + int underline_y = y + 1; - if (surface) - y = dest_y + xtext->font->ascent + 1; - else - { - y++; - dest_x = x; - } - /* draw directly to window, it's out of the range of our DB */ cr = xtext_create_context (xtext); - xtext_draw_line (xtext, cr, &xtext->fgc, dest_x, y, dest_x + str_width - 1, y); + xtext_draw_line (xtext, cr, &xtext->fgc, x, underline_y, x + str_width - 1, underline_y); cairo_destroy (cr); } } @@ -3094,6 +3038,46 @@ dounder: return str_width; } +static int +gtk_xtext_lookup_run_width (textentry *ent, GSList **slp_cache, int off, int len, int emphasis) +{ + GSList *lp; + int emph = emphasis & (EMPH_ITAL | EMPH_BOLD); + + if (len < 1) + return 0; + + lp = slp_cache ? *slp_cache : NULL; + if (!lp) + lp = ent->slp; + + while (lp) + { + offlen_t *meta = lp->data; + + if (meta->off < off) + { + lp = g_slist_next (lp); + continue; + } + + break; + } + + if (slp_cache) + *slp_cache = lp; + + if (lp) + { + offlen_t *meta = lp->data; + + if (meta->off == off && meta->len == len && (meta->emph & (EMPH_ITAL | EMPH_BOLD)) == emph) + return meta->width; + } + + return -1; +} + static void gtk_xtext_reset (GtkXText * xtext, int mark, int attribs) { @@ -3177,7 +3161,7 @@ gtk_xtext_search_offset (xtext_buffer *buf, textentry *ent, unsigned int off) /* render a single line, which WONT wrap, and parse mIRC colors */ -#define RENDER_FLUSH x += gtk_xtext_render_flush (xtext, x, y, pstr, j, emphasis) +#define RENDER_FLUSH x += gtk_xtext_render_flush (xtext, x, y, pstr, j, emphasis, gtk_xtext_lookup_run_width (ent, &slp_cache, pstr - ent->str, j, *emphasis)) static int gtk_xtext_render_str (GtkXText * xtext, int y, textentry * ent, @@ -3193,6 +3177,7 @@ gtk_xtext_render_str (GtkXText * xtext, int y, textentry * ent, int k; int srch_underline = FALSE; int srch_mark = FALSE; + GSList *slp_cache = ent->slp; xtext->in_hilight = FALSE; @@ -3476,7 +3461,8 @@ gtk_xtext_render_str (GtkXText * xtext, int y, textentry * ent, /* have we been told to stop rendering at this point? */ if (xtext->jump_out_offset > 0 && xtext->jump_out_offset <= (i + offset)) { - gtk_xtext_render_flush (xtext, x, y, pstr, j, emphasis); + gtk_xtext_render_flush (xtext, x, y, pstr, j, emphasis, + gtk_xtext_lookup_run_width (ent, &slp_cache, pstr - ent->str, j, *emphasis)); ret = 0; /* skip the rest of the lines, we're done. */ j = 0; break; @@ -3837,7 +3823,7 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line, { /* small optimization */ gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline + 1)); - return g_slist_length (ent->sublines) - subline; + return ent->subline_count - subline; } } else { @@ -3910,6 +3896,7 @@ static void gtk_xtext_recalc_widths (xtext_buffer *buf, int do_str_width) { textentry *ent; + int prefix_width; /* since we have a new font, we have to recalc the text widths */ ent = buf->text_first; @@ -3921,10 +3908,10 @@ gtk_xtext_recalc_widths (xtext_buffer *buf, int do_str_width) } if (ent->left_len != -1) { + prefix_width = gtk_xtext_text_width (buf->xtext, ent->str, + ent->left_len + 1); ent->indent = - (buf->indent - - gtk_xtext_text_width (buf->xtext, ent->str, - ent->left_len)) - buf->xtext->space_width; + (buf->indent - prefix_width); if (ent->indent < MARGIN) ent->indent = MARGIN; } @@ -4015,12 +4002,14 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent) g_slist_free (ent->sublines); ent->sublines = NULL; + ent->subline_count = 0; win_width = buf->window_width - MARGIN; if (win_width >= ent->indent + ent->str_width) { ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len)); - return 1; + ent->subline_count = 1; + return ent->subline_count; } indent = ent->indent; @@ -4035,7 +4024,8 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent) } while (str < ent->str + ent->str_len); - return g_slist_length (ent->sublines); + ent->subline_count = g_slist_length (ent->sublines); + return ent->subline_count; } /* Calculate number of actual lines (with wraps), to set adj->lower. * @@ -4112,7 +4102,7 @@ gtk_xtext_nth (GtkXText *xtext, int line, int *subline) ent = ent->prev; if (!ent) break; - lines -= g_slist_length (ent->sublines); + lines -= ent->subline_count; } return NULL; } @@ -4121,10 +4111,10 @@ gtk_xtext_nth (GtkXText *xtext, int line, int *subline) while (ent) { - lines += g_slist_length (ent->sublines); + lines += ent->subline_count; if (lines > line) { - *subline = g_slist_length (ent->sublines) - (lines - line); + *subline = ent->subline_count - (lines - line); return ent; } ent = ent->next; @@ -4218,7 +4208,7 @@ gtk_xtext_render_ents (GtkXText * xtext, textentry * enta, textentry * entb) line -= subline; subline = 0; } - line += g_slist_length (ent->sublines); + line += ent->subline_count; } if (ent == entb) @@ -4303,7 +4293,7 @@ gtk_xtext_render_page (GtkXText * xtext) xtext->buffer->last_pixel_pos = pos; #ifndef __APPLE__ - if (!xtext->background_surface && abs (overlap) < height) + if (abs (overlap) < height) { GdkRectangle area; cairo_t *cr; @@ -4452,6 +4442,8 @@ gtk_xtext_kill_ent (xtext_buffer *buffer, textentry *ent) g_slist_free_full (ent->slp, g_free); g_slist_free (ent->sublines); + ent->sublines = NULL; + ent->subline_count = 0; g_free (ent); return visible; @@ -4467,22 +4459,22 @@ gtk_xtext_remove_top (xtext_buffer *buffer) ent = buffer->text_first; if (!ent) return; - buffer->num_lines -= g_slist_length (ent->sublines); - buffer->pagetop_line -= g_slist_length (ent->sublines); - buffer->last_pixel_pos -= (g_slist_length (ent->sublines) * buffer->xtext->fontsize); + buffer->num_lines -= ent->subline_count; + buffer->pagetop_line -= ent->subline_count; + buffer->last_pixel_pos -= (ent->subline_count * buffer->xtext->fontsize); buffer->text_first = ent->next; if (buffer->text_first) buffer->text_first->prev = NULL; else buffer->text_last = NULL; - buffer->old_value -= g_slist_length (ent->sublines); + buffer->old_value -= ent->subline_count; if (buffer->xtext->buffer == buffer) /* is it the current buffer? */ { xtext_adj_set_value (buffer->xtext->adj, xtext_adj_get_value (buffer->xtext->adj) - - g_slist_length (ent->sublines)); - buffer->xtext->select_start_adj -= g_slist_length (ent->sublines); + ent->subline_count); + buffer->xtext->select_start_adj -= ent->subline_count; } if (gtk_xtext_kill_ent (buffer, ent)) @@ -4512,7 +4504,7 @@ gtk_xtext_remove_bottom (xtext_buffer *buffer) ent = buffer->text_last; if (!ent) return; - buffer->num_lines -= g_slist_length (ent->sublines); + buffer->num_lines -= ent->subline_count; buffer->text_last = ent->prev; if (buffer->text_last) buffer->text_last->next = NULL; @@ -4638,7 +4630,7 @@ gtk_xtext_check_ent_visibility (GtkXText * xtext, textentry *find_ent, int add) lines = ((height + xtext->pixel_offset) / xtext->fontsize) + buf->pagetop_subline + add; while (ent) { - lines -= g_slist_length (ent->sublines); + lines -= ent->subline_count; if (lines <= 0) { return FALSE; @@ -5025,7 +5017,7 @@ gtk_xtext_search (GtkXText * xtext, const gchar *text, gtk_xtext_search_flags fl for (value = 0, ent = buf->text_first; ent && ent != buf->hintsearch; ent = ent->next) { - value += g_slist_length (ent->sublines); + value += ent->subline_count; } if (value > xtext_adj_get_upper (adj) - xtext_adj_get_page_size (adj)) { @@ -5034,7 +5026,7 @@ gtk_xtext_search (GtkXText * xtext, const gchar *text, gtk_xtext_search_flags fl else if ((flags & backward) && ent) { value -= xtext_adj_get_page_size (adj) - - g_slist_length (ent->sublines); + ent->subline_count; if (value < 0) { value = 0; @@ -5111,6 +5103,7 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp) ent->mark_end = -1; ent->next = NULL; ent->marks = NULL; + ent->subline_count = 0; if (ent->indent < MARGIN) ent->indent = MARGIN; /* 2 pixels is the left margin */ @@ -5194,7 +5187,7 @@ gtk_xtext_append_indent (xtext_buffer *buf, unsigned char *str; int space; int tempindent; - int left_width; + int prefix_width; int min_indent; if (left_len == -1) @@ -5219,12 +5212,12 @@ gtk_xtext_append_indent (xtext_buffer *buf, memcpy (str + left_len + 1, right_text, right_len); str[left_len + 1 + right_len] = 0; - left_width = gtk_xtext_text_width (buf->xtext, left_text, left_len); + prefix_width = gtk_xtext_text_width (buf->xtext, str, left_len + 1); ent->left_len = left_len; ent->str = str; ent->str_len = left_len + 1 + right_len; - ent->indent = (buf->indent - left_width) - buf->xtext->space_width; + ent->indent = buf->indent - prefix_width; /* This is copied into the scratch buffer later, double check math */ g_assert (ent->str_len < sizeof (buf->xtext->scratch_buffer)); @@ -5241,7 +5234,7 @@ gtk_xtext_append_indent (xtext_buffer *buf, buf->indent < buf->xtext->max_auto_indent && ent->indent < min_indent) { - tempindent = min_indent + buf->xtext->space_width + left_width; + tempindent = min_indent + prefix_width; /* Ignore tiny one-pixel style nudges. * They can trigger expensive full-width recalculations and are @@ -5252,12 +5245,12 @@ gtk_xtext_append_indent (xtext_buffer *buf, if (buf->indent > buf->xtext->max_auto_indent) buf->indent = buf->xtext->max_auto_indent; - if (buf->indent > ent->indent + left_width + buf->xtext->space_width) + if (buf->indent > ent->indent + prefix_width) { gtk_xtext_fix_indent (buf); gtk_xtext_recalc_widths (buf, FALSE); - ent->indent = (buf->indent - left_width) - buf->xtext->space_width; + ent->indent = buf->indent - prefix_width; buf->xtext->force_render = TRUE; } } @@ -5457,7 +5450,7 @@ gtk_xtext_moveto_marker_pos (GtkXText *xtext) { if (ent == buf->marker_pos) break; - value += g_slist_length (ent->sublines); + value += ent->subline_count; ent = ent->next; } if (value >= xtext_adj_get_value (adj) && @@ -5595,7 +5588,7 @@ gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render) xtext_adjustment_apply (xtext->adj, lower, upper, value, page_size); gtk_adjustment_set_page_increment (xtext->adj, page_size); - gtk_adjustment_value_changed (xtext->adj); + g_signal_emit_by_name (xtext->adj, "value-changed"); } } } diff --git a/troubleshooting.md b/troubleshooting.md index 23cf8c51..295c2991 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -39,3 +39,16 @@ You can reset overrides after testing: ```bash flatpak override --user --reset net.zoite.Zoitechat ``` + +## Theme palette migration and legacy keys + +New builds store theme colors as semantic token keys in `colors.conf` (for example `theme.mode.light.token.mirc_0`). +When loading old configs that only contain legacy keys (`color_*` and `dark_color_*`), ZoiteChat migrates them automatically if `theme.palette.semantic_migrated` is not present. + +By default saves write both semantic token keys and legacy keys for compatibility. To phase out legacy writes, set: + +```bash +export ZOITECHAT_THEME_WRITE_LEGACY_KEYS=0 +``` + +With that setting, saves write semantic token keys only while legacy-key loading remains available for older config files that have not yet been marked as migrated.