46 Commits

Author SHA1 Message Date
69985913b8 win32 argv: stop trusting GLib, add “zoitechat” lifesaver 2026-02-26 13:35:09 -07:00
df45cc996d Fix Windows theme imports with uppercase .TAR.GZ/etc (case-insensitive archive check) 2026-02-26 13:15:49 -07:00
5763653672 I fixed a Windows theming regression path in fe_apply_windows_theme() by changing the fallback-disable condition from “a GTK3 theme name is configured” to “a GTK3 theme provider is actually active.” This ensures fallback dark/light palette CSS is still applied when a configured imported GTK3 theme fails to load, preserving Windows release theming behavior instead of leaving mismatched visuals.
I added an in-code explanation documenting this Windows-release safety behavior so the intent is explicit for future maintenance.
2026-02-26 12:10:59 -07:00
2ece544792 Updated the Windows Inno Setup installer template to explicitly show the install-directory step by setting DisableDirPage=no, so users are asked where to install ZoiteChat.
Disabled automatic reuse of a previously selected install path by setting UsePreviousAppDir=no, ensuring the prompt appears each installer run.
2026-02-26 11:56:01 -07:00
8e0958fd19 Added a Windows-specific startup path in main() to reconstruct argc/argv from g_win32_get_command_line() before any argument parsing, so GLib option parsing no longer depends on potentially malformed CRT argv in subsystem/protocol-handler launches (a likely source of your launch crash stack through glib-2.0-0.dll).
Ensured the allocated Windows command-line vector is freed on all early-return and normal-exit paths in main() to avoid leaks/regressions from the new startup handling.
2026-02-26 11:54:42 -07:00
324aeab8c9 Fixed the Win32 startup path initialization in fe_args to avoid blindly calling g_path_get_dirname(argv[0]) when argv may be missing/invalid in subsystem:windows launch contexts (like URL handler launches).
Updated logic to prefer g_win32_get_package_installation_directory_of_module(NULL) first, and only fall back to argv[0] when argc > 0, argv != NULL, and argv[0] != NULL, preventing null/invalid pointer access during startup.
2026-02-26 11:38:08 -07:00
e8dbe06f01 Added a Win32-only forward declaration for fe_apply_windows_theme(gboolean dark) before its first use so MSVC sees the correct signature and no longer infers an int return type. This addresses the C4013 + C2371 sequence from your build log. 2026-02-26 11:10:33 -07:00
f3213c56eb I implemented a broader, platform-agnostic theme application path so chat-area surfaces and controls actually follow the selected ZoiteChat dark/light mode, instead of relying on a narrow fallback path. This adds a dedicated app CSS provider (theme_surface_provider) and applies palette-based foreground/background rules for core widgets (box, grid, frame, viewport, paned, notebook, eventbox, scrolledwindow, treeview, button, entry) scoped by .zoitechat-light / .zoitechat-dark.
I wired that CSS application into the theme switch flow (fe_apply_theme_for_mode) so changing theme mode now reapplies those surface colors immediately during runtime mode changes.
2026-02-26 11:03:18 -07:00
9ef607f44a Fixed GTK3 theme switching on Win32 so fallback window CSS is synchronized immediately when switching back to the system theme (theme_name == NULL) by calling fe_apply_windows_theme(dark) in that path before provider cleanup.
Fixed GTK3 theme activation on Win32 so fallback window CSS is disabled immediately after loading an imported GTK3 theme, preventing mixed styling (like unthemed buttons/chat areas) during runtime theme changes
2026-02-26 08:58:38 -07:00
a93bed2c41 Adjusted the Windows theme fallback path so it does not apply ZoiteChat’s fallback window-color CSS when a GTK3 theme is selected, preventing the mixed-theme effect you reported (e.g., mismatched button/chat window colors).
Added cleanup for previously-added fallback CSS providers (with widget style reset) when a GTK3 theme is active, so the imported theme can fully control styling consistently.
Kept the original Windows fallback CSS behavior for cases where no imported GTK3 theme is set, so non-GTK3-theme behavior remains unchanged.
2026-02-26 08:41:52 -07:00
d10c9654fa Added a GTK3 theme directory resolver that supports versioned theme folders (gtk-3.x) and selects the best match for the running GTK minor version, instead of assuming only gtk-3.0. This improves compatibility with real-world GTK3 themes that split selectors/styles by GTK minor versions.
Updated theme application to load CSS/resources from the resolved gtk-3.x folder and updated the error path/message when a valid gtk-3.x/gtk.css layout is missing.

Exposed the resolver in the GTK frontend header so other GTK UI code can validate theme layouts consistently.

Updated the Preferences GTK3 theme picker to validate themes via the resolver (so themes with e.g. gtk-3.24/gtk.css now appear as valid).

Updated archive import validation to recognize gtk-3.x directories (not just gtk-3.0) and adjusted user-facing validation messages accordingly.
2026-02-26 08:35:37 -07:00
a823047104 Fixed imported GTK3 theme precedence so theme CSS can override ZoiteChat app-level CSS (including button background rules) by changing the provider priority from GTK_STYLE_PROVIDER_PRIORITY_APPLICATION to GTK_STYLE_PROVIDER_PRIORITY_USER.
Updated the inline comment to document why user-level priority is needed for correct themed button styling behavior.
2026-02-26 08:26:03 -07:00
bf349a27b1 Updated GTK3 theme provider registration to use GTK_STYLE_PROVIDER_PRIORITY_APPLICATION (instead of GTK_STYLE_PROVIDER_PRIORITY_THEME) when applying imported themes, so the selected GTK3 theme consistently overrides the system theme for ZoiteChat widgets.
Added an inline code comment explaining the priority choice and the consistency intent.
2026-02-26 08:21:15 -07:00
90ada474a0 Added GTK3 theme resource lifecycle management so ZoiteChat now unregisters prior theme resources and can register a theme’s gtk.gresource bundle when available (gtk-3.0/gtk.gresource). This improves compatibility with full GTK3 themes that depend on bundled resource URIs, not just plain CSS files.
Extended fe_apply_gtk3_theme_with_reload to:

detect gtk.gresource,

register it before applying CSS,

unregister resources when no resource file is present or when switching back to system theme,

and clean up allocated paths consistently on error paths
2026-02-26 08:04:27 -07:00
4a996c9135 Added a reload-capable GTK3 theme apply API (fe_apply_gtk3_theme_with_reload) and kept fe_apply_gtk3_theme as the default fast-path wrapper (force_reload = FALSE).
Updated the same-theme early return so it is bypassed when reload is requested, while preserving the existing provider reset/replacement flow, gtk_style_context_reset_widgets, and top-level reapply behavior.

Wired setup import/apply flow to force a reload on the next apply after successful archive import, ensuring same-name imported themes are reloaded from disk; the flag is cleared after apply and when switching back to system theme.
2026-02-26 02:38:12 -07:00
c9682d98f3 Updated setup_theme_gtk3_import_cb so the GTK3 theme import file chooser dialog now calls fe_apply_theme_to_toplevel(dialog) immediately after gtk_file_chooser_dialog_new, ensuring it receives app-level theme classes like other dialogs.
Added the same short explanatory comment style already used in setup_theme_show_message (“Window classes are required for GTK CSS selectors like .zoitechat-dark / .zoitechat-light.”), keeping behavior/style aligned with existing code conventions.

    Verified the open/cancel/import flow is unchanged: the same GTK_RESPONSE_ACCEPT gate, early return on cancel, file extraction path retrieval, and import success/error handling remain intact.
2026-02-26 02:33:52 -07:00
8a4ecf8649 Fixed the GTK3 theme persistence bug in Preferences by syncing the in-dialog working copy (setup_prefs.hex_gui_gtk3_theme_name) whenever a GTK3 theme is applied, so pressing OK no longer overwrites the newly selected theme with stale state.
Fixed the same persistence path for “Use System GTK Theme” by clearing both prefs and setup_prefs, preventing the theme from reverting after the dialog closes.

    Reverted GTK3 provider priority back to GTK_STYLE_PROVIDER_PRIORITY_THEME (from application priority), which addresses the dropdown/menu rendering regression introduced by the previous commit.
2026-02-26 02:30:34 -07:00
252f4a3c07 Fixed GTK3 theme teardown to force a full widget style refresh after removing ZoiteChat’s GTK3 theme provider, so switching back to system theme updates existing windows/widgets immediately.
Changed imported GTK3 theme provider registration from GTK_STYLE_PROVIDER_PRIORITY_APPLICATION to GTK_STYLE_PROVIDER_PRIORITY_THEME, so GTK3 theme rules apply correctly across themed controls (including menu/dropdown-related GTK theme elements) instead of being overly overridden by app-priority styling.
2026-02-26 02:22:00 -07:00
d21a5c1b60 Removed the legacy .hct/.zct theme-path behavior and made theme argument detection GTK3-archive-only (.zip, .tar, .tar.gz, .tgz, .tar.xz, .txz).
Deleted the old ZoiteChat palette/event theme import/apply implementation and kept GTK3 archive import as the sole theme import backend; the GTK3 import API now optionally returns the imported theme name for better caller messaging.

Updated startup and /URL handling so theme archives are imported into gtk3-themes and users are prompted to apply them via Theme settings, instead of auto-applying old ZoiteChat themes.

Simplified the Theme Manager UI/data model to GTK3-only by removing the old “ZoiteChat Theme” controls and keeping the GTK3 import/apply/use-system workflow.
2026-02-26 02:18:22 -07:00
a1ba30865a Kept legacy .zct/.hct import flow untouched while improving GTK3 archive validation logic: GTK3 import now explicitly distinguishes between archives that include gtk-3.0 but miss gtk.css, versus archives that are not GTK3 theme layouts at all. This is implemented in the GTK3 archive scan/import path only (zoitechat_find_gtk3_theme_root + zoitechat_import_gtk3_theme_archive). 2026-02-26 01:01:09 -07:00
607faa80ca Fixed the GTK3 dropdown regression by switching from combo-box ID/path behavior to an explicit internal index→full-path mapping (gtk3_theme_paths) while keeping user-facing theme names in the dropdown text. This preserves internal full path mapping without relying on active-id lookups.
Kept GTK3 discovery restricted to the app-local install directory (get_xdir()/gtk3-themes) and only included directories containing gtk-3.0/gtk.css.

Ensured theme selection is restored/saved correctly by matching saved pref name against discovered entries and applying via the selected internal path, then persisting prefs.hex_gui_gtk3_theme_name with save_config().

Preserved/updated empty-invalid status behavior to show “No valid GTK3 themes found.” when no usable themes exist (or saved selection is invalid).

Added proper cleanup for the new internal mapping via setup_theme_ui_free and wired it into g_object_set_data_full.
2026-02-26 00:56:17 -07:00
1c1110847c Fixed the GTK3 theme dropdown population to include all expected sources again (ZoiteChat local store, user local themes, and system theme dirs), which resolves the “messed up selector” behavior from the previous change.
Restored proper initial selection logic so the dropdown now prefers saved gui_gtk3_theme_name when present, and otherwise falls back to the current GTK gtk-theme-name.

    Fixed selection UX by not forcing index 0; it starts unselected and selects only if a real match is found. Also made Apply button sensitivity follow actual selection state.

    Updated the status text to reflect mixed-source theme discovery and added cleanup for allocated selection strings/path entries in this code path.
2026-02-26 00:47:04 -07:00
a796f78884 Fixed the GTK3 theme import crash by replacing the dark-variant notification call from the signal macro path to fe_message(..., FE_MSG_INFO), avoiding the segfaulting path during import completion while preserving user feedback. 2026-02-26 00:35:42 -07:00
4354aaa57a Added a new backend API, zoitechat_import_gtk3_theme_archive(const char *archive_path, GError **error), and exposed it in the common header so GTK setup code can call a shared import path instead of inlining extraction logic.
Updated the GTK3 import callback in setup.c to:

    open a file chooser titled Import GTK3 Theme ZIP,

    enforce a ZIP-focused chooser filter (*.zip, *.ZIP),

    call the new backend function,

    show success/failure dialogs through setup_theme_show_message,

    refresh the GTK3 theme list immediately after successful import.

Updated the GTK3 section button label to Import GTK3 Theme ZIP and added/used new translatable strings for the new button text and import messages.
2026-02-26 00:16:05 -07:00
4aeb5b5697 Split the theme setup page into two explicit framed sections: “ZoiteChat Theme” (legacy colors.conf / pevents.conf flow) and “GTK3 Theme” (GTK theme import/select/apply flow), with distinct button labels like “Apply ZoiteChat Theme” and “Apply GTK3 Theme.”
Expanded setup_theme_ui to track separate widget state for ZoiteChat controls and GTK3 controls (zoitechat_* and gtk3_* fields), so the two flows are no longer sharing ambiguous UI pointers.

    Kept the existing ZoiteChat apply path using zoitechat_apply_theme, but updated selection/status/apply messaging so it clearly indicates palette/event updates rather than GTK theme activation.

    Added GTK3 theme management behavior:

        discovery/population from standard theme directories,

        import from archive via file chooser into ~/.themes/<basename>,

        apply via GtkSettings (gtk-theme-name),

        status/info messages that explicitly say GTK3 activation does not change ZoiteChat palette settings.
