diff --git a/src/common/cfgfiles.c b/src/common/cfgfiles.c index 8c17dc0d..c147fb47 100644 --- a/src/common/cfgfiles.c +++ b/src/common/cfgfiles.c @@ -433,6 +433,7 @@ const struct prefs vars[] = {"gui_tab_dialogs", P_OFFINT (hex_gui_tab_dialogs), TYPE_BOOL}, {"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL}, {"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL}, + {"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_BOOL}, {"gui_tab_layout", P_OFFINT (hex_gui_tab_layout), TYPE_INT}, {"gui_tab_middleclose", P_OFFINT (hex_gui_tab_middleclose), TYPE_BOOL}, {"gui_tab_newtofront", P_OFFINT (hex_gui_tab_newtofront), TYPE_INT}, diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 2a5d466e..41eb98ac 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -131,6 +131,7 @@ struct zoitechatprefs unsigned int hex_gui_tab_dialogs; unsigned int hex_gui_tab_dots; unsigned int hex_gui_tab_icons; + unsigned int hex_gui_dark_mode; unsigned int hex_gui_tab_scrollchans; unsigned int hex_gui_tab_server; unsigned int hex_gui_tab_sort; diff --git a/src/fe-gtk/chanview-tree.c b/src/fe-gtk/chanview-tree.c index 02d43bfa..9dd8359e 100644 --- a/src/fe-gtk/chanview-tree.c +++ b/src/fe-gtk/chanview-tree.c @@ -182,6 +182,7 @@ cv_tree_init (chanview *cv) ((treeview *)cv)->tree = GTK_TREE_VIEW (view); ((treeview *)cv)->scrollw = win; + chanview_apply_theme (cv); gtk_widget_show (view); } diff --git a/src/fe-gtk/chanview.c b/src/fe-gtk/chanview.c index 9f360865..ea5113b7 100644 --- a/src/fe-gtk/chanview.c +++ b/src/fe-gtk/chanview.c @@ -28,6 +28,7 @@ #include "maingui.h" #include "gtkutil.h" #include "chanview.h" +#include "palette.h" /* treeStore columns */ #define COL_NAME 0 /* (char *) */ @@ -103,6 +104,37 @@ static int cv_find_number_of_chan (chanview *cv, chan *find_ch); /* ==== ABSTRACT CHANVIEW ==== */ +void +chanview_apply_theme (chanview *cv) +{ + GtkWidget *w; + treeview *tv; + + if (cv == NULL) + return; + + /* Only the tree implementation has a GtkTreeView we can explicitly style. */ + if (cv->func_init != cv_tree_init) + return; + + tv = (treeview *) cv; + if (tv->tree == NULL) + return; + + w = GTK_WIDGET (tv->tree); + if (prefs.hex_gui_dark_mode) + { + gtk_widget_modify_base (w, GTK_STATE_NORMAL, &colors[COL_BG]); + gtk_widget_modify_text (w, GTK_STATE_NORMAL, &colors[COL_FG]); + } + else + { + /* Revert back to theme defaults. */ + gtk_widget_modify_base (w, GTK_STATE_NORMAL, NULL); + gtk_widget_modify_text (w, GTK_STATE_NORMAL, NULL); + } +} + static char * truncate_tab_name (char *name, int max) { diff --git a/src/fe-gtk/chanview.h b/src/fe-gtk/chanview.h index 2f911a5a..88ef9413 100644 --- a/src/fe-gtk/chanview.h +++ b/src/fe-gtk/chanview.h @@ -48,6 +48,13 @@ gboolean chan_remove (chan *ch, gboolean force); gboolean chan_is_collapsed (chan *ch); chan * chan_get_parent (chan *ch); +/* + * Apply ZoiteChat's optional "dark mode" styling to the channel list. + * + * This is a no-op for implementations that don't have a tree view. + */ +void chanview_apply_theme (chanview *cv); + #define FOCUS_NEW_ALL 1 #define FOCUS_NEW_ONLY_ASKED 2 #define FOCUS_NEW_NONE 0 diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c index c5ede069..da415d25 100644 --- a/src/fe-gtk/fe-gtk.c +++ b/src/fe-gtk/fe-gtk.c @@ -317,6 +317,7 @@ void fe_init (void) { palette_load (); + palette_apply_dark_mode (prefs.hex_gui_dark_mode); key_init (); pixmaps_init (); diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index 4b69c8d5..0052cc29 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -2480,7 +2480,20 @@ mg_create_userlist (session_gui *gui, GtkWidget *box) if (prefs.hex_gui_ulist_style) { gtk_widget_set_style (ulist, input_style); + } + + /* + * 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 GTK2 themes. + */ + if (prefs.hex_gui_ulist_style || prefs.hex_gui_dark_mode) + { gtk_widget_modify_base (ulist, GTK_STATE_NORMAL, &colors[COL_BG]); + gtk_widget_modify_text (ulist, GTK_STATE_NORMAL, &colors[COL_FG]); } mg_create_meters (gui, vbox); diff --git a/src/fe-gtk/palette.c b/src/fe-gtk/palette.c index 46445179..96831353 100644 --- a/src/fe-gtk/palette.c +++ b/src/fe-gtk/palette.c @@ -169,3 +169,86 @@ palette_save (void) close (fh); } } + +static gboolean +palette_color_eq (const GdkColor *a, const GdkColor *b) +{ + return a->red == b->red && a->green == b->green && a->blue == b->blue; +} + +gboolean +palette_apply_dark_mode (gboolean enable) +{ + /* + * Stock ZoiteChat defaults from this file (keep them in sync with the + * colors[] initializer above): + * - Foreground: 0x2512/0x29e8/0x2b85 + * - Background: 0xfae0/0xfae0/0xf8c4 + */ + static const GdkColor light_fg = {0, 0x2512, 0x29e8, 0x2b85}; + static const GdkColor light_bg = {0, 0xfae0, 0xfae0, 0xf8c4}; + + /* Common legacy "defaults" seen in the wild (pure black/white). */ + static const GdkColor legacy_light_fg = {0, 0x0000, 0x0000, 0x0000}; + static const GdkColor legacy_light_bg = {0, 0xffff, 0xffff, 0xffff}; + + /* A readable "dark" preset (roughly #D4D4D4 on #1E1E1E). */ + static const GdkColor dark_fg = {0, 0xd4d4, 0xd4d4, 0xd4d4}; + static const GdkColor dark_bg = {0, 0x1e1e, 0x1e1e, 0x1e1e}; + + gboolean changed = FALSE; + + if (enable) + { + if (palette_color_eq (&colors[COL_FG], &light_fg) || + palette_color_eq (&colors[COL_FG], &legacy_light_fg)) + { + colors[COL_FG].red = dark_fg.red; + colors[COL_FG].green = dark_fg.green; + colors[COL_FG].blue = dark_fg.blue; + changed = TRUE; + } + if (palette_color_eq (&colors[COL_BG], &light_bg) || + palette_color_eq (&colors[COL_BG], &legacy_light_bg)) + { + colors[COL_BG].red = dark_bg.red; + colors[COL_BG].green = dark_bg.green; + colors[COL_BG].blue = dark_bg.blue; + changed = TRUE; + } + } + else + { + if (palette_color_eq (&colors[COL_FG], &dark_fg)) + { + /* + * Revert to a predictable light default. Most users who hit this toggle + * are coming from black-on-white palettes. + */ + colors[COL_FG].red = legacy_light_fg.red; + colors[COL_FG].green = legacy_light_fg.green; + colors[COL_FG].blue = legacy_light_fg.blue; + changed = TRUE; + } + if (palette_color_eq (&colors[COL_BG], &dark_bg)) + { + colors[COL_BG].red = legacy_light_bg.red; + colors[COL_BG].green = legacy_light_bg.green; + colors[COL_BG].blue = legacy_light_bg.blue; + changed = TRUE; + } + } + + /* Ensure the new colors have pixels allocated in the current colormap. */ + if (changed) + { + GdkColormap *cmap = gdk_colormap_get_system (); + if (cmap) + { + gdk_colormap_alloc_color (cmap, &colors[COL_FG], TRUE, TRUE); + gdk_colormap_alloc_color (cmap, &colors[COL_BG], TRUE, TRUE); + } + } + + return changed; +} diff --git a/src/fe-gtk/palette.h b/src/fe-gtk/palette.h index 2a451429..5b887f5c 100644 --- a/src/fe-gtk/palette.h +++ b/src/fe-gtk/palette.h @@ -38,4 +38,15 @@ void palette_alloc (GtkWidget * widget); void palette_load (void); void palette_save (void); +/* + * Apply ZoiteChat's built-in "dark mode" background/foreground overrides. + * + * This is intentionally conservative: it only adjusts the palette if the + * colors are still at ZoiteChat's stock defaults, so user-customized palettes + * continue to take precedence. + * + * Returns TRUE if any palette entries were changed. + */ +gboolean palette_apply_dark_mode (gboolean enable); + #endif diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index ba9de165..fdc9c1c3 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -33,6 +33,7 @@ #include "fe-gtk.h" #include "gtkutil.h" #include "maingui.h" +#include "chanview.h" #include "palette.h" #include "pixmaps.h" #include "menu.h" @@ -341,6 +342,17 @@ static const setting color_settings[] = {ST_END, 0, 0, 0, 0, 0} }; +static const setting dark_mode_setting = +{ + ST_TOGGLE, + N_("Enable dark mode for chat views"), + P_OFFINTNL(hex_gui_dark_mode), + N_("Makes the chat buffer, channel list, and user list use a dark background/foreground.\n" + "This only changes the stock background/foreground colors. If you've customized them, your palette wins."), + 0, + 0 +}; + static const char *const dccaccept[] = { N_("Ask for confirmation"), @@ -1570,6 +1582,7 @@ setup_create_color_page (void) 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_toggleL (tab, 13, &dark_mode_setting); setup_create_header (tab, 15, N_("Color Stripping")); @@ -2048,10 +2061,25 @@ static void setup_apply_to_sess (session_gui *gui) { mg_update_xtext (gui->xtext); + chanview_apply_theme ((chanview *) gui->chanview); if (prefs.hex_gui_ulist_style) gtk_widget_set_style (gui->user_tree, input_style); + if (prefs.hex_gui_ulist_style || prefs.hex_gui_dark_mode) + { + gtk_widget_modify_base (gui->user_tree, GTK_STATE_NORMAL, &colors[COL_BG]); + if (prefs.hex_gui_dark_mode) + gtk_widget_modify_text (gui->user_tree, GTK_STATE_NORMAL, &colors[COL_FG]); + else + gtk_widget_modify_text (gui->user_tree, GTK_STATE_NORMAL, NULL); + } + else + { + gtk_widget_modify_base (gui->user_tree, GTK_STATE_NORMAL, NULL); + gtk_widget_modify_text (gui->user_tree, GTK_STATE_NORMAL, NULL); + } + if (prefs.hex_gui_input_style) { extern char cursor_color_rc[]; @@ -2168,6 +2196,7 @@ setup_apply (struct zoitechatprefs *pr) int do_ulist = FALSE; int do_layout = FALSE; int do_identd = FALSE; + int old_dark_mode = prefs.hex_gui_dark_mode; if (strcmp (pr->hex_text_background, prefs.hex_text_background) != 0) new_pix = TRUE; @@ -2238,6 +2267,20 @@ setup_apply (struct zoitechatprefs *pr) memcpy (&prefs, pr, sizeof (prefs)); + /* + * "Dark mode" is mostly about the chat views. Be conservative: only adjust + * the stock Foreground/Background colors so user palettes keep winning. + * + * IMPORTANT: don't short-circuit this call. + * We MUST run palette_apply_dark_mode() when the toggle changes, otherwise + * the preference flips but the palette stays the same (aka: "nothing happens"). + */ + { + gboolean pal_changed = palette_apply_dark_mode (prefs.hex_gui_dark_mode); + if (prefs.hex_gui_dark_mode != old_dark_mode || pal_changed) + color_change = TRUE; + } + #ifdef WIN32 /* merge hex_font_main and hex_font_alternative into hex_font_normal */ old_desc = pango_font_description_from_string (prefs.hex_text_font_main);