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.