2026-02-26 00:11:05 -07:00
30609ba6db Updated Win32 theme CSS generation in fe_apply_windows_theme to build .zoitechat-dark and .zoitechat-light styles dynamically from palette values (COL_FG/COL_BG) instead of fixed hex literals, via a new helper that serializes PaletteColor with gdk_rgba_to_string.
Kept native titlebar dark-mode behavior intact by leaving the existing Win32 titlebar flow untouched and continuing to call theme preference updates in fe_apply_windows_theme.
Ensured theme/palette apply paths refresh CSS by routing both FE_GUI_APPLY and setup theme apply through fe_apply_theme_for_mode(...), which re-runs Win32 CSS application and re-applies classes to toplevel windows.
2026-02-25 23:53:31 -07:00
6b8e41b4c6 Updated mg_create_tabmenu to remove the hardcoded inline foreground="#3344cc" color from the top menu title and keep only bold emphasis (<b>…</b>), so GTK theme colors are used naturally. 2026-02-25 23:47:42 -07:00
0edab77fac Removed the local menu_icon_exists_in_resource probe from menu.c, so this file no longer carries a hardcoded /icons/menu/light resource check path. menu_icon_widget_new now calls gtkutil_menu_icon_exists for the zc-menu-* candidate check. This keeps the same fallback flow for custom icons: direct readable path first, then config-dir path, then stock/themed lookup.
Added gtkutil_menu_icon_exists to gtkutil.c and declared it in gtkutil.h. The implementation uses a shared helper that resolves menu icon resources with theme-variant-aware lookup (variant first, then light fallback) for both png and svg, so probing behavior now lives in the same subsystem as gtkutil_image_new_from_stock.
2026-02-25 23:40:38 -07:00
c8ae4f3b18 Removed the unused gtkutil_menu_icon_pixbuf_new helper from src/fe-gtk/gtkutil.c (it no longer appears between the surrounding menu icon helper and stock-icon mapping functions).
Kept gtkutil_menu_icon_image_new as the single menu-icon loading path, which already contains the requested theme variant selection and light-variant fallback behavior for both PNG and SVG assets.
2026-02-25 23:36:01 -07:00
578a417804 Added theming to menu_about() immediately after gtk_about_dialog_new() by calling fe_apply_theme_to_toplevel (GTK_WIDGET (dialog));, with the requested short CSS-selector comment style.
Added theming to setup_browsefont_cb() immediately after gtk_font_chooser_dialog_new(...) by calling fe_apply_theme_to_toplevel (dialog);, plus the same short comment for consistency/discoverability.
Added theming to setup_color_cb() immediately after gtk_color_chooser_dialog_new(...) by calling fe_apply_theme_to_toplevel (dialog);, again with the matching comment.
2026-02-25 23:30:35 -07:00
440e9ecf5a Updated fe_ctrl_gui()’s FE_GUI_APPLY branch to match the Preferences → Theme apply sequence by loading palette data first, then re-applying current dark-mode state, and only then calling setup_apply_real(TRUE, TRUE, TRUE, FALSE);.
Added an inline regression-prevention comment noting this path should stay in parity with setup_theme_apply_cb.

    Verified palette.h is already included in src/fe-gtk/fe-gtk.c (no include changes required).
2026-02-25 23:27:21 -07:00
ac2ab1443c Added fe_apply_theme_to_toplevel() for:
Standard GTK file chooser fallback dialog in src/fe-gtk/gtkutil.c

        Font chooser dialog in src/fe-gtk/setup.c

        Color chooser dialog in src/fe-gtk/setup.c

        About dialog in src/fe-gtk/menu.c
2026-02-25 23:18:19 -07:00
ce5128e4fb Added a theme_name null guard in the Adwaita/Yaru workaround condition inside create_input_style() so g_str_has_prefix() is only called when theme_name is non-null, while preserving existing behavior for non-null theme names.
Left allocation/free flow unchanged; theme_name is still freed exactly once at the existing cleanup point after the reload block.
2026-02-25 22:52:40 -07:00
c37faa1492 Refactored the Preferences color-page edit-source model to use an explicit snapshot pointer (setup_color_edit_source_colors) and a helper that selects light vs dark snapshot palettes, so the page preview is decoupled from the runtime colors[] palette. This affects selector refresh, page initialization, and cleanup lifecycle.
Updated edit-target and dark-mode combo callbacks on the color page to stop applying global palette mode directly; they now only switch/refresh the Preferences page source palette.
Changed color-chooser behavior so setup_color_response_cb() writes edits to the selected target snapshot via palette_user_set_color / palette_dark_set_color, then refreshes only page widgets (no runtime palette apply). Also updated dialog initialization to read from the current edit-source snapshot.
Added palette snapshot accessors (palette_user_colors, palette_dark_colors) in the palette API and implementation so Preferences can render from light/dark snapshots safely without mutating runtime palette state.
Runtime palette changes remain on the real apply paths (theme/dark-mode application), not edit-target toggles.
2026-02-25 22:48:55 -07:00
685989fa25 Updated fe_system_prefers_dark() to remove the old !has_theme_name gate and always read gtk-application-prefer-dark-theme whenever that property exists. Theme-name matching is still kept as an additional signal.
Added deterministic dark-mode precedence by combining explicit signals (theme_name_prefers_dark || property_prefers_dark), while preserving Windows system preference as another dark-enabling signal when available. This ensures “dark if any explicit dark signal is true.”
Re-checked the relevant callers (fe_auto_dark_mode_changed() and fe_apply_theme_for_mode()): they consume fe_system_prefers_dark() output and do not depend on the removed !has_theme_name conditional behavior.
2026-02-25 22:42:43 -07:00
cbc474477b Updated gtkutil_menu_icon_theme_variant() to select the menu icon theme variant from the app’s effective dark-mode state first (fe_dark_mode_state_is_initialized() + fe_dark_mode_is_enabled()), and only use GTK/theme-name heuristics as fallback before app state is initialized. This preserves the existing light-asset fallback behavior in menu icon loading logic.
Added a new frontend helper declaration fe_dark_mode_state_is_initialized() in the GTK frontend header so callers can check whether dark-mode state is ready.
Implemented dark-mode initialization tracking in fe-gtk.c via a new static flag, set when auto mode state is externally set and during fe_init(), and exposed it through fe_dark_mode_state_is_initialized().
2026-02-25 22:31:52 -07:00
51f8795d1a Updated create_input_style() so input selection CSS now prefers palette-derived COL_MARK_FG / COL_MARK_BG colors directly (as #RRGGBB from sel_* values), instead of defaulting to @theme_selected_*.
Added a guarded fallback path that only uses theme lookup/string conversion when palette selection components are invalid (isfinite checks fail), with palette values as secondary fallback there too. This keeps theme colors out of the primary path while preserving robustness for malformed data.
Kept needs_reload dependent on last_sel_* tracking, so changing selection colors in Preferences still triggers immediate CSS regeneration (including across light/dark mode).
2026-02-25 22:25:35 -07:00
faacd95dfc Added setup-local state for palette editing target (setup_color_edit_dark_palette) plus Light/Dark option strings, so palette editing is no longer coupled to runtime AUTO dark-mode detection. This keeps the choice in Preferences state only.
Added a new Editing: combo control near the dark-mode selector in setup_create_color_page, with a callback that switches between Light/Dark palette targets and refreshes swatches immediately using the same refresh path as dark-mode UI updates.
    Updated the dark-mode combo callback to refresh color swatches based on the explicit editing target (not runtime dark-mode detection), preserving existing runtime theme behavior while editing persisted overrides.
    Updated setup_color_response_cb to write color changes to palette_dark_set_color vs palette_user_set_color based on the new edit-target state.
    Initialized the editing target in setup_create_color_page from explicit Dark mode selection only (hex_gui_dark_mode == ZOITECHAT_DARK_MODE_DARK), avoiding AUTO runtime detection for editing context.
2026-02-25 21:28:08 -07:00
ed02b21228 Extended setup_dark_mode_menu_cb so after setup_menu_cb it now computes the selected dark-mode state via fe_dark_mode_is_enabled_for(setup_prefs.hex_gui_dark_mode), swaps the in-memory editor palette with palette_apply_dark_mode(...), and immediately refreshes color swatches. This keeps the editor model synchronized with the dark-mode combo selection.
Added setup_refresh_color_selector_widgets() to iterate color_selector_widgets, resolve each button’s stored color index, and re-run setup_color_button_apply against colors[] so all selectors repaint to the active palette.
    Updated setup_create_color_button to register each widget’s palette index (zoitechat-color-index), enabling correct per-button repaint during mode switches.
    Preserved existing persistence behavior in setup_color_response_cb (palette_dark_set_color vs palette_user_set_color) so changes remain non-destructive until existing OK/Save flow persists them.
2026-02-25 21:22:56 -07:00
d321717da8 Hardened create_input_style() so selection-color CSS no longer hard-depends on @theme_selected_*: it now first tries symbolic theme colors via gtk_style_context_lookup_color(), and only falls back when unavailable.
Added a fallback path that derives selection colors from palette values (COL_MARK_FG/COL_MARK_BG) and then samples selected-state colors from a GTK style context when possible, emitting explicit color strings for CSS.
Ensured fallback regeneration is tied to the same reload flow by extending needs_reload tracking with selection palette channels and persisting those last-seen values.
Kept Preferences > Colors foreground/background behavior intact for normal input rendering; only selection-specific CSS is conditionally overridden. Base text/background/caret rules remain unchanged while only text selection uses the new selection logic.
2026-02-25 20:59:07 -07:00
6310ab245c Updated fe_apply_theme_for_mode() to immediately reapply fe_apply_theme_to_toplevel() across all currently-open GTK toplevel windows (gtk_window_list_toplevels()), so mode switches update existing windows without reopening them.
Added/kept lifecycle call sites for main and detached/channel windows, with comments explaining that .zoitechat-dark / .zoitechat-light classes are required for GTK CSS selectors. This includes top/tab windows and server list/edit windows.
Wired dialog/toplevel creation paths in src/fe-gtk/ to call fe_apply_theme_to_toplevel() (without duplicating platform logic), including common prompt/message dialogs and feature-specific dialogs (join, notify, setup, ignore/ban, etc.), each with the requested brief comment near the new call site.
2026-02-25 20:39:48 -07:00
5952006662 Refactored fe_system_prefers_dark() to always allow gtk-application-prefer-dark-theme to be read as a fallback (when no explicit theme name is set), removing the previous suppression tied to app-written state. This preserves priority for explicit dark theme names and platform-native preference checks before the GTK property fallback.
Added a short in-code comment explaining why app-written GTK property state must not permanently block future reads in AUTO mode.
Removed the now-unneeded app_set_prefer_dark tracking behavior from fe_set_gtk_prefer_dark_theme(), so writes no longer disable later fallback reads.
Confirmed fe_auto_dark_mode_changed() still prevents reapply loops by early-returning when enabled == auto_dark_mode_enabled before calling theme/apply logic.
2026-02-25 20:32:44 -07:00
e4cb453915 Added a centralized theme-class application in gtkutil_window_new() so any toplevel created through this helper gets fe_apply_theme_to_toplevel() during construction. This covers most non-session windows consistently at creation time.
Updated Preferences window creation to explicitly apply the toplevel theme after gtk_widget_show_all() in setup_window_open(), matching your requested timing there.
Added explicit theme application for non-session windows that bypass gtkutil_window_new() and use direct gtk_window_new() in server list UI (editserv and servlist).
Extended mg_apply_setup() to iterate all current GTK toplevels (gtk_window_list_toplevels()) and reapply fe_apply_theme_to_toplevel() during setup/theme reapply flows, ensuring existing windows stay consistent after theme changes. Palette override widgets were not touched.
2026-02-25 20:27:15 -07:00
361e35de7f Updated chanview_apply_theme() in src/fe-gtk/chanview.c to apply the palette colors unconditionally (for tree view chanview), instead of branching on raw dark-mode enum checks. This keys behavior off the already-resolved palette output, which matches how setup_apply_to_sess() / palette_apply_dark_mode() flows are intended to work.
Preserved font handling exactly as before (input_style->font_desc when available).
Added inline rationale comment clarifying that AUTO/light should continue using palette-managed colors and should not revert to GTK theme defaults in a way that clears custom colors.
2026-02-25 20:23:01 -07:00
bbde2e5578 Added a new internal state flag, app_set_prefer_dark, and now set it whenever fe_set_gtk_prefer_dark_theme() writes gtk-application-prefer-dark-theme, so AUTO detection can distinguish app-authored writes from system/user signals.
Reworked fe_system_prefers_dark() to prioritize non-app-written sources in AUTO flow:

        first: gtk-theme-name dark-variant heuristic,

        then (Windows): native platform detection via fe_win32_try_get_system_dark,

        only then fallback to gtk-application-prefer-dark-theme when no theme-name signal was available and the app has not written that property.

    Kept manual LIGHT/DARK behavior unchanged by preserving the existing mode application path (fe_apply_theme_for_mode() still drives fe_set_gtk_prefer_dark_theme() directly).

    Verified fe_auto_dark_mode_changed() and fe_init() signal wiring still trigger AUTO palette/theme refreshes on system theme changes (notify::gtk-theme-name and existing notify callback flow remain in place).
2026-02-25 20:03:20 -07:00
97c6f36b20 Adjusted create_input_style so it no longer globally forces color on #zoitechat-inputbox, reducing over-constraining while still applying the user-selected background and caret color where intended. This keeps non-text-node state rendering more theme-driven.
Kept user override support for baseline text color and caret color on the input’s text node (#zoitechat-inputbox text).
Added targeted state-aware text rules for :focus and :backdrop (to preserve readability when theme state styling changes), plus a softened :disabled text rule instead of broad global color forcing.
Added explicit selection-node handling (#zoitechat-inputbox text selection) using theme selection tokens so selected text remains readable and aligned with Adwaita/Yaru-style theme behavior.
Preserved the existing Adwaita/Yaru background-image workaround logic, so fallback behavior remains compatible with those themes.
2026-02-25 19:43:19 -07:00
20 changed files with 1799 additions and 521 deletions

View File

@@ -419,6 +419,7 @@ const struct prefs vars[] =
{"gui_input_nick", P_OFFINT (hex_gui_input_nick), TYPE_BOOL},
{"gui_input_spell", P_OFFINT (hex_gui_input_spell), TYPE_BOOL},
{"gui_input_style", P_OFFINT (hex_gui_input_style), TYPE_BOOL},
{"gui_gtk3_theme_name", P_OFFSET (hex_gui_gtk3_theme_name), TYPE_STR},
{"gui_join_dialog", P_OFFINT (hex_gui_join_dialog), TYPE_BOOL},
{"gui_lagometer", P_OFFINT (hex_gui_lagometer), TYPE_INT},
{"gui_lang", P_OFFINT (hex_gui_lang), TYPE_INT},

View File

@@ -3774,37 +3774,29 @@ cmd_url (struct session *sess, char *tbuf, char *word[], char *word_eol[])
if (zoitechat_theme_path_from_arg (word[2], &theme_path))
{
GError *error = NULL;
char *basename = g_path_get_basename (theme_path);
char *dot = strrchr (basename, '.');
char *message;
char *theme_name = NULL;
if (dot)
*dot = '\0';
if (zoitechat_import_theme (theme_path, &error))
if (zoitechat_import_gtk3_theme_archive (theme_path, &theme_name, &error))
{
if (zoitechat_apply_theme (basename, &error))
if (theme_name)
{
message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename);
char *message = g_strdup_printf (_("GTK3 theme \"%s\" imported. Use Theme settings to apply it."), theme_name);
fe_message (message, FE_MSG_INFO);
handle_command (sess, "gui apply", FALSE);
g_free (message);
}
else
{
fe_message (error ? error->message : _("Theme imported, but failed to apply."),
FE_MSG_ERROR);
g_clear_error (&error);
fe_message (_("GTK3 theme imported. Use Theme settings to apply it."), FE_MSG_INFO);
}
}
else
{
fe_message (error ? error->message : _("Failed to import theme."),
fe_message (error ? error->message : _("Failed to import GTK3 theme archive."),
FE_MSG_ERROR);
g_clear_error (&error);
}
g_free (basename);
g_free (theme_name);
g_free (theme_path);
return TRUE;
}

View File

@@ -113,7 +113,7 @@ gboolean
zoitechat_theme_path_from_arg (const char *arg, char **path_out)
{
char *path = NULL;
const char *ext;
gboolean valid_ext = FALSE;
if (!arg)
return FALSE;
@@ -126,11 +126,21 @@ zoitechat_theme_path_from_arg (const char *arg, char **path_out)
if (!path)
return FALSE;
ext = strrchr (path, '.');
if (!g_file_test (path, G_FILE_TEST_IS_REGULAR) ||
!ext ||
(g_ascii_strcasecmp (ext, ".zct") != 0 &&
g_ascii_strcasecmp (ext, ".hct") != 0))
if (g_file_test (path, G_FILE_TEST_IS_REGULAR))
{
char *path_lower = g_ascii_strdown (path, -1);
valid_ext = g_str_has_suffix (path_lower, ".zip")
|| g_str_has_suffix (path_lower, ".tar")
|| g_str_has_suffix (path_lower, ".tar.gz")
|| g_str_has_suffix (path_lower, ".tgz")
|| g_str_has_suffix (path_lower, ".tar.xz")
|| g_str_has_suffix (path_lower, ".txz");
g_free (path_lower);
}
if (!valid_ext)
{
g_free (path);
return FALSE;
@@ -253,149 +263,444 @@ zoitechat_remote_win32 (void)
static gboolean
zoitechat_copy_theme_file (const char *src, const char *dest, GError **error)
zoitechat_is_safe_archive_entry (const char *entry)
{
char *data = NULL;
gsize len = 0;
char **parts;
gboolean safe = TRUE;
guint i;
if (!g_file_get_contents (src, &data, &len, error))
if (!entry || !*entry)
return FALSE;
if (!g_file_set_contents (dest, data, len, error))
{
g_free (data);
if (g_path_is_absolute (entry) || entry[0] == '/' || entry[0] == '\\')
return FALSE;
}
g_free (data);
return TRUE;
}
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."));
if (g_ascii_isalpha (entry[0]) && entry[1] == ':')
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))
parts = g_strsplit_set (entry, "/\\", -1);
for (i = 0; parts[i] != NULL; i++)
{
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)
if (strcmp (parts[i], "..") == 0)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Failed to remove existing event settings."));
goto cleanup;
safe = FALSE;
break;
}
}
ok = TRUE;
g_strfreev (parts);
return safe;
}
static gboolean
zoitechat_remove_tree (const char *path, GError **error)
{
GDir *dir;
const char *name;
if (!g_file_test (path, G_FILE_TEST_EXISTS))
return TRUE;
if (!g_file_test (path, G_FILE_TEST_IS_DIR))
return g_remove (path) == 0;
dir = g_dir_open (path, 0, error);
if (!dir)
return FALSE;
while ((name = g_dir_read_name (dir)))
{
char *child = g_build_filename (path, name, NULL);
if (!zoitechat_remove_tree (child, error))
{
g_free (child);
g_dir_close (dir);
return FALSE;
}
g_free (child);
}
g_dir_close (dir);
if (g_rmdir (path) != 0)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Failed to remove temporary directory."));
return FALSE;
}
return TRUE;
}
static gboolean
zoitechat_copy_directory_recursive (const char *src_dir, const char *dest_dir, GError **error)
{
GDir *dir;
const char *name;
if (g_mkdir_with_parents (dest_dir, 0700) != 0)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Failed to create destination theme directory."));
return FALSE;
}
dir = g_dir_open (src_dir, 0, error);
if (!dir)
return FALSE;
while ((name = g_dir_read_name (dir)))
{
char *src_path = g_build_filename (src_dir, name, NULL);
char *dest_path = g_build_filename (dest_dir, name, NULL);
if (g_file_test (src_path, G_FILE_TEST_IS_DIR))
{
if (!zoitechat_copy_directory_recursive (src_path, dest_path, error))
{
g_free (dest_path);
g_free (src_path);
g_dir_close (dir);
return FALSE;
}
}
else if (g_file_test (src_path, G_FILE_TEST_IS_REGULAR))
{
GFile *src_file = g_file_new_for_path (src_path);
GFile *dest_file = g_file_new_for_path (dest_path);
gboolean copied = g_file_copy (src_file, dest_file,
G_FILE_COPY_OVERWRITE,
NULL, NULL, NULL, error);
g_object_unref (dest_file);
g_object_unref (src_file);
if (!copied)
{
g_free (dest_path);
g_free (src_path);
g_dir_close (dir);
return FALSE;
}
}
g_free (dest_path);
g_free (src_path);
}
g_dir_close (dir);
return TRUE;
}
static gboolean
zoitechat_find_gtk3_theme_root (const char *search_dir,
char **theme_root_out,
gboolean *has_dark_css_out,
gboolean *missing_gtk_css_out,
GError **error)
{
GDir *dir;
const char *name;
dir = g_dir_open (search_dir, 0, error);
if (!dir)
return FALSE;
while ((name = g_dir_read_name (dir)))
{
char *child = g_build_filename (search_dir, name, NULL);
if (g_file_test (child, G_FILE_TEST_IS_DIR))
{
if (g_str_has_prefix (name, "gtk-3."))
{
char *gtk_css = g_build_filename (child, "gtk.css", NULL);
if (g_file_test (gtk_css, G_FILE_TEST_IS_REGULAR))
{
char *dark_css = g_build_filename (child, "gtk-dark.css", NULL);
if (theme_root_out)
*theme_root_out = g_strdup (search_dir);
if (has_dark_css_out)
*has_dark_css_out = g_file_test (dark_css, G_FILE_TEST_IS_REGULAR);
g_free (dark_css);
g_free (gtk_css);
g_free (child);
g_dir_close (dir);
return TRUE;
}
g_free (gtk_css);
if (missing_gtk_css_out)
*missing_gtk_css_out = TRUE;
}
else if (zoitechat_find_gtk3_theme_root (child,
theme_root_out,
has_dark_css_out,
missing_gtk_css_out,
error))
{
g_free (child);
g_dir_close (dir);
return TRUE;
}
if (error && *error)
{
g_free (child);
g_dir_close (dir);
return FALSE;
}
}
g_free (child);
}
g_dir_close (dir);
return FALSE;
}
typedef enum
{
ZOITECHAT_GTK3_ARCHIVE_UNKNOWN = 0,
ZOITECHAT_GTK3_ARCHIVE_ZIP,
ZOITECHAT_GTK3_ARCHIVE_TAR
} ZoiteChatGtk3ArchiveType;
static ZoiteChatGtk3ArchiveType
zoitechat_detect_gtk3_archive_type (const char *archive_path)
{
char *archive_path_lower;
ZoiteChatGtk3ArchiveType type = ZOITECHAT_GTK3_ARCHIVE_UNKNOWN;
if (!archive_path)
return ZOITECHAT_GTK3_ARCHIVE_UNKNOWN;
archive_path_lower = g_ascii_strdown (archive_path, -1);
if (g_str_has_suffix (archive_path_lower, ".zip"))
type = ZOITECHAT_GTK3_ARCHIVE_ZIP;
else if (g_str_has_suffix (archive_path_lower, ".tar")
|| g_str_has_suffix (archive_path_lower, ".tar.gz")
|| g_str_has_suffix (archive_path_lower, ".tgz")
|| g_str_has_suffix (archive_path_lower, ".tar.xz")
|| g_str_has_suffix (archive_path_lower, ".txz"))
type = ZOITECHAT_GTK3_ARCHIVE_TAR;
g_free (archive_path_lower);
return type;
}
#ifndef WIN32
static gboolean
zoitechat_validate_zip_entries_unix (const char *archive_path, char **archive_root_out, GError **error)
{
char *stdout_buf = NULL;
char *stderr_buf = NULL;
char *argv[] = {"unzip", "-Z1", (char *)archive_path, NULL};
char *archive_root = NULL;
char **lines;
gboolean ok;
int status = 0;
guint i;
ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH,
NULL, NULL, &stdout_buf, &stderr_buf, &status, error);
if (!ok)
goto cleanup;
if (!g_spawn_check_exit_status (status, error))
{
ok = FALSE;
goto cleanup;
}
lines = g_strsplit (stdout_buf ? stdout_buf : "", "\n", -1);
for (i = 0; lines[i] != NULL; i++)
{
const char *entry = lines[i];
char **parts;
if (!entry[0])
continue;
if (!zoitechat_is_safe_archive_entry (entry))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
_("Archive contains unsafe path: %s"), entry);
ok = FALSE;
break;
}
parts = g_strsplit (entry, "/", 2);
if (parts[0] && *parts[0])
{
if (!archive_root)
archive_root = g_strdup (parts[0]);
else if (strcmp (archive_root, parts[0]) != 0)
g_clear_pointer (&archive_root, g_free);
}
g_strfreev (parts);
}
g_strfreev (lines);
if (ok && archive_root_out)
*archive_root_out = g_strdup (archive_root);
cleanup:
g_free (events_dest);
g_free (events_src);
g_free (colors_dest);
g_free (colors_src);
g_free (theme_dir);
g_free (archive_root);
g_free (stderr_buf);
g_free (stdout_buf);
return ok;
}
#endif
static gboolean
zoitechat_validate_tar_entries_unix (const char *archive_path, char **archive_root_out, GError **error)
{
char *stdout_buf = NULL;
char *stderr_buf = NULL;
char *argv[] = {"tar", "-tf", (char *)archive_path, NULL};
char *archive_root = NULL;
char **lines;
gboolean ok;
int status = 0;
guint i;
ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH,
NULL, NULL, &stdout_buf, &stderr_buf, &status, error);
if (!ok)
goto cleanup;
if (!g_spawn_check_exit_status (status, error))
{
ok = FALSE;
goto cleanup;
}
lines = g_strsplit (stdout_buf ? stdout_buf : "", "\n", -1);
for (i = 0; lines[i] != NULL; i++)
{
const char *entry = lines[i];
char **parts;
if (!entry[0])
continue;
if (!zoitechat_is_safe_archive_entry (entry))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
_("Archive contains unsafe path: %s"), entry);
ok = FALSE;
break;
}
parts = g_strsplit_set (entry, "/\\", 2);
if (parts[0] && *parts[0])
{
if (!archive_root)
archive_root = g_strdup (parts[0]);
else if (strcmp (archive_root, parts[0]) != 0)
g_clear_pointer (&archive_root, g_free);
}
g_strfreev (parts);
}
g_strfreev (lines);
if (ok && archive_root_out)
*archive_root_out = g_strdup (archive_root);
cleanup:
g_free (archive_root);
g_free (stderr_buf);
g_free (stdout_buf);
return ok;
}
gboolean
zoitechat_import_theme (const char *path, GError **error)
zoitechat_import_gtk3_theme_archive (const char *archive_path,
char **theme_name_out,
GError **error)
{
char *themes_dir;
char *basename;
char *dot;
char *theme_dir;
char *argv[] = {"unzip", "-o", (char *)path, "-d", NULL, NULL};
ZoiteChatGtk3ArchiveType archive_type;
char *temp_dir = NULL;
char *archive_root = NULL;
char *theme_root = NULL;
char *theme_name = NULL;
char *store_dir = NULL;
char *dest_dir = NULL;
int status = 0;
gboolean ok;
#ifdef WIN32
char *command = NULL;
char *powershell = NULL;
#endif
gboolean ok = FALSE;
gboolean has_dark_css = FALSE;
gboolean missing_gtk_css = FALSE;
if (!path)
if (theme_name_out)
*theme_name_out = NULL;
if (!archive_path)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("No theme file specified."));
_("No GTK3 theme archive specified."));
return FALSE;
}
themes_dir = g_build_filename (get_xdir (), "themes", NULL);
basename = g_path_get_basename (path);
if (!basename || basename[0] == '\0')
archive_type = zoitechat_detect_gtk3_archive_type (archive_path);
if (archive_type == ZOITECHAT_GTK3_ARCHIVE_UNKNOWN)
{
g_free (themes_dir);
g_free (basename);
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Failed to determine theme name."));
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
_("Unsupported archive format. Use .zip, .tar, .tar.gz, .tgz, .tar.xz, or .txz."));
return FALSE;
}
dot = strrchr (basename, '.');
if (dot)
*dot = '\0';
theme_dir = g_build_filename (themes_dir, basename, NULL);
if (g_mkdir_with_parents (theme_dir, 0700) != 0)
{
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Failed to create theme directory."));
g_free (theme_dir);
char *basename = g_path_get_basename (archive_path);
char *dot;
if (basename && *basename)
{
dot = strrchr (basename, '.');
if (dot)
*dot = '\0';
if (g_str_has_suffix (basename, ".tar"))
basename[strlen (basename) - 4] = '\0';
if (*basename)
archive_root = g_strdup (basename);
}
g_free (basename);
g_free (themes_dir);
return FALSE;
}
temp_dir = g_dir_make_tmp ("zoitechat-gtk3-theme-XXXXXX", error);
if (!temp_dir)
return FALSE;
#ifdef WIN32
powershell = g_find_program_in_path ("powershell.exe");
if (!powershell)
powershell = g_find_program_in_path ("powershell");
if (!powershell)
{
g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT,
_("No archive extractor was found."));
ok = FALSE;
}
else
if (archive_type == ZOITECHAT_GTK3_ARCHIVE_ZIP)
{
char *powershell = NULL;
char *command = NULL;
GString *escaped_path = g_string_new ("'");
GString *escaped_dir = g_string_new ("'");
const char *cursor;
for (cursor = path; *cursor != '\0'; cursor++)
powershell = g_find_program_in_path ("powershell.exe");
if (!powershell)
powershell = g_find_program_in_path ("powershell");
if (!powershell)
{
g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT,
_("No archive extractor was found."));
g_string_free (escaped_path, TRUE);
g_string_free (escaped_dir, TRUE);
goto cleanup;
}
for (cursor = archive_path; *cursor != '\0'; cursor++)
{
if (*cursor == '\'')
g_string_append (escaped_path, "''");
@@ -404,7 +709,7 @@ zoitechat_import_theme (const char *path, GError **error)
}
g_string_append_c (escaped_path, '\'');
for (cursor = theme_dir; *cursor != '\0'; cursor++)
for (cursor = temp_dir; *cursor != '\0'; cursor++)
{
if (*cursor == '\'')
g_string_append (escaped_dir, "''");
@@ -414,25 +719,18 @@ zoitechat_import_theme (const char *path, GError **error)
g_string_append_c (escaped_dir, '\'');
command = g_strdup_printf (
"Add-Type -AssemblyName WindowsBase; "
"Add-Type -AssemblyName System.IO.Compression.FileSystem; "
"$ErrorActionPreference='Stop'; "
"$package=[System.IO.Packaging.Package]::Open(%s); "
"$archive=[System.IO.Compression.ZipFile]::OpenRead(%s); "
"try { "
"foreach ($part in $package.GetParts()) { "
"$relative=$part.Uri.OriginalString.TrimStart('/'); "
"if ([string]::IsNullOrEmpty($relative)) { continue }; "
"$destPath=[System.IO.Path]::Combine(%s, $relative); "
"$destDir=[System.IO.Path]::GetDirectoryName($destPath); "
"if ($destDir -and -not (Test-Path -LiteralPath $destDir)) { "
"[System.IO.Directory]::CreateDirectory($destDir) | Out-Null "
"foreach ($entry in $archive.Entries) { "
"$name=$entry.FullName; "
"if ([string]::IsNullOrEmpty($name)) { continue }; "
"if ([System.IO.Path]::IsPathRooted($name) -or $name.StartsWith('/') -or $name.StartsWith('\\') -or $name.Contains('..\\') -or $name.Contains('../')) { throw 'Archive contains unsafe path: ' + $name } "
"}; "
"$partStream=$part.GetStream(); "
"$fileStream=[System.IO.File]::Open($destPath,[System.IO.FileMode]::Create,[System.IO.FileAccess]::Write); "
"$partStream.CopyTo($fileStream); "
"$fileStream.Dispose(); "
"$partStream.Dispose(); "
"} "
"} finally { $package.Close(); }",
"[System.IO.Compression.ZipFile]::ExtractToDirectory(%s,%s,$true); "
"} finally { $archive.Dispose(); }",
escaped_path->str,
escaped_path->str,
escaped_dir->str);
g_string_free (escaped_path, TRUE);
@@ -443,47 +741,141 @@ zoitechat_import_theme (const char *path, GError **error)
ok = g_spawn_sync (NULL, ps_argv, NULL, 0, NULL, NULL,
NULL, NULL, &status, error);
}
if (ok)
ok = g_spawn_check_exit_status (status, error);
g_free (command);
g_free (powershell);
if (!ok)
goto cleanup;
}
else
{
char *argv[] = {"tar", "-xf", (char *)archive_path, "-C", temp_dir, NULL};
char *extracted_root = NULL;
if (!zoitechat_validate_tar_entries_unix (archive_path, &extracted_root, error))
goto cleanup;
if (extracted_root)
{
g_free (archive_root);
archive_root = extracted_root;
}
ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, NULL, &status, error);
if (!ok)
goto cleanup;
if (!g_spawn_check_exit_status (status, error))
goto cleanup;
}
#else
argv[4] = theme_dir;
ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, NULL, &status, error);
#endif
if (!ok)
if (archive_type == ZOITECHAT_GTK3_ARCHIVE_ZIP)
{
#ifdef WIN32
g_free (command);
g_free (powershell);
char *argv[] = {"unzip", "-o", (char *)archive_path, "-d", temp_dir, NULL};
char *extracted_root = NULL;
if (!zoitechat_validate_zip_entries_unix (archive_path, &extracted_root, error))
goto cleanup;
if (extracted_root)
{
g_free (archive_root);
archive_root = extracted_root;
}
ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, NULL, &status, error);
if (!ok)
goto cleanup;
if (!g_spawn_check_exit_status (status, error))
goto cleanup;
}
else
{
char *argv[] = {"tar", "-xf", (char *)archive_path, "-C", temp_dir, NULL};
char *extracted_root = NULL;
if (!zoitechat_validate_tar_entries_unix (archive_path, &extracted_root, error))
goto cleanup;
if (extracted_root)
{
g_free (archive_root);
archive_root = extracted_root;
}
ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, NULL, &status, error);
if (!ok)
goto cleanup;
if (!g_spawn_check_exit_status (status, error))
goto cleanup;
}
#endif
g_free (theme_dir);
g_free (basename);
g_free (themes_dir);
return FALSE;
if (!zoitechat_find_gtk3_theme_root (temp_dir, &theme_root, &has_dark_css, &missing_gtk_css, error))
{
if (error && *error)
goto cleanup;
if (missing_gtk_css)
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
_("Archive contains a gtk-3.x directory, but gtk.css is missing."));
else
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
_("Archive is not a GTK3 theme. Expected layout: <theme-name>/gtk-3.x/gtk.css."));
goto cleanup;
}
if (!g_spawn_check_exit_status (status, error))
theme_name = g_path_get_basename (theme_root);
if (!theme_name || !*theme_name)
{
#ifdef WIN32
g_free (command);
g_free (powershell);
#endif
g_free (theme_dir);
g_free (basename);
g_free (themes_dir);
return FALSE;
g_clear_pointer (&theme_name, g_free);
if (archive_root)
theme_name = g_strdup (archive_root);
}
#ifdef WIN32
g_free (command);
g_free (powershell);
#endif
if (!theme_name || !*theme_name)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
_("Unable to determine GTK3 theme directory name from archive."));
goto cleanup;
}
g_free (theme_dir);
g_free (basename);
g_free (themes_dir);
return TRUE;
store_dir = g_build_filename (get_xdir (), "gtk3-themes", NULL);
dest_dir = g_build_filename (store_dir, theme_name, NULL);
if (g_file_test (dest_dir, G_FILE_TEST_EXISTS) && !zoitechat_remove_tree (dest_dir, error))
goto cleanup;
if (!zoitechat_copy_directory_recursive (theme_root, dest_dir, error))
goto cleanup;
if (has_dark_css)
fe_message (_("Imported GTK3 theme includes gtk-dark.css."), FE_MSG_INFO);
if (theme_name_out)
*theme_name_out = g_strdup (theme_name);
ok = TRUE;
cleanup:
if (temp_dir)
{
GError *cleanup_error = NULL;
if (!zoitechat_remove_tree (temp_dir, &cleanup_error))
g_clear_error (&cleanup_error);
}
g_free (dest_dir);
g_free (store_dir);
g_free (theme_name);
g_free (theme_root);
g_free (archive_root);
g_free (temp_dir);
return ok;
}
/*
* Update the priority queue of the "interesting sessions"
* (sess_list_by_lastact).
@@ -812,37 +1204,29 @@ irc_init (session *sess)
if (zoitechat_theme_path_from_arg (arg_url, &theme_path))
{
GError *error = NULL;
char *basename = g_path_get_basename (theme_path);
char *dot = strrchr (basename, '.');
char *message;
char *theme_name = NULL;
if (dot)
*dot = '\0';
if (zoitechat_import_theme (theme_path, &error))
if (zoitechat_import_gtk3_theme_archive (theme_path, &theme_name, &error))
{
if (zoitechat_apply_theme (basename, &error))
if (theme_name)
{
message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename);
char *message = g_strdup_printf (_("GTK3 theme \"%s\" imported. Use Theme settings to apply it."), theme_name);
fe_message (message, FE_MSG_INFO);
handle_command (sess, "gui apply", FALSE);
g_free (message);
}
else
{
fe_message (error ? error->message : _("Theme imported, but failed to apply."),
FE_MSG_ERROR);
g_clear_error (&error);
fe_message (_("GTK3 theme imported. Use Theme settings to apply it."), FE_MSG_INFO);
}
}
else
{
fe_message (error ? error->message : _("Failed to import theme."),
fe_message (error ? error->message : _("Failed to import GTK3 theme archive."),
FE_MSG_ERROR);
g_clear_error (&error);
}
g_free (basename);
g_free (theme_name);
}
else
{
@@ -864,37 +1248,29 @@ irc_init (session *sess)
if (zoitechat_theme_path_from_arg (arg_urls[i], &theme_path))
{
GError *error = NULL;
char *basename = g_path_get_basename (theme_path);
char *dot = strrchr (basename, '.');
char *message;
char *theme_name = NULL;
if (dot)
*dot = '\0';
if (zoitechat_import_theme (theme_path, &error))
if (zoitechat_import_gtk3_theme_archive (theme_path, &theme_name, &error))
{
if (zoitechat_apply_theme (basename, &error))
if (theme_name)
{
message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename);
char *message = g_strdup_printf (_("GTK3 theme \"%s\" imported. Use Theme settings to apply it."), theme_name);
fe_message (message, FE_MSG_INFO);
handle_command (sess, "gui apply", FALSE);
g_free (message);
}
else
{
fe_message (error ? error->message : _("Theme imported, but failed to apply."),
FE_MSG_ERROR);
g_clear_error (&error);
fe_message (_("GTK3 theme imported. Use Theme settings to apply it."), FE_MSG_INFO);
}
}
else
{
fe_message (error ? error->message : _("Failed to import theme."),
fe_message (error ? error->message : _("Failed to import GTK3 theme archive."),
FE_MSG_ERROR);
g_clear_error (&error);
}
g_free (basename);
g_free (theme_name);
}
else
{
@@ -1477,6 +1853,10 @@ main (int argc, char *argv[])
{
int i;
int ret;
#ifdef WIN32
char **win32_argv = NULL;
int win32_argc;
#endif
#ifdef WIN32
HRESULT coinit_result;
@@ -1484,6 +1864,23 @@ main (int argc, char *argv[])
srand ((unsigned int) time (NULL)); /* CL: do this only once! */
#ifdef WIN32
win32_argv = g_win32_get_command_line ();
if (win32_argv != NULL)
{
win32_argc = g_strv_length (win32_argv);
if (win32_argc == 0 || win32_argv[0] == NULL || win32_argv[0][0] == '\0')
{
g_strfreev (win32_argv);
win32_argv = g_new0 (char *, 2);
win32_argv[0] = g_strdup ("zoitechat");
win32_argc = 1;
}
argv = win32_argv;
argc = win32_argc;
}
#endif
/* We must check for the config dir parameter, otherwise load_config() will behave incorrectly.
* load_config() must come before fe_args() because fe_args() calls gtk_init() which needs to
* know the language which is set in the config. The code below is copy-pasted from fe_args()
@@ -1534,11 +1931,19 @@ main (int argc, char *argv[])
ret = fe_args (argc, argv);
if (ret != -1)
{
#ifdef WIN32
g_strfreev (win32_argv);
#endif
return ret;
}
#ifdef WIN32
if (zoitechat_remote_win32 ())
{
g_strfreev (win32_argv);
return 0;
}
#endif
#ifdef USE_DBUS
@@ -1591,5 +1996,9 @@ main (int argc, char *argv[])
WSACleanup ();
#endif
#ifdef WIN32
g_strfreev (win32_argv);
#endif
return 0;
}

View File

@@ -30,8 +30,10 @@
#define ZOITECHAT_H
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);
/* Imports a GTK3 theme archive into ZoiteChat's own gtk3-themes store. */
gboolean zoitechat_import_gtk3_theme_archive (const char *archive_path,
char **theme_name_out,
GError **error);
#ifdef USE_OPENSSL
#ifdef __APPLE__
@@ -295,6 +297,7 @@ struct zoitechatprefs
char hex_dcc_completed_dir[PATHLEN + 1];
char hex_dcc_dir[PATHLEN + 1];
char hex_dcc_ip[DOMAINLEN + 1];
char hex_gui_gtk3_theme_name[128];
char hex_gui_ulist_doubleclick[256];
char hex_input_command_char[4];
char hex_irc_extra_hilight[300];

View File

@@ -562,7 +562,10 @@ banlist_clear (GtkWidget * wid, banlist_info *banl)
dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
_("Are you sure you want to remove all listed items in %s?"), banl->sess->channel);
_("Are you sure you want to remove all listed items in %s?"), banl->sess->channel);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (banlist_clear_cb), banl);

View File

@@ -127,15 +127,15 @@ 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);
}
/*
* setup_apply_to_sess() and palette_apply_dark_mode() treat all dark-mode
* preference modes as palette-driven: dark uses curated dark colors, while
* light/auto-light use the user's saved palette.
*
* Keep chanview aligned with that resolved behavior so AUTO doesn't
* accidentally revert to theme defaults and clear custom colors.
*/
gtkutil_apply_palette (w, &colors[COL_BG], &colors[COL_FG], font);
}
static char *

View File

@@ -19,6 +19,7 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include "fe-gtk.h"
@@ -105,6 +106,9 @@ create_msg_dialog (gchar *title, gchar *message)
GtkWidget *dialog;
dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
gtk_window_set_title (GTK_WINDOW (dialog), title);
/* On Win32 we automatically have the icon. If we try to load it explicitly, it will look ugly for some reason. */
@@ -406,7 +410,12 @@ fe_args (int argc, char *argv[])
/* of the exe. */
{
g_free (win32_argv0_dir);
win32_argv0_dir = g_path_get_dirname (argv[0]);
/* In subsystem:windows builds, argv can be absent/invalid depending on
* launch context (e.g. shell URL handlers). Prefer the module path,
* then only fall back to argv[0] when it is available. */
win32_argv0_dir = g_win32_get_package_installation_directory_of_module (NULL);
if (win32_argv0_dir == NULL && argc > 0 && argv != NULL && argv[0] != NULL)
win32_argv0_dir = g_path_get_dirname (argv[0]);
if (win32_argv0_dir)
chdir (win32_argv0_dir);
}
@@ -537,10 +546,13 @@ static gboolean
fe_system_prefers_dark (void)
{
GtkSettings *settings = gtk_settings_get_default ();
gboolean theme_name_prefers_dark = FALSE;
gboolean property_prefers_dark = FALSE;
gboolean prefer_dark = FALSE;
char *theme_name = NULL;
#ifdef G_OS_WIN32
gboolean have_win_pref = FALSE;
gboolean win_prefers_dark = FALSE;
if (fe_win32_high_contrast_is_enabled ())
return FALSE;
@@ -549,37 +561,336 @@ fe_system_prefers_dark (void)
if (!settings)
return FALSE;
#ifdef G_OS_WIN32
have_win_pref = fe_win32_try_get_system_dark (&prefer_dark);
if (!have_win_pref)
#endif
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"))
theme_name_prefers_dark = TRUE;
g_free (lower);
g_free (theme_name);
}
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);
/* Even if we last wrote this property, the toolkit or desktop can update
* it later, so AUTO mode should keep reading it as a signal. */
g_object_get (settings, "gtk-application-prefer-dark-theme", &property_prefers_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);
}
}
#ifdef G_OS_WIN32
have_win_pref = fe_win32_try_get_system_dark (&win_prefers_dark);
#endif
/* Deterministic precedence: any explicit dark signal wins. */
prefer_dark = theme_name_prefers_dark || property_prefers_dark;
#ifdef G_OS_WIN32
prefer_dark = prefer_dark || (have_win_pref && win_prefers_dark);
#endif
return prefer_dark;
}
static gboolean auto_dark_mode_enabled = FALSE;
static gboolean dark_mode_state_initialized = FALSE;
static gboolean
fe_parse_gtk3_minor_from_dirname (const char *name, gint *minor_out)
{
const char *prefix = "gtk-3.";
char *endptr = NULL;
long value;
if (!name || !g_str_has_prefix (name, prefix))
return FALSE;
value = strtol (name + strlen (prefix), &endptr, 10);
if (endptr == name + strlen (prefix) || *endptr != '\0' || value < 0 || value > G_MAXINT)
return FALSE;
if (minor_out)
*minor_out = (gint) value;
return TRUE;
}
gboolean
fe_resolve_gtk3_theme_dir (const char *theme_root,
char **gtk3_dir_out,
gboolean *has_dark_css_out)
{
GDir *dir;
const char *entry;
gint runtime_minor = gtk_get_minor_version ();
gint best_minor = -1;
gint best_distance = G_MAXINT;
char *best_dir = NULL;
gboolean has_dark = FALSE;
if (gtk3_dir_out)
*gtk3_dir_out = NULL;
if (has_dark_css_out)
*has_dark_css_out = FALSE;
if (!theme_root || !*theme_root)
return FALSE;
dir = g_dir_open (theme_root, 0, NULL);
if (!dir)
return FALSE;
while ((entry = g_dir_read_name (dir)) != NULL)
{
char *candidate_dir;
char *gtk_css;
char *gtk_dark_css;
gint minor;
gint distance;
if (!fe_parse_gtk3_minor_from_dirname (entry, &minor))
continue;
candidate_dir = g_build_filename (theme_root, entry, NULL);
if (!g_file_test (candidate_dir, G_FILE_TEST_IS_DIR))
{
g_free (candidate_dir);
continue;
}
gtk_css = g_build_filename (candidate_dir, "gtk.css", NULL);
if (!g_file_test (gtk_css, G_FILE_TEST_IS_REGULAR))
{
g_free (gtk_css);
g_free (candidate_dir);
continue;
}
g_free (gtk_css);
distance = (minor <= runtime_minor)
? (runtime_minor - minor)
: (10000 + (minor - runtime_minor));
if (!best_dir || distance < best_distance || (distance == best_distance && minor > best_minor))
{
g_free (best_dir);
best_dir = candidate_dir;
best_minor = minor;
best_distance = distance;
gtk_dark_css = g_build_filename (best_dir, "gtk-dark.css", NULL);
has_dark = g_file_test (gtk_dark_css, G_FILE_TEST_IS_REGULAR);
g_free (gtk_dark_css);
candidate_dir = NULL;
}
g_free (candidate_dir);
}
g_dir_close (dir);
if (!best_dir)
return FALSE;
if (gtk3_dir_out)
*gtk3_dir_out = g_strdup (best_dir);
if (has_dark_css_out)
*has_dark_css_out = has_dark;
g_free (best_dir);
return TRUE;
}
static GtkCssProvider *gtk3_theme_provider = NULL;
static char *gtk3_theme_provider_name = NULL;
static gboolean gtk3_theme_provider_dark = FALSE;
static GResource *gtk3_theme_resource = NULL;
static char *gtk3_theme_resource_path = NULL;
#ifdef G_OS_WIN32
static void fe_apply_windows_theme (gboolean dark);
#endif
static void
fe_apply_windows_theme (gboolean dark)
fe_gtk3_theme_unregister_resource (void)
{
if (gtk3_theme_resource)
{
g_resources_unregister (gtk3_theme_resource);
g_clear_pointer (&gtk3_theme_resource, g_resource_unref);
}
g_clear_pointer (&gtk3_theme_resource_path, g_free);
}
static gboolean
fe_gtk3_theme_register_resource (const char *resource_path, GError **error)
{
GResource *resource;
if (!resource_path || !*resource_path)
{
fe_gtk3_theme_unregister_resource ();
return TRUE;
}
if (gtk3_theme_resource && gtk3_theme_resource_path
&& g_strcmp0 (gtk3_theme_resource_path, resource_path) == 0)
return TRUE;
resource = g_resource_load (resource_path, error);
if (!resource)
return FALSE;
fe_gtk3_theme_unregister_resource ();
g_resources_register (resource);
gtk3_theme_resource = resource;
gtk3_theme_resource_path = g_strdup (resource_path);
return TRUE;
}
gboolean
fe_apply_gtk3_theme_with_reload (const char *theme_name, gboolean force_reload, GError **error)
{
GdkScreen *screen = gdk_screen_get_default ();
char *theme_dir = NULL;
char *gtk3_dir = NULL;
char *gtk_css = NULL;
char *gtk_dark_css = NULL;
char *gtk_resource = NULL;
const char *selected_css = NULL;
gboolean dark = fe_dark_mode_is_enabled ();
GList *toplevels, *node;
if (!theme_name || !*theme_name)
{
#ifdef G_OS_WIN32
/* Keep the Win32 fallback provider in sync when returning to the
* system theme from Preferences > Themes. */
fe_apply_windows_theme (dark);
#endif
if (gtk3_theme_provider && screen)
{
gtk_style_context_remove_provider_for_screen (
screen,
GTK_STYLE_PROVIDER (gtk3_theme_provider));
gtk_style_context_reset_widgets (screen);
}
g_clear_object (&gtk3_theme_provider);
g_clear_pointer (&gtk3_theme_provider_name, g_free);
gtk3_theme_provider_dark = FALSE;
fe_gtk3_theme_unregister_resource ();
toplevels = gtk_window_list_toplevels ();
for (node = toplevels; node; node = node->next)
fe_apply_theme_to_toplevel (GTK_WIDGET (node->data));
g_list_free (toplevels);
return TRUE;
}
if (!force_reload
&& gtk3_theme_provider_name
&& g_strcmp0 (gtk3_theme_provider_name, theme_name) == 0
&& gtk3_theme_provider_dark == dark)
{
return TRUE;
}
theme_dir = g_build_filename (get_xdir (), "gtk3-themes", theme_name, NULL);
if (!fe_resolve_gtk3_theme_dir (theme_dir, &gtk3_dir, NULL))
{
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
_("GTK3 theme '%s' is missing a valid gtk-3.x/gtk.css directory."), theme_name);
g_free (theme_dir);
return FALSE;
}
gtk_css = g_build_filename (gtk3_dir, "gtk.css", NULL);
gtk_dark_css = g_build_filename (gtk3_dir, "gtk-dark.css", NULL);
gtk_resource = g_build_filename (gtk3_dir, "gtk.gresource", NULL);
if (dark && g_file_test (gtk_dark_css, G_FILE_TEST_IS_REGULAR))
selected_css = gtk_dark_css;
else
selected_css = gtk_css;
if (g_file_test (gtk_resource, G_FILE_TEST_IS_REGULAR))
{
if (!fe_gtk3_theme_register_resource (gtk_resource, error))
{
g_free (gtk_resource);
g_free (gtk_dark_css);
g_free (gtk_css);
g_free (gtk3_dir);
g_free (theme_dir);
return FALSE;
}
}
else
{
fe_gtk3_theme_unregister_resource ();
}
if (!gtk3_theme_provider)
gtk3_theme_provider = gtk_css_provider_new ();
if (!gtk_css_provider_load_from_path (gtk3_theme_provider, selected_css, error))
{
g_free (gtk_dark_css);
g_free (gtk_css);
g_free (gtk_resource);
g_free (gtk3_dir);
g_free (theme_dir);
return FALSE;
}
if (screen)
{
gtk_style_context_add_provider_for_screen (
screen,
GTK_STYLE_PROVIDER (gtk3_theme_provider),
/* Use user priority so imported GTK3 themes can also override
* ZoiteChat's own application CSS (for example, button backgrounds). */
GTK_STYLE_PROVIDER_PRIORITY_USER);
gtk_style_context_reset_widgets (screen);
}
#ifdef G_OS_WIN32
/* Applying a GTK3 theme should immediately disable ZoiteChat's Win32
* fallback window CSS so buttons/chat widgets are fully theme-driven. */
fe_apply_windows_theme (dark);
#endif
g_free (gtk3_theme_provider_name);
gtk3_theme_provider_name = g_strdup (theme_name);
gtk3_theme_provider_dark = dark;
toplevels = gtk_window_list_toplevels ();
for (node = toplevels; node; node = node->next)
fe_apply_theme_to_toplevel (GTK_WIDGET (node->data));
g_list_free (toplevels);
g_free (gtk_dark_css);
g_free (gtk_css);
g_free (gtk_resource);
g_free (gtk3_dir);
g_free (theme_dir);
return TRUE;
}
gboolean
fe_apply_gtk3_theme (const char *theme_name, GError **error)
{
return fe_apply_gtk3_theme_with_reload (theme_name, FALSE, error);
}
static void
fe_set_gtk_prefer_dark_theme (gboolean dark)
{
GtkSettings *settings = gtk_settings_get_default ();
@@ -588,24 +899,94 @@ fe_apply_windows_theme (gboolean dark)
{
g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL);
}
}
#ifdef G_OS_WIN32
static void
fe_append_window_theme_class_css (GString *css,
const char *class_name,
const PaletteColor *fg,
const PaletteColor *bg)
{
char *fg_css = gdk_rgba_to_string (fg);
char *bg_css = gdk_rgba_to_string (bg);
g_string_append_printf (css,
"window.%s, .%s {"
"background-color: %s;"
"color: %s;"
"}"
"window.%s button, .%s button, window.%s entry, .%s entry, "
"window.%s treeview, .%s treeview, window.%s scrolledwindow, .%s scrolledwindow {"
"background-color: %s;"
"color: %s;"
"}",
class_name,
class_name,
class_name,
class_name,
class_name,
class_name,
class_name,
class_name,
class_name,
class_name,
bg_css,
fg_css);
g_free (fg_css);
g_free (bg_css);
}
static void
fe_apply_windows_theme (gboolean dark)
{
static GtkCssProvider *win_theme_provider = NULL;
fe_set_gtk_prefer_dark_theme (dark);
{
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;"
"}";
const PaletteColor *light_palette = palette_user_colors ();
const PaletteColor *dark_palette = palette_dark_colors ();
GString *css;
/* Let imported GTK3 themes own all widget/window colors on Windows.
* Otherwise ZoiteChat's fallback dark/light window background CSS can
* clash with theme widget colors (for example white buttons on a dark
* window background).
*
* Use the active provider state (not only the configured preference): if
* a configured theme fails to load we still want fallback palette CSS so
* the app keeps a coherent dark/light appearance on Windows releases.
*/
if (gtk3_theme_provider != NULL)
{
if (win_theme_provider && screen)
{
gtk_style_context_remove_provider_for_screen (
screen,
GTK_STYLE_PROVIDER (win_theme_provider));
gtk_style_context_reset_widgets (screen);
}
return;
}
css = g_string_new (NULL);
if (!win_theme_provider)
win_theme_provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL);
fe_append_window_theme_class_css (css,
"zoitechat-dark",
&dark_palette[COL_FG],
&dark_palette[COL_BG]);
fe_append_window_theme_class_css (css,
"zoitechat-light",
&light_palette[COL_FG],
&light_palette[COL_BG]);
gtk_css_provider_load_from_data (win_theme_provider, css->str, -1, NULL);
g_string_free (css, TRUE);
if (screen)
gtk_style_context_add_provider_for_screen (
screen,
@@ -640,6 +1021,13 @@ void
fe_set_auto_dark_mode_state (gboolean enabled)
{
auto_dark_mode_enabled = enabled;
dark_mode_state_initialized = TRUE;
}
gboolean
fe_dark_mode_state_is_initialized (void)
{
return dark_mode_state_initialized;
}
void
@@ -652,6 +1040,11 @@ gboolean
fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed)
{
gboolean enabled = fe_dark_mode_is_enabled_for (mode);
GList *toplevels, *node;
/* Apply the optional global GTK preference first, then reapply palette-driven
* chat/input colors so Preferences > Colors continues to take precedence. */
fe_set_gtk_prefer_dark_theme (enabled);
gboolean changed = palette_apply_dark_mode (enabled);
#ifdef G_OS_WIN32
@@ -664,30 +1057,42 @@ fe_apply_theme_for_mode (unsigned int mode, gboolean *palette_changed)
if (input_style)
create_input_style (input_style);
if (!fe_apply_gtk3_theme (prefs.hex_gui_gtk3_theme_name, NULL)
&& prefs.hex_gui_gtk3_theme_name[0] != '\0')
{
fe_message (_("Failed to apply configured GTK3 theme from gtk3-themes."), FE_MSG_ERROR);
}
/* Existing toplevel windows also need the class refreshed for selectors like
* .zoitechat-dark / .zoitechat-light to update immediately. */
toplevels = gtk_window_list_toplevels ();
for (node = toplevels; node; node = node->next)
fe_apply_theme_to_toplevel (GTK_WIDGET (node->data));
g_list_free (toplevels);
return enabled;
}
void
fe_apply_theme_to_toplevel (GtkWidget *window)
{
GtkStyleContext *context;
gboolean dark;
if (!window)
return;
#ifdef G_OS_WIN32
context = gtk_widget_get_style_context (window);
dark = fe_dark_mode_is_enabled ();
if (context)
{
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");
}
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 ());
fe_win32_apply_native_titlebar (window, dark);
}
gboolean
@@ -727,6 +1132,12 @@ create_input_style (InputStyle *style)
static guint16 last_bg_red;
static guint16 last_bg_green;
static guint16 last_bg_blue;
static guint16 last_sel_fg_red;
static guint16 last_sel_fg_green;
static guint16 last_sel_fg_blue;
static guint16 last_sel_bg_red;
static guint16 last_sel_bg_green;
static guint16 last_sel_bg_blue;
if (!style)
style = g_new0 (InputStyle, 1);
@@ -758,6 +1169,12 @@ create_input_style (InputStyle *style)
guint16 bg_red;
guint16 bg_green;
guint16 bg_blue;
guint16 sel_fg_red;
guint16 sel_fg_green;
guint16 sel_fg_blue;
guint16 sel_bg_red;
guint16 sel_bg_green;
guint16 sel_bg_blue;
gboolean dark_mode = fe_dark_mode_is_enabled ();
gboolean needs_reload;
@@ -765,6 +1182,8 @@ create_input_style (InputStyle *style)
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);
palette_color_get_rgb16 (&colors[COL_MARK_FG], &sel_fg_red, &sel_fg_green, &sel_fg_blue);
palette_color_get_rgb16 (&colors[COL_MARK_BG], &sel_bg_red, &sel_bg_green, &sel_bg_blue);
needs_reload = !done_rc
|| !last_input_style
|| last_dark_mode != dark_mode
@@ -775,7 +1194,13 @@ create_input_style (InputStyle *style)
|| last_fg_blue != fg_blue
|| last_bg_red != bg_red
|| last_bg_green != bg_green
|| last_bg_blue != bg_blue;
|| last_bg_blue != bg_blue
|| last_sel_fg_red != sel_fg_red
|| last_sel_fg_green != sel_fg_green
|| last_sel_fg_blue != sel_fg_blue
|| last_sel_bg_red != sel_bg_red
|| last_sel_bg_green != sel_bg_green
|| last_sel_bg_blue != sel_bg_blue;
if (needs_reload)
{
@@ -799,28 +1224,107 @@ create_input_style (InputStyle *style)
}
{
GString *css = g_string_new ("#zoitechat-inputbox {");
GtkWidget *tmp_entry = NULL;
GtkStyleContext *tmp_context = NULL;
GdkRGBA selected_fg = { 0.0, 0.0, 0.0, 1.0 };
GdkRGBA selected_bg = { 0.0, 0.0, 0.0, 1.0 };
gboolean have_palette_selected_colors;
const char *selection_fg_css = NULL;
const char *selection_bg_css = NULL;
char selection_fg_hex[8];
char selection_bg_hex[8];
char *selection_fg_fallback = NULL;
char *selection_bg_fallback = NULL;
/* GTK3 equivalents for adwaita_workaround_rc/cursor_color_rc. */
if (adwaita_workaround_rc[0] != '\0'
&& theme_name
&& (g_str_has_prefix (theme_name, "Adwaita")
|| g_str_has_prefix (theme_name, "Yaru")))
g_string_append (css, "background-image: none;");
have_palette_selected_colors =
isfinite (colors[COL_MARK_FG].red)
&& isfinite (colors[COL_MARK_FG].green)
&& isfinite (colors[COL_MARK_FG].blue)
&& isfinite (colors[COL_MARK_BG].red)
&& isfinite (colors[COL_MARK_BG].green)
&& isfinite (colors[COL_MARK_BG].blue);
if (have_palette_selected_colors)
{
g_snprintf (selection_fg_hex, sizeof (selection_fg_hex), "#%02x%02x%02x",
(sel_fg_red >> 8), (sel_fg_green >> 8), (sel_fg_blue >> 8));
g_snprintf (selection_bg_hex, sizeof (selection_bg_hex), "#%02x%02x%02x",
(sel_bg_red >> 8), (sel_bg_green >> 8), (sel_bg_blue >> 8));
selection_fg_css = selection_fg_hex;
selection_bg_css = selection_bg_hex;
}
else
{
tmp_entry = gtk_entry_new ();
tmp_context = tmp_entry ? gtk_widget_get_style_context (tmp_entry) : NULL;
if (tmp_context)
{
if (!gtk_style_context_lookup_color (
tmp_context,
"theme_selected_fg_color",
&selected_fg))
selected_fg = colors[COL_MARK_FG];
if (!gtk_style_context_lookup_color (
tmp_context,
"theme_selected_bg_color",
&selected_bg))
selected_bg = colors[COL_MARK_BG];
}
else
{
selected_fg = colors[COL_MARK_FG];
selected_bg = colors[COL_MARK_BG];
}
selection_fg_fallback = gdk_rgba_to_string (&selected_fg);
selection_bg_fallback = gdk_rgba_to_string (&selected_bg);
selection_fg_css = selection_fg_fallback ? selection_fg_fallback : "@theme_selected_fg_color";
selection_bg_css = selection_bg_fallback ? selection_bg_fallback : "@theme_selected_bg_color";
}
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;"
"}"
"#zoitechat-inputbox:focus text,"
"#zoitechat-inputbox:backdrop text {"
"color: #%02x%02x%02x;"
"caret-color: %s;"
"}"
"#zoitechat-inputbox:disabled text {"
"color: alpha(#%02x%02x%02x, 0.7);"
"}"
"#zoitechat-inputbox text selection {"
"color: %s;"
"background-color: %s;"
"}",
(bg_red >> 8), (bg_green >> 8), (bg_blue >> 8),
cursor_color,
(fg_red >> 8), (fg_green >> 8), (fg_blue >> 8),
cursor_color,
(fg_red >> 8), (fg_green >> 8), (fg_blue >> 8),
cursor_color);
cursor_color,
(fg_red >> 8), (fg_green >> 8), (fg_blue >> 8),
selection_fg_css,
selection_bg_css);
if (tmp_entry)
gtk_widget_destroy (tmp_entry);
g_clear_pointer (&selection_fg_fallback, g_free);
g_clear_pointer (&selection_bg_fallback, g_free);
gtk_css_provider_load_from_data (input_css_provider, css->str, -1, NULL);
g_string_free (css, TRUE);
}
@@ -841,6 +1345,12 @@ create_input_style (InputStyle *style)
last_bg_red = bg_red;
last_bg_green = bg_green;
last_bg_blue = bg_blue;
last_sel_fg_red = sel_fg_red;
last_sel_fg_green = sel_fg_green;
last_sel_fg_blue = sel_fg_blue;
last_sel_bg_red = sel_bg_red;
last_sel_bg_green = sel_bg_green;
last_sel_bg_blue = sel_bg_blue;
g_free (last_theme_name);
last_theme_name = g_strdup (theme_name);
}
@@ -876,7 +1386,10 @@ fe_init (void)
palette_load ();
settings = gtk_settings_get_default ();
if (settings)
{
auto_dark_mode_enabled = fe_system_prefers_dark ();
dark_mode_state_initialized = TRUE;
}
fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL);
key_init ();
@@ -1039,7 +1552,10 @@ fe_message (char *msg, int flags)
type = GTK_MESSAGE_INFO;
dialog = gtk_message_dialog_new (GTK_WINDOW (parent_window), 0, type,
GTK_BUTTONS_OK, "%s", msg);
GTK_BUTTONS_OK, "%s", msg);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
if (flags & FE_MSG_MARKUP)
gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), msg);
g_signal_connect (G_OBJECT (dialog), "response",
@@ -1440,6 +1956,9 @@ 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:
/* Keep parity with Preferences -> Theme apply path (setup_theme_apply_cb). */
palette_load ();
fe_apply_theme_for_mode (prefs.hex_gui_dark_mode, NULL);
setup_apply_real (TRUE, TRUE, TRUE, FALSE);
}
}

View File

@@ -189,9 +189,16 @@ extern cairo_surface_t *dialogwin_pix;
gboolean fe_dark_mode_is_enabled (void);
gboolean fe_dark_mode_is_enabled_for (unsigned int mode);
gboolean fe_dark_mode_state_is_initialized (void);
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);
gboolean fe_apply_gtk3_theme (const char *theme_name, GError **error);
gboolean fe_apply_gtk3_theme_with_reload (const char *theme_name, gboolean force_reload,
GError **error);
gboolean fe_resolve_gtk3_theme_dir (const char *theme_root,
char **gtk3_dir_out,
gboolean *has_dark_css_out);
void fe_apply_theme_to_toplevel (GtkWidget *window);
#define SPELL_ENTRY_GET_TEXT(e) ((char *)(gtk_entry_get_text (GTK_ENTRY(e))))

View File

@@ -163,28 +163,6 @@ gtkutil_menu_custom_icon_from_icon_name (const char *icon_name)
}
static GdkPixbuf *
gtkutil_menu_icon_pixbuf_new (const char *icon_name)
{
GdkPixbuf *pixbuf = NULL;
char *resource_path;
if (!icon_name || !g_str_has_prefix (icon_name, "zc-menu-"))
return NULL;
resource_path = g_strdup_printf ("/icons/menu/light/%s.png", icon_name + strlen ("zc-menu-"));
pixbuf = gdk_pixbuf_new_from_resource (resource_path, NULL);
if (!pixbuf)
{
g_free (resource_path);
resource_path = g_strdup_printf ("/icons/menu/light/%s.svg", icon_name + strlen ("zc-menu-"));
pixbuf = gdk_pixbuf_new_from_resource (resource_path, NULL);
}
g_free (resource_path);
return pixbuf;
}
const char *
gtkutil_icon_name_from_stock (const char *stock_name)
{
@@ -251,6 +229,11 @@ gtkutil_menu_icon_theme_variant (void)
char *theme_name_lower = NULL;
const char *theme_variant = "light";
/* Prefer ZoiteChat's explicit dark-mode selection when available so icon
* variants stay in sync with the app mode, not only the system theme. */
if (fe_dark_mode_state_is_initialized () || prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO)
return fe_dark_mode_is_enabled () ? "dark" : "light";
settings = gtk_settings_get_default ();
if (settings)
{
@@ -269,41 +252,68 @@ gtkutil_menu_icon_theme_variant (void)
return theme_variant;
}
static char *
gtkutil_menu_icon_resource_path (const char *icon_name, const char *extension)
{
char *resource_path;
const char *variant;
if (!icon_name || !extension || !g_str_has_prefix (icon_name, "zc-menu-"))
return NULL;
variant = gtkutil_menu_icon_theme_variant ();
resource_path = g_strdup_printf ("/icons/menu/%s/%s.%s", variant,
icon_name + strlen ("zc-menu-"), extension);
if (!g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL))
{
g_free (resource_path);
resource_path = g_strdup_printf ("/icons/menu/light/%s.%s",
icon_name + strlen ("zc-menu-"), extension);
if (!g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL))
{
g_free (resource_path);
return NULL;
}
}
return resource_path;
}
gboolean
gtkutil_menu_icon_exists (const char *icon_name)
{
char *resource_path;
gboolean found;
resource_path = gtkutil_menu_icon_resource_path (icon_name, "png");
if (!resource_path)
resource_path = gtkutil_menu_icon_resource_path (icon_name, "svg");
found = resource_path != NULL;
g_free (resource_path);
return found;
}
static GtkWidget *
gtkutil_menu_icon_image_new (const char *icon_name, GtkIconSize size)
{
GtkWidget *image = NULL;
GdkPixbuf *pixbuf = NULL;
char *resource_path;
const char *variant;
if (!icon_name || !g_str_has_prefix (icon_name, "zc-menu-"))
return NULL;
resource_path = gtkutil_menu_icon_resource_path (icon_name, "png");
if (!resource_path)
resource_path = gtkutil_menu_icon_resource_path (icon_name, "svg");
variant = gtkutil_menu_icon_theme_variant ();
resource_path = g_strdup_printf ("/icons/menu/%s/%s.png", variant, icon_name + strlen ("zc-menu-"));
if (!g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL))
if (resource_path)
{
g_free (resource_path);
resource_path = g_strdup_printf ("/icons/menu/light/%s.png", icon_name + strlen ("zc-menu-"));
}
pixbuf = gdk_pixbuf_new_from_resource (resource_path, NULL);
if (!pixbuf)
{
g_free (resource_path);
resource_path = g_strdup_printf ("/icons/menu/%s/%s.svg", variant, icon_name + strlen ("zc-menu-"));
if (!g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL))
{
g_free (resource_path);
resource_path = g_strdup_printf ("/icons/menu/light/%s.svg", icon_name + strlen ("zc-menu-"));
}
pixbuf = gdk_pixbuf_new_from_resource (resource_path, NULL);
}
if (pixbuf)
{
image = gtk_image_new_from_pixbuf (pixbuf);
g_object_unref (pixbuf);
if (pixbuf)
{
image = gtk_image_new_from_pixbuf (pixbuf);
g_object_unref (pixbuf);
}
}
g_free (resource_path);
@@ -327,20 +337,19 @@ gtkutil_image_new_from_stock (const char *stock, GtkIconSize size)
{
GtkWidget *image;
const char *icon_name;
const char *custom_icon_name;
icon_name = gtkutil_icon_name_from_stock (stock);
if (!icon_name && stock && g_str_has_prefix (stock, "zc-menu-"))
icon_name = stock;
if (size == GTK_ICON_SIZE_MENU)
{
const char *menu_icon_name = gtkutil_menu_custom_icon_from_stock (stock);
if (!menu_icon_name)
menu_icon_name = gtkutil_menu_custom_icon_from_icon_name (icon_name);
if (menu_icon_name)
icon_name = menu_icon_name;
}
/* Use ZoiteChat's themed icon resources consistently across menu and button
* images so dark/light mode swaps all app icons together. */
custom_icon_name = gtkutil_menu_custom_icon_from_stock (stock);
if (!custom_icon_name)
custom_icon_name = gtkutil_menu_custom_icon_from_icon_name (icon_name);
if (custom_icon_name)
icon_name = custom_icon_name;
image = gtkutil_menu_icon_image_new (icon_name, size);
if (image)
@@ -803,21 +812,25 @@ gtkutil_file_req (GtkWindow *parent, const char *title, void *callback, void *us
if (flags & FRF_WRITE)
{
dialog = gtk_file_chooser_dialog_new (title, NULL,
GTK_FILE_CHOOSER_ACTION_SAVE,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Save"), GTK_RESPONSE_ACCEPT,
NULL);
dialog = gtk_file_chooser_dialog_new (title, effective_parent,
GTK_FILE_CHOOSER_ACTION_SAVE,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Save"), GTK_RESPONSE_ACCEPT,
NULL);
if (!(flags & FRF_NOASKOVERWRITE))
gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
}
else
dialog = gtk_file_chooser_dialog_new (title, NULL,
GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
dialog = gtk_file_chooser_dialog_new (title, effective_parent,
GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
if (filter && filter[0] && (flags & FRF_FILTERISINITIAL))
{
@@ -888,7 +901,8 @@ gtkutil_file_req (GtkWindow *parent, const char *title, void *callback, void *us
g_signal_connect (G_OBJECT (dialog), "destroy",
G_CALLBACK (gtkutil_file_req_destroy), (gpointer) freq);
if (effective_parent)
if (effective_parent &&
gtk_window_get_transient_for (GTK_WINDOW (dialog)) != effective_parent)
gtk_window_set_transient_for (GTK_WINDOW (dialog), effective_parent);
if (flags & FRF_MODAL)
@@ -977,6 +991,9 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
_("_Cancel"), GTK_RESPONSE_REJECT,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
@@ -1072,6 +1089,9 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
_("_Cancel"), GTK_RESPONSE_REJECT,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
@@ -1112,6 +1132,9 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
_("_No"), GTK_RESPONSE_REJECT,
_("_Yes"), GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
@@ -1235,6 +1258,7 @@ gtkutil_window_new (char *title, char *role, int width, int height, int flags)
gtk_window_set_title (GTK_WINDOW (win), title);
gtk_window_set_default_size (GTK_WINDOW (win), width, height);
gtk_window_set_role (GTK_WINDOW (win), role);
fe_apply_theme_to_toplevel (win);
if (flags & 1)
gtk_window_set_position (GTK_WINDOW (win), GTK_WIN_POS_MOUSE);
if ((flags & 2) && parent_window)

View File

@@ -41,6 +41,7 @@ GtkWidget *gtkutil_button (GtkWidget *box, char *stock, char *tip, void *callbac
void *userdata, char *labeltext);
GtkWidget *gtkutil_image_new_from_stock (const char *stock, GtkIconSize size);
GtkWidget *gtkutil_button_new_from_stock (const char *stock, const char *label);
gboolean gtkutil_menu_icon_exists (const char *icon_name);
const char *gtkutil_icon_name_from_stock (const char *stock_name);
void gtkutil_label_new (char *text, GtkWidget * box);
GtkWidget *gtkutil_entry_new (int max, GtkWidget * box, void *callback,

View File

@@ -294,7 +294,10 @@ ignore_clear_entry_clicked (GtkWidget * wid)
dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
_("Are you sure you want to remove all ignores?"));
_("Are you sure you want to remove all ignores?"));
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (ignore_clear_cb), NULL);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);

View File

@@ -129,6 +129,9 @@ joind_show_dialog (server *serv)
char buf2[256];
serv->gui->joind_win = dialog1 = gtk_dialog_new ();
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog1);
g_snprintf(buf, sizeof(buf), _("Connection Complete - %s"), _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (dialog1), buf);
gtk_window_set_type_hint (GTK_WINDOW (dialog1), GDK_WINDOW_TYPE_HINT_DIALOG);

View File

@@ -1362,6 +1362,9 @@ mg_tab_close (session *sess)
GTK_MESSAGE_WARNING, GTK_BUTTONS_OK_CANCEL,
_("This server still has %d channels or dialogs associated with it. "
"Close them all?"), i);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (mg_tab_close_cb), sess);
if (prefs.hex_gui_tab_layout)
@@ -1459,6 +1462,9 @@ mg_open_quit_dialog (gboolean minimize_button)
}
dialog = gtk_dialog_new ();
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
@@ -1863,7 +1869,7 @@ mg_create_tabmenu (session *sess, GdkEventButton *event, chan *ch)
if (sess)
{
char *name = g_markup_escape_text (sess->channel[0] ? sess->channel : _("<none>"), -1);
g_snprintf (buf, sizeof (buf), "<span foreground=\"#3344cc\"><b>%s</b></span>", name);
g_snprintf (buf, sizeof (buf), "<b>%s</b>", name);
g_free (name);
item = gtk_menu_item_new_with_label ("");
@@ -3681,6 +3687,8 @@ mg_create_topwindow (session *sess)
mg_place_userlist_and_chanview (sess->gui);
gtk_widget_show (win);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (win);
#ifdef G_OS_WIN32
@@ -3857,6 +3865,8 @@ mg_create_tabwindow (session *sess)
mg_place_userlist_and_chanview (sess->gui);
gtk_widget_show (win);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (win);
#ifdef G_OS_WIN32
@@ -3868,6 +3878,7 @@ mg_create_tabwindow (session *sess)
void
mg_apply_setup (void)
{
GList *toplevels, *node;
GSList *list = sess_list;
session *sess;
int done_main = FALSE;
@@ -3887,6 +3898,11 @@ mg_apply_setup (void)
done_main = TRUE;
list = list->next;
}
toplevels = gtk_window_list_toplevels ();
for (node = toplevels; node; node = node->next)
fe_apply_theme_to_toplevel (GTK_WIDGET (node->data));
g_list_free (toplevels);
}
static chan *

View File

@@ -65,28 +65,6 @@
static GSList *submenu_list;
static gboolean
menu_icon_exists_in_resource (const char *icon_name)
{
char *resource_path;
gboolean found;
if (!icon_name || !g_str_has_prefix (icon_name, "zc-menu-"))
return FALSE;
resource_path = g_strdup_printf ("/icons/menu/light/%s.png", icon_name + strlen ("zc-menu-"));
found = g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL);
if (!found)
{
g_free (resource_path);
resource_path = g_strdup_printf ("/icons/menu/light/%s.svg", icon_name + strlen ("zc-menu-"));
found = g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL);
}
g_free (resource_path);
return found;
}
static GtkWidget *
menu_icon_widget_new (const char *icon)
{
@@ -112,7 +90,7 @@ menu_icon_widget_new (const char *icon)
{
char *menu_icon_name = g_strdup_printf ("zc-menu-%s", icon);
if (menu_icon_exists_in_resource (menu_icon_name))
if (gtkutil_menu_icon_exists (menu_icon_name))
img = gtkutil_image_new_from_stock (menu_icon_name, GTK_ICON_SIZE_MENU);
else
img = gtkutil_image_new_from_stock (icon, GTK_ICON_SIZE_MENU);
@@ -1520,6 +1498,9 @@ menu_join (GtkWidget * wid, gpointer none)
_("_Cancel"), GTK_RESPONSE_REJECT,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
{
GtkWidget *button;
@@ -1856,6 +1837,9 @@ static void
menu_about (GtkWidget *wid, gpointer sess)
{
GtkAboutDialog *dialog = GTK_ABOUT_DIALOG(gtk_about_dialog_new());
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark/.zoitechat-light. */
fe_apply_theme_to_toplevel (GTK_WIDGET (dialog));
char comment[512];
char *license = "This program is free software; you can redistribute it and/or modify\n" \
"it under the terms of the GNU General Public License as published by\n" \

View File

@@ -373,6 +373,9 @@ fe_notify_ask (char *nick, char *networks)
LABEL_NOTIFY_CANCEL, GTK_RESPONSE_REJECT,
LABEL_NOTIFY_OK, GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
if (parent_window)
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);

View File

@@ -224,6 +224,27 @@ palette_dark_set_color (int idx, const PaletteColor *col)
dark_user_colors[idx] = *col;
}
const PaletteColor *
palette_user_colors (void)
{
if (!user_colors_valid)
{
memcpy (user_colors, colors, sizeof (user_colors));
user_colors_valid = TRUE;
}
return user_colors;
}
const PaletteColor *
palette_dark_colors (void)
{
if (!dark_user_colors_valid)
return dark_colors;
return dark_user_colors;
}
void
palette_alloc (GtkWidget * widget)
{

View File

@@ -57,6 +57,8 @@ 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);
const PaletteColor *palette_user_colors (void);
const PaletteColor *palette_dark_colors (void);
/*
* Apply ZoiteChat's built-in "dark mode" palette.

View File

@@ -781,12 +781,15 @@ servlist_deletenet_cb (GtkWidget *item, ircnet *net)
if (!net)
return;
dialog = gtk_message_dialog_new (GTK_WINDOW (serverlist_win),
GTK_DIALOG_DESTROY_WITH_PARENT |
GTK_DIALOG_MODAL,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_OK_CANCEL,
_("Really remove network \"%s\" and all its servers?"),
net->name);
GTK_DIALOG_DESTROY_WITH_PARENT |
GTK_DIALOG_MODAL,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_OK_CANCEL,
_("Really remove network \"%s\" and all its servers?"),
net->name);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
g_signal_connect (dialog, "response",
G_CALLBACK (servlist_deletenetdialog_cb), net);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
@@ -1799,6 +1802,9 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
gtk_window_set_role (GTK_WINDOW (editwindow), "editserv");
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (editwindow);
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
@@ -2078,6 +2084,9 @@ servlist_open_networks (void)
gtk_window_set_default_size (GTK_WINDOW (servlist), netlist_win_width, netlist_win_height);
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (servlist);
if (current_sess)
gtk_window_set_transient_for (GTK_WINDOW (servlist), GTK_WINDOW (current_sess->gui->window));

View File

@@ -56,6 +56,8 @@ static GtkWidget *setup_window = NULL;
static int last_selected_page = 0;
static int last_selected_row = 0; /* sound row */
static gboolean color_change;
static gboolean setup_color_edit_dark_palette;
static const PaletteColor *setup_color_edit_source_colors;
static struct zoitechatprefs setup_prefs;
static GSList *color_selector_widgets;
static GtkWidget *cancel_button;
@@ -64,11 +66,30 @@ 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;
GtkWidget *gtk3_combo;
GtkWidget *gtk3_import_button;
GtkWidget *gtk3_apply_button;
GtkWidget *gtk3_use_system_button;
GtkWidget *gtk3_status_label;
GPtrArray *gtk3_theme_paths;
gboolean gtk3_force_reload_next_apply;
} setup_theme_ui;
static void
setup_theme_ui_free (gpointer data)
{
setup_theme_ui *ui = data;
if (!ui)
return;
if (ui->gtk3_theme_paths)
g_ptr_array_free (ui->gtk3_theme_paths, TRUE);
g_free (ui);
}
enum
{
ST_END,
@@ -372,6 +393,13 @@ static const setting dark_mode_setting =
0
};
static const char *const color_edit_target_modes[] =
{
N_("Light"),
N_("Dark"),
NULL
};
static const char *const dccaccept[] =
{
N_("Ask for confirmation"),
@@ -1233,6 +1261,9 @@ setup_browsefont_cb (GtkWidget *button, GtkWidget *entry)
const char *font_name;
dialog = gtk_font_chooser_dialog_new (_("Select font"), GTK_WINDOW (setup_window));
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark/.zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
font_dialog = dialog; /* global var */
gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
@@ -1474,6 +1505,9 @@ setup_create_page (const setting *set)
return tab;
}
static void
setup_color_button_apply (GtkWidget *button, const PaletteColor *color);
static void
setup_color_selectors_set_sensitive (gboolean sensitive)
{
@@ -1487,14 +1521,98 @@ setup_color_selectors_set_sensitive (gboolean sensitive)
}
}
static void
setup_color_update_source_palette (void)
{
setup_color_edit_source_colors = setup_color_edit_dark_palette
? palette_dark_colors ()
: palette_user_colors ();
}
static void
setup_refresh_color_selector_widgets (void)
{
GSList *l = color_selector_widgets;
while (l)
{
GtkWidget *w = (GtkWidget *) l->data;
gpointer color_index_ptr;
int color_index;
if (!GTK_IS_WIDGET (w))
{
l = l->next;
continue;
}
color_index_ptr = g_object_get_data (G_OBJECT (w), "zoitechat-color-index");
if (!color_index_ptr)
{
l = l->next;
continue;
}
color_index = GPOINTER_TO_INT (color_index_ptr);
if (setup_color_edit_source_colors && color_index >= 0 && color_index <= MAX_COL)
setup_color_button_apply (w, &setup_color_edit_source_colors[color_index]);
l = l->next;
}
}
static void
setup_dark_mode_menu_cb (GtkWidget *cbox, const setting *set)
{
setup_menu_cb (cbox, set);
setup_color_update_source_palette ();
setup_refresh_color_selector_widgets ();
/* Keep color selectors usable even when dark mode is enabled. */
setup_color_selectors_set_sensitive (TRUE);
}
static void
setup_color_edit_target_menu_cb (GtkComboBox *cbox, gpointer userdata)
{
(void) userdata;
setup_color_edit_dark_palette = gtk_combo_box_get_active (cbox) == 1;
setup_color_update_source_palette ();
setup_refresh_color_selector_widgets ();
setup_color_selectors_set_sensitive (TRUE);
}
static GtkWidget *
setup_create_color_edit_target_menu (GtkWidget *table, int row)
{
GtkWidget *wid;
GtkWidget *cbox;
GtkWidget *box;
int i;
wid = gtk_label_new (_("Editing:"));
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; color_edit_target_modes[i]; i++)
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (cbox), _(color_edit_target_modes[i]));
gtk_combo_box_set_active (GTK_COMBO_BOX (cbox), setup_color_edit_dark_palette ? 1 : 0);
g_signal_connect (G_OBJECT (cbox), "changed",
G_CALLBACK (setup_color_edit_target_menu_cb), NULL);
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 GtkWidget *
setup_create_dark_mode_menu (GtkWidget *table, int row, const setting *set)
{
@@ -1543,8 +1661,7 @@ setup_color_button_apply (GtkWidget *button, const PaletteColor *color)
typedef struct
{
GtkWidget *button;
PaletteColor *color;
int color_index;
} setup_color_dialog_data;
static void
@@ -1575,14 +1692,15 @@ setup_color_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data
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);
if (setup_color_edit_dark_palette)
palette_dark_set_color (data->color_index, &rgba);
else
palette_user_set_color ((int)(data->color - colors), data->color);
palette_user_set_color (data->color_index, &rgba);
color_change = TRUE;
setup_color_update_source_palette ();
setup_refresh_color_selector_widgets ();
}
gtk_widget_destroy (GTK_WIDGET (dialog));
@@ -1593,20 +1711,26 @@ static void
setup_color_cb (GtkWidget *button, gpointer userdata)
{
GtkWidget *dialog;
PaletteColor *color;
int color_index;
GdkRGBA rgba;
setup_color_dialog_data *data;
color = &colors[GPOINTER_TO_INT (userdata)];
(void) button;
color_index = GPOINTER_TO_INT (userdata);
dialog = gtk_color_chooser_dialog_new (_("Select color"), GTK_WINDOW (setup_window));
setup_rgba_from_palette (color, &rgba);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark/.zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
if (setup_color_edit_source_colors && color_index >= 0 && color_index <= MAX_COL)
setup_rgba_from_palette (&setup_color_edit_source_colors[color_index], &rgba);
else
setup_rgba_from_palette (&colors[color_index], &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;
data->color_index = color_index;
g_signal_connect (dialog, "response", G_CALLBACK (setup_color_response_cb), data);
gtk_widget_show (dialog);
}
@@ -1640,11 +1764,15 @@ setup_create_color_button (GtkWidget *table, int num, int row, int col)
/* 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);
g_object_set_data (G_OBJECT (but), "zoitechat-color-index", GINT_TO_POINTER (num));
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]);
if (setup_color_edit_source_colors)
setup_color_button_apply (but, &setup_color_edit_source_colors[num]);
else
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);
@@ -1682,6 +1810,8 @@ static GtkWidget *
setup_create_color_page (void)
{
color_selector_widgets = NULL;
setup_color_edit_dark_palette = setup_prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_DARK;
setup_color_update_source_palette ();
GtkWidget *tab, *box, *label;
int i;
@@ -1734,8 +1864,10 @@ setup_create_color_page (void)
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_create_color_edit_target_menu (tab, 14);
setup_refresh_color_selector_widgets ();
setup_color_selectors_set_sensitive (TRUE);
setup_create_header (tab, 15, N_("Color Stripping"));
setup_create_header (tab, 16, N_("Color Stripping"));
/* label = gtk_label_new (_("Strip colors from:"));
gtk_widget_set_halign (label, GTK_ALIGN_START);
@@ -1746,7 +1878,7 @@ setup_create_color_page (void)
for (i = 0; i < 3; i++)
{
setup_create_toggleL (tab, i + 16, &color_settings[i]);
setup_create_toggleL (tab, i + 17, &color_settings[i]);
}
return box;
@@ -1759,121 +1891,230 @@ setup_theme_show_message (GtkMessageType message_type, const char *primary)
dialog = gtk_message_dialog_new (GTK_WINDOW (setup_window), GTK_DIALOG_MODAL,
message_type, GTK_BUTTONS_CLOSE, "%s", primary);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
}
static void
setup_theme_populate (setup_theme_ui *ui)
setup_gtk3_theme_populate (setup_theme_ui *ui)
{
char *themes_dir;
GDir *dir;
const char *name;
GtkTreeModel *model;
GtkTreeIter iter;
int count;
char *themes_dir;
GtkTreeModel *model;
GtkTreeIter iter;
GDir *dir;
const char *name;
gboolean have_valid_theme = FALSE;
gint active = -1;
guint i;
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 (), "gtk3-themes", NULL);
g_mkdir_with_parents (themes_dir, 0700);
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);
model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->gtk3_combo));
while (gtk_tree_model_get_iter_first (model, &iter))
gtk_combo_box_text_remove (GTK_COMBO_BOX_TEXT (ui->gtk3_combo), 0);
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);
}
if (!ui->gtk3_theme_paths)
ui->gtk3_theme_paths = g_ptr_array_new_with_free_func (g_free);
else
g_ptr_array_set_size (ui->gtk3_theme_paths, 0);
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);
dir = g_dir_open (themes_dir, 0, NULL);
if (dir)
{
while ((name = g_dir_read_name (dir)))
{
char *theme_path;
char *gtk3_dir = NULL;
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."));
theme_path = g_build_filename (themes_dir, name, NULL);
g_free (themes_dir);
if (g_file_test (theme_path, G_FILE_TEST_IS_DIR)
&& fe_resolve_gtk3_theme_dir (theme_path, &gtk3_dir, NULL))
{
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (ui->gtk3_combo), name);
g_ptr_array_add (ui->gtk3_theme_paths, theme_path);
have_valid_theme = TRUE;
theme_path = NULL;
}
g_free (gtk3_dir);
g_free (theme_path);
}
g_dir_close (dir);
}
if (!have_valid_theme)
{
gtk_label_set_text (GTK_LABEL (ui->gtk3_status_label), _("No valid GTK3 themes found."));
}
else
{
for (i = 0; i < ui->gtk3_theme_paths->len; i++)
{
const char *theme_path = g_ptr_array_index (ui->gtk3_theme_paths, i);
char *theme_name = g_path_get_basename (theme_path);
if (g_strcmp0 (prefs.hex_gui_gtk3_theme_name, theme_name) == 0)
{
active = (gint) i;
g_free (theme_name);
break;
}
g_free (theme_name);
}
if (active >= 0)
gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), active);
else if (prefs.hex_gui_gtk3_theme_name[0] == '\0')
gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), 0);
else
gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), -1);
if (gtk_combo_box_get_active (GTK_COMBO_BOX (ui->gtk3_combo)) >= 0)
gtk_label_set_text (GTK_LABEL (ui->gtk3_status_label), _("Select a GTK3 theme to apply."));
else
gtk_label_set_text (GTK_LABEL (ui->gtk3_status_label), _("No valid GTK3 themes found."));
}
gtk_widget_set_sensitive (ui->gtk3_apply_button,
gtk_combo_box_get_active (GTK_COMBO_BOX (ui->gtk3_combo)) >= 0);
g_free (themes_dir);
}
static void
setup_theme_refresh_cb (GtkWidget *button, gpointer user_data)
setup_theme_gtk3_selection_changed (GtkComboBox *combo, gpointer user_data)
{
setup_theme_ui *ui = user_data;
setup_theme_ui *ui = user_data;
gboolean has_selection = gtk_combo_box_get_active (combo) >= 0;
setup_theme_populate (ui);
gtk_widget_set_sensitive (ui->gtk3_apply_button, has_selection);
}
static void
setup_theme_open_folder_cb (GtkWidget *button, gpointer user_data)
setup_theme_gtk3_import_cb (GtkWidget *button, gpointer user_data)
{
char *themes_dir;
setup_theme_ui *ui = user_data;
GtkWidget *dialog;
GtkFileFilter *filter;
gint response;
char *archive_path;
GError *error = NULL;
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);
dialog = gtk_file_chooser_dialog_new (_("Import GTK3 Theme Archive"), GTK_WINDOW (setup_window),
GTK_FILE_CHOOSER_ACTION_OPEN,
_ ("_Cancel"), GTK_RESPONSE_CANCEL,
_ ("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
/* Window classes are required for GTK CSS selectors like
* .zoitechat-dark / .zoitechat-light. */
fe_apply_theme_to_toplevel (dialog);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, _("Theme archives (.zip, .tar.xz, .tar.gz, .tar)"));
gtk_file_filter_add_pattern (filter, "*.zip");
gtk_file_filter_add_pattern (filter, "*.ZIP");
gtk_file_filter_add_pattern (filter, "*.tar");
gtk_file_filter_add_pattern (filter, "*.tar.gz");
gtk_file_filter_add_pattern (filter, "*.tgz");
gtk_file_filter_add_pattern (filter, "*.tar.xz");
gtk_file_filter_add_pattern (filter, "*.txz");
gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter);
response = gtk_dialog_run (GTK_DIALOG (dialog));
if (response != GTK_RESPONSE_ACCEPT)
{
gtk_widget_destroy (dialog);
return;
}
archive_path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
gtk_widget_destroy (dialog);
if (!archive_path)
return;
if (!zoitechat_import_gtk3_theme_archive (archive_path, NULL, &error))
{
setup_theme_show_message (GTK_MESSAGE_ERROR,
error ? error->message : _("Failed to import GTK3 theme archive."));
g_clear_error (&error);
}
else
{
ui->gtk3_force_reload_next_apply = TRUE;
setup_gtk3_theme_populate (ui);
gtk_label_set_text (GTK_LABEL (ui->gtk3_status_label), _("GTK3 theme archive imported successfully."));
setup_theme_show_message (GTK_MESSAGE_INFO, _("GTK3 theme archive imported successfully."));
}
g_free (archive_path);
}
static void
setup_theme_apply_gtk3_cb (GtkWidget *button, gpointer user_data)
{
setup_theme_ui *ui = user_data;
gint active;
const char *theme_path;
char *theme;
GError *error = NULL;
active = gtk_combo_box_get_active (GTK_COMBO_BOX (ui->gtk3_combo));
if (active < 0 || !ui->gtk3_theme_paths || (guint) active >= ui->gtk3_theme_paths->len)
return;
theme_path = g_ptr_array_index (ui->gtk3_theme_paths, active);
theme = g_path_get_basename (theme_path);
if (!theme || !*theme)
{
g_free (theme);
return;
}
if (!fe_apply_gtk3_theme_with_reload (theme, ui->gtk3_force_reload_next_apply, &error))
{
setup_theme_show_message (GTK_MESSAGE_ERROR,
error ? error->message : _("Failed to apply GTK3 theme."));
g_clear_error (&error);
g_free (theme);
return;
}
ui->gtk3_force_reload_next_apply = FALSE;
safe_strcpy (prefs.hex_gui_gtk3_theme_name, theme, sizeof (prefs.hex_gui_gtk3_theme_name));
/* Keep the Preferences working copy in sync so pressing OK does not
* overwrite the just-selected theme with stale setup_prefs data. */
safe_strcpy (setup_prefs.hex_gui_gtk3_theme_name, theme,
sizeof (setup_prefs.hex_gui_gtk3_theme_name));
save_config ();
gtk_label_set_text (GTK_LABEL (ui->gtk3_status_label), _("GTK3 theme activated from ZoiteChat's local theme store."));
setup_theme_show_message (GTK_MESSAGE_INFO, _("GTK3 theme activated and saved."));
g_free (theme);
}
static void
setup_theme_selection_changed (GtkComboBox *combo, gpointer user_data)
setup_theme_gtk3_use_system_cb (GtkWidget *button, gpointer user_data)
{
setup_theme_ui *ui = user_data;
gboolean has_selection = gtk_combo_box_get_active (combo) >= 0;
setup_theme_ui *ui = user_data;
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);
fe_apply_gtk3_theme (NULL, NULL);
ui->gtk3_force_reload_next_apply = FALSE;
prefs.hex_gui_gtk3_theme_name[0] = '\0';
setup_prefs.hex_gui_gtk3_theme_name[0] = '\0';
save_config ();
gtk_label_set_text (GTK_LABEL (ui->gtk3_status_label), _("Using system GTK theme."));
setup_theme_show_message (GTK_MESSAGE_INFO, _("Using system GTK theme."));
}
static GtkWidget *
@@ -1881,62 +2122,59 @@ setup_create_theme_page (void)
{
setup_theme_ui *ui;
GtkWidget *box;
GtkWidget *label;
GtkWidget *hbox;
GtkWidget *button_box;
char *themes_dir;
char *markup;
GtkWidget *label;
GtkWidget *hbox;
GtkWidget *button_box;
GtkWidget *frame;
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 <tt>%s</tt>."), 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);
frame = gtk_frame_new (_("GTK3 Theme"));
gtk_box_pack_start (GTK_BOX (box), frame, FALSE, FALSE, 0);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0);
hbox = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 6);
gtk_container_set_border_width (GTK_CONTAINER (hbox), 6);
gtk_container_add (GTK_CONTAINER (frame), hbox);
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);
label = gtk_label_new (_("Import a GTK3 theme archive or select a GTK3 theme installed in ZoiteChat's local theme store."));
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_widget_set_halign (label, GTK_ALIGN_START);
gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
button_box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
gtk_box_pack_start (GTK_BOX (hbox), button_box, FALSE, FALSE, 0);
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);
ui->gtk3_combo = gtk_combo_box_text_new ();
gtk_box_pack_start (GTK_BOX (button_box), ui->gtk3_combo, TRUE, TRUE, 0);
g_signal_connect (G_OBJECT (ui->gtk3_combo), "changed",
G_CALLBACK (setup_theme_gtk3_selection_changed), 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);
ui->gtk3_import_button = gtk_button_new_with_mnemonic (_("_Import GTK3 Theme Archive"));
gtk_box_pack_start (GTK_BOX (button_box), ui->gtk3_import_button, FALSE, FALSE, 0);
g_signal_connect (G_OBJECT (ui->gtk3_import_button), "clicked",
G_CALLBACK (setup_theme_gtk3_import_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->gtk3_apply_button = gtk_button_new_with_mnemonic (_("Apply GTK_3 Theme"));
gtk_box_pack_start (GTK_BOX (button_box), ui->gtk3_apply_button, FALSE, FALSE, 0);
g_signal_connect (G_OBJECT (ui->gtk3_apply_button), "clicked",
G_CALLBACK (setup_theme_apply_gtk3_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);
ui->gtk3_use_system_button = gtk_button_new_with_mnemonic (_("Use _System GTK Theme"));
gtk_box_pack_start (GTK_BOX (button_box), ui->gtk3_use_system_button, FALSE, FALSE, 0);
g_signal_connect (G_OBJECT (ui->gtk3_use_system_button), "clicked",
G_CALLBACK (setup_theme_gtk3_use_system_cb), ui);
setup_theme_populate (ui);
ui->gtk3_status_label = gtk_label_new (NULL);
gtk_widget_set_halign (ui->gtk3_status_label, GTK_ALIGN_START);
gtk_widget_set_valign (ui->gtk3_status_label, GTK_ALIGN_CENTER);
gtk_box_pack_start (GTK_BOX (hbox), ui->gtk3_status_label, FALSE, FALSE, 0);
g_object_set_data_full (G_OBJECT (box), "setup-theme-ui", ui, g_free);
setup_gtk3_theme_populate (ui);
g_object_set_data_full (G_OBJECT (box), "setup-theme-ui", ui, setup_theme_ui_free);
return box;
}
@@ -2412,6 +2650,47 @@ setup_apply_entry_style (GtkWidget *entry)
input_style->font_desc);
}
static void
setup_apply_input_caret_provider (GtkWidget *widget, const char *css)
{
GtkCssProvider *provider;
GtkStyleContext *context;
if (!widget)
return;
context = gtk_widget_get_style_context (widget);
provider = g_object_get_data (G_OBJECT (widget), "zoitechat-input-caret-provider");
if (!provider)
{
provider = gtk_css_provider_new ();
g_object_set_data_full (G_OBJECT (widget), "zoitechat-input-caret-provider",
provider, g_object_unref);
gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
gtk_css_provider_load_from_data (provider, css, -1, NULL);
}
static void
setup_remove_input_caret_provider (GtkWidget *widget)
{
GtkCssProvider *provider;
GtkStyleContext *context;
if (!widget)
return;
context = gtk_widget_get_style_context (widget);
provider = g_object_get_data (G_OBJECT (widget), "zoitechat-input-caret-provider");
if (!provider)
return;
gtk_style_context_remove_provider (context, GTK_STYLE_PROVIDER (provider));
g_object_set_data (G_OBJECT (widget), "zoitechat-input-caret-provider", NULL);
}
static void
setup_apply_to_sess (session_gui *gui)
{
@@ -2437,35 +2716,29 @@ setup_apply_to_sess (session_gui *gui)
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; }",
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_input_caret_provider (gui->input_box, buf);
setup_apply_input_caret_provider (gui->limit_entry, buf);
setup_apply_input_caret_provider (gui->key_entry, buf);
setup_apply_input_caret_provider (gui->topic_entry, buf);
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);
}
else
{
setup_remove_input_caret_provider (gui->input_box);
setup_remove_input_caret_provider (gui->limit_entry);
setup_remove_input_caret_provider (gui->key_entry);
setup_remove_input_caret_provider (gui->topic_entry);
}
if (prefs.hex_gui_ulist_buttons)
gtk_widget_show (gui->button_box);
@@ -2742,6 +3015,7 @@ setup_window_open (void)
gtk_box_pack_start (GTK_BOX (hbbox), wid, FALSE, FALSE, 0);
gtk_widget_show_all (win);
fe_apply_theme_to_toplevel (win);
return win;
}
@@ -2757,6 +3031,8 @@ setup_close_cb (GtkWidget *win, GtkWidget **swin)
color_selector_widgets = NULL;
}
setup_color_edit_source_colors = NULL;
if (font_dialog)
{
gtk_widget_destroy (font_dialog);

View File

@@ -27,6 +27,8 @@ DefaultDirName={pf64}\ZoiteChat
#else
DefaultDirName={pf32}\ZoiteChat
#endif
DisableDirPage=no
UsePreviousAppDir=no
DefaultGroupName=ZoiteChat
AllowNoIcons=yes
SolidCompression=yes