8 Commits

Author SHA1 Message Date
4c66005003 fix input font style/size 2026-07-16 14:42:13 -06:00
f87f1bfc4b New emoji menu w/windows 7 fixed fallback. 2026-07-16 03:28:04 -06:00
deepend-tildeclub
2db3076c92 Merge pull request #350 from ZoiteChat/fix-gtk3-quirks
fix a few GTK3 theming quirks
2026-07-16 02:29:43 -06:00
90daed18df fix a few GTK3 theming quirks 2026-07-15 23:56:05 -06:00
deepend-tildeclub
65dd2c645a Merge pull request #345 from ZoiteChat/gtk3-consistency-fixes
theming: respect system theme by default
2026-07-14 14:27:08 -06:00
89d70a940c theming: respect system theme by default 2026-07-13 13:26:55 -06:00
deepend-tildeclub
4944517a0e Merge pull request #344 from ZoiteChat/ask_sasl_password_reconnect
Prompt SASL password again on reconnect
2026-07-13 09:53:41 -06:00
2a7ee7f0b2 Prompt SASL password again on reconnect 2026-07-13 09:18:19 -06:00
38 changed files with 1934 additions and 274 deletions

View File

@@ -119,8 +119,9 @@ jobs:
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/ cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
fi fi
test -f AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf # The emoji font is not bundled on Linux; the AppImage uses the
test -f AppDir/usr/share/doc/zoitechat/fonts/OFL.txt # host system's emoji font (e.g. fonts-noto-color-emoji).
test ! -e AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then
install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0 install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0
@@ -184,14 +185,6 @@ jobs:
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}" export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
fi fi
if [ -d "$APPDIR/etc/fonts" ]; then
export FONTCONFIG_SYSROOT="$APPDIR"
export FONTCONFIG_PATH="$APPDIR/etc/fonts${FONTCONFIG_PATH:+:$FONTCONFIG_PATH}"
if [ -f "$APPDIR/etc/fonts/fonts.conf" ]; then
export FONTCONFIG_FILE="$APPDIR/etc/fonts/fonts.conf"
fi
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then

View File

@@ -5,9 +5,14 @@ Unicode Emoji 17.0. It is vendored from Debian's
`fonts-noto-color-emoji` 2.051 package, built from the corresponding `fonts-noto-color-emoji` 2.051 package, built from the corresponding
Google Noto Emoji release. Google Noto Emoji release.
Official ZoiteChat packages install the font inside the application and The font is bundled only with Windows builds, where it is installed
register it only for the ZoiteChat process. It is not installed as a inside the application and registered only for the ZoiteChat process.
global operating-system font. It is not installed as a global operating-system font.
Linux (and other Unix) builds do not install this file. They rely on
the emoji font provided by the operating system instead — typically the
distribution's Noto Color Emoji package (`fonts-noto-color-emoji` on
Debian/Ubuntu, `google-noto-color-emoji-fonts` on Fedora).
Upstream release: https://github.com/googlefonts/noto-emoji/releases/tag/v2.051 Upstream release: https://github.com/googlefonts/noto-emoji/releases/tag/v2.051

View File

@@ -3,7 +3,11 @@ if get_option('plugin')
endif endif
if get_option('gtk-frontend') if get_option('gtk-frontend')
# The bundled emoji font ships only with Windows builds; Linux and other
# Unix systems use the system emoji font package instead.
if host_machine.system() == 'windows'
subdir('fonts') subdir('fonts')
endif
subdir('icons') subdir('icons')
subdir('misc') subdir('misc')
subdir('man') subdir('man')

View File

@@ -66,8 +66,7 @@
], ],
"post-install": [ "post-install": [
"install -d /app/extensions", "install -d /app/extensions",
"test -f /app/share/fonts/zoitechat/NotoColorEmoji.ttf", "test ! -e /app/share/fonts/zoitechat/NotoColorEmoji.ttf"
"test -f /app/share/doc/zoitechat/fonts/OFL.txt"
], ],
"sources": [ "sources": [
{ {

View File

@@ -20,7 +20,6 @@ libcanberra_dep = dependency('libcanberra', version: '>= 0.22',
required: get_option('libcanberra')) required: get_option('libcanberra'))
dbus_dep = dependency('gio-2.0', required: get_option('dbus')) dbus_dep = dependency('gio-2.0', required: get_option('dbus'))
libsecret_dep = dependency('libsecret-1', required: false) libsecret_dep = dependency('libsecret-1', required: false)
fontconfig_dep = dependency('fontconfig', required: false)
global_deps = [] global_deps = []
if cc.get_id() == 'msvc' if cc.get_id() == 'msvc'
@@ -36,9 +35,6 @@ config_h.set_quoted('PACKAGE_NAME', meson.project_name())
config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat') config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
get_option('datadir'), 'locale')) get_option('datadir'), 'locale'))
config_h.set_quoted('ZOITECHAT_EMOJI_FONT_FILE',
join_paths(get_option('prefix'), get_option('datadir'), 'fonts',
'zoitechat', 'NotoColorEmoji.ttf'))
config_h.set10('ENABLE_NLS', true) config_h.set10('ENABLE_NLS', true)
# Optional features # Optional features
@@ -46,7 +42,6 @@ config_h.set('USE_OPENSSL', libssl_dep.found())
config_h.set('USE_LIBCANBERRA', libcanberra_dep.found()) config_h.set('USE_LIBCANBERRA', libcanberra_dep.found())
config_h.set('USE_DBUS', dbus_dep.found()) config_h.set('USE_DBUS', dbus_dep.found())
config_h.set('HAVE_LIBSECRET', libsecret_dep.found()) config_h.set('HAVE_LIBSECRET', libsecret_dep.found())
config_h.set('HAVE_FONTCONFIG', fontconfig_dep.found())
config_h.set('USE_PLUGIN', get_option('plugin')) config_h.set('USE_PLUGIN', get_option('plugin'))
config_h.set('USE_GTK_FRONTEND', get_option('gtk-frontend')) config_h.set('USE_GTK_FRONTEND', get_option('gtk-frontend'))

View File

@@ -31,6 +31,7 @@ BuildRequires: pkgconfig(lua)
BuildRequires: pkgconfig(openssl) >= 0.9.8 BuildRequires: pkgconfig(openssl) >= 0.9.8
BuildRequires: pkgconfig(python3) BuildRequires: pkgconfig(python3)
Requires: google-noto-color-emoji-fonts
Requires: hicolor-icon-theme Requires: hicolor-icon-theme
Requires: iso-codes Requires: iso-codes

View File

@@ -40,7 +40,7 @@
#define DEF_FONT "Monospace 9" #define DEF_FONT "Monospace 9"
#ifdef WIN32 #ifdef WIN32
#define DEF_FONT_ALTER "Segoe UI Emoji,Arial Unicode MS,Lucida Sans Unicode,Meiryo,Symbola,Unifont" #define DEF_FONT_ALTER "Segoe UI Emoji,Segoe UI Symbol,Arial Unicode MS,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
#else #else
#define DEF_FONT_ALTER "Arial Unicode MS,Segoe UI Emoji,Lucida Sans Unicode,Meiryo,Symbola,Unifont" #define DEF_FONT_ALTER "Arial Unicode MS,Segoe UI Emoji,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
#endif #endif

View File

@@ -315,6 +315,59 @@ resolve_parent_theme_root (const char *child_theme_root, const char *parent_name
return home_local; return home_local;
g_free (home_local); g_free (home_local);
{
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
if (xdg_data_home && xdg_data_home[0])
{
candidate = g_build_filename (xdg_data_home, "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
return candidate;
g_free (candidate);
}
}
candidate = g_build_filename (g_get_user_data_dir (), "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
return candidate;
g_free (candidate);
{
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
guint i;
if (xdg_data_dirs && xdg_data_dirs[0])
{
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
for (i = 0; data_dirs && data_dirs[i]; i++)
{
if (!data_dirs[i][0])
continue;
candidate = g_build_filename (data_dirs[i], "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
{
g_strfreev (data_dirs);
return candidate;
}
g_free (candidate);
}
g_strfreev (data_dirs);
}
else
{
const char *const *system_data_dirs = g_get_system_data_dirs ();
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
{
candidate = g_build_filename (system_data_dirs[i], "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
return candidate;
g_free (candidate);
}
}
}
user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir (); user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
candidate = g_build_filename (user_dir, parent_name, NULL); candidate = g_build_filename (user_dir, parent_name, NULL);
g_free (user_dir); g_free (user_dir);
@@ -632,12 +685,47 @@ zoitechat_gtk3_theme_service_discover (void)
g_mkdir_with_parents (user_dir, 0700); g_mkdir_with_parents (user_dir, 0700);
/* Prefer the live XDG environment over GLib's process-cached copies so
* theme lookup matches what GTK resolves for the running session. */
{
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
if (xdg_data_home && xdg_data_home[0])
user_data_themes = g_build_filename (xdg_data_home, "themes", NULL);
else
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL); user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
}
/* Scan the app's own themes dir first: imported themes get a symlink
* under the XDG data themes dir so GTK can resolve gtk-theme-name, and
* scanning that link first would make the canonical-root dedup keep
* the symlink path - changing the theme's path-derived id and breaking
* the saved gui_gtk3_theme reference on the next start. */
add_theme_root (user_roots, seen_user_roots, user_dir);
add_theme_root (user_roots, seen_user_roots, user_data_themes); add_theme_root (user_roots, seen_user_roots, user_data_themes);
g_free (user_data_themes); g_free (user_data_themes);
add_theme_root (user_roots, seen_user_roots, home_themes); add_theme_root (user_roots, seen_user_roots, home_themes);
add_theme_root (user_roots, seen_user_roots, user_dir);
{
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
if (xdg_data_dirs && xdg_data_dirs[0])
{
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
for (i = 0; data_dirs && data_dirs[i]; i++)
{
char *system_themes;
if (!data_dirs[i][0])
continue;
system_themes = g_build_filename (data_dirs[i], "themes", NULL);
add_theme_root (system_roots, seen_system_roots, system_themes);
g_free (system_themes);
}
g_strfreev (data_dirs);
}
else
{
system_data_dirs = g_get_system_data_dirs (); system_data_dirs = g_get_system_data_dirs ();
for (i = 0; system_data_dirs && system_data_dirs[i]; i++) for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
{ {
@@ -645,6 +733,8 @@ zoitechat_gtk3_theme_service_discover (void)
add_theme_root (system_roots, seen_system_roots, system_themes); add_theme_root (system_roots, seen_system_roots, system_themes);
g_free (system_themes); g_free (system_themes);
} }
}
}
for (i = 0; i < system_roots->len; i++) for (i = 0; i < system_roots->len; i++)
discover_dir (themes, seen_theme_roots, g_ptr_array_index (system_roots, i), ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM); discover_dir (themes, seen_theme_roots, g_ptr_array_index (system_roots, i), ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM);
@@ -922,7 +1012,7 @@ static gboolean
validate_theme_root_for_import (const char *theme_root, GError **error) validate_theme_root_for_import (const char *theme_root, GError **error)
{ {
char *index_theme; char *index_theme;
GKeyFile *keyfile; GKeyFile *keyfile = NULL;
char *css_dir; char *css_dir;
char *css_path; char *css_path;
char *raw_inherits; char *raw_inherits;
@@ -930,16 +1020,12 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
guint i; guint i;
GError *load_error = NULL; GError *load_error = NULL;
/* index.theme is optional for GTK3 CSS themes (GTK only needs
* gtk-3.x/gtk.css), but when it is present it must be well formed so
* broken archives are rejected up front. */
index_theme = g_build_filename (theme_root, "index.theme", NULL); index_theme = g_build_filename (theme_root, "index.theme", NULL);
if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR)) if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
{ {
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Invalid GTK3 theme at '%s': missing required index.theme at '%s'.",
theme_root, index_theme);
g_free (index_theme);
return FALSE;
}
keyfile = g_key_file_new (); keyfile = g_key_file_new ();
if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error)) if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
{ {
@@ -961,6 +1047,7 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
g_free (index_theme); g_free (index_theme);
return FALSE; return FALSE;
} }
}
css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root); css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root);
if (!css_dir) if (!css_dir)
@@ -968,6 +1055,7 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).", "Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).",
theme_root); theme_root);
if (keyfile)
g_key_file_unref (keyfile); g_key_file_unref (keyfile);
g_free (index_theme); g_free (index_theme);
return FALSE; return FALSE;
@@ -981,6 +1069,7 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
theme_root, css_path); theme_root, css_path);
g_free (css_path); g_free (css_path);
g_free (css_dir); g_free (css_dir);
if (keyfile)
g_key_file_unref (keyfile); g_key_file_unref (keyfile);
g_free (index_theme); g_free (index_theme);
return FALSE; return FALSE;
@@ -988,8 +1077,12 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
g_free (css_path); g_free (css_path);
g_free (css_dir); g_free (css_dir);
raw_inherits = NULL;
if (keyfile)
{
raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL); raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
g_key_file_unref (keyfile); g_key_file_unref (keyfile);
}
g_free (index_theme); g_free (index_theme);
if (!raw_inherits) if (!raw_inherits)
return TRUE; return TRUE;

View File

@@ -109,6 +109,58 @@ static void server_disconnect (session * sess, int sendquit, int err);
static int server_cleanup (server * serv); static int server_cleanup (server * serv);
static void server_connect (server *serv, char *hostname, int port, int no_login); static void server_connect (server *serv, char *hostname, int port, int no_login);
typedef struct
{
server *serv;
char hostname[128];
int port;
} reconnect_password_prompt;
static gboolean
server_login_uses_password_sasl (int logintype)
{
return logintype == LOGIN_SASL ||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
logintype == LOGIN_SASL_SCRAM_SHA_512;
}
static void
server_reconnect_password_cb (int cancel, char *text, void *userdata)
{
reconnect_password_prompt *prompt = userdata;
if (!cancel && is_server (prompt->serv) && !prompt->serv->connected && !prompt->serv->connecting && prompt->serv->server_session)
{
prompt->serv->password[0] = 0;
if (text && *text)
safe_strcpy (prompt->serv->password, text, sizeof (prompt->serv->password));
server_connect (prompt->serv, prompt->hostname, prompt->port, FALSE);
}
g_free (prompt);
}
static gboolean
server_prompt_reconnect_password (server *serv)
{
reconnect_password_prompt *prompt;
char *msg;
ircnet *net = serv->network;
if (!net || !(net->flags & FLAG_PROMPT_PASSWORD) || !server_login_uses_password_sasl (serv->loginmethod))
return FALSE;
prompt = g_new0 (reconnect_password_prompt, 1);
prompt->serv = serv;
safe_strcpy (prompt->hostname, serv->hostname, sizeof (prompt->hostname));
prompt->port = serv->port;
msg = g_strdup_printf (_("Enter SASL password for %s:"), net->name ? net->name : _("this network"));
fe_get_password (msg, server_reconnect_password_cb, prompt);
g_free (msg);
return TRUE;
}
static void static void
write_error (char *message, GError **error) write_error (char *message, GError **error)
{ {
@@ -734,6 +786,7 @@ timeout_auto_reconnect (server *serv)
serv->recondelay_tag = 0; serv->recondelay_tag = 0;
if (!serv->connected && !serv->connecting && serv->server_session) if (!serv->connected && !serv->connecting && serv->server_session)
{ {
if (!server_prompt_reconnect_password (serv))
server_connect (serv, serv->hostname, serv->port, FALSE); server_connect (serv, serv->hostname, serv->port, FALSE);
} }
} }

View File

@@ -381,9 +381,10 @@ test_archive_without_theme_reports_css_error (void)
} }
archive_path = g_build_filename (tmp_root, "invalid-theme.zip", NULL); archive_path = g_build_filename (tmp_root, "invalid-theme.zip", NULL);
command = g_strdup_printf ("cd %s && zip -qr %s .", archive_root, archive_path); command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", archive_root, archive_path);
g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL)); g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
g_free (command); g_free (command);
g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR));
g_assert_false (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, &error)); g_assert_false (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, &error));
g_assert_null (imported_id); g_assert_null (imported_id);
@@ -397,25 +398,31 @@ test_archive_without_theme_reports_css_error (void)
} }
static void static void
test_import_rejects_theme_missing_index_theme (void) test_import_accepts_theme_missing_index_theme (void)
{ {
char *tmp_root; char *tmp_root;
char *src_root; char *src_root;
char *theme_root; char *theme_root;
char *imported_id = NULL; char *imported_id = NULL;
GError *error = NULL; GError *error = NULL;
ZoitechatGtk3Theme *found;
setup_test_xdir (&tmp_root); setup_test_xdir (&tmp_root);
src_root = g_build_filename (tmp_root, "src", NULL); src_root = g_build_filename (tmp_root, "src", NULL);
g_mkdir_with_parents (src_root, 0700); g_mkdir_with_parents (src_root, 0700);
theme_root = make_theme_dir (src_root, "NoIndex", FALSE, FALSE); theme_root = make_theme_dir (src_root, "NoIndex", FALSE, FALSE);
g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error)); /* index.theme is optional for GTK3 CSS themes: gtk-3.x/gtk.css alone
g_assert_null (imported_id); * makes a loadable theme. */
g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL); g_assert_true (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
g_assert_nonnull (g_strstr_len (error->message, -1, "missing required index.theme")); g_assert_no_error (error);
g_assert_nonnull (g_strstr_len (error->message, -1, "NoIndex")); g_assert_nonnull (imported_id);
g_error_free (error);
found = zoitechat_gtk3_theme_find_by_id (imported_id);
g_assert_nonnull (found);
g_assert_true (g_str_has_suffix (found->path, "NoIndex"));
zoitechat_gtk3_theme_free (found);
g_free (imported_id);
g_free (theme_root); g_free (theme_root);
g_free (src_root); g_free (src_root);
@@ -663,9 +670,10 @@ test_zip_import_nested_root (void)
theme = make_theme_dir (nested, "Juno-ocean", TRUE, FALSE); theme = make_theme_dir (nested, "Juno-ocean", TRUE, FALSE);
archive_path = g_build_filename (tmp_root, "themes.zip", NULL); archive_path = g_build_filename (tmp_root, "themes.zip", NULL);
command = g_strdup_printf ("cd %s && zip -qr %s .", zip_root, archive_path); command = g_strdup_printf ("sh -c \"cd '%s' && zip -qr '%s' .\"", zip_root, archive_path);
g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL)); g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
g_free (command); g_free (command);
g_assert_true (g_file_test (archive_path, G_FILE_TEST_IS_REGULAR));
g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL)); g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL));
found = zoitechat_gtk3_theme_find_by_id (imported_id); found = zoitechat_gtk3_theme_find_by_id (imported_id);
@@ -697,7 +705,7 @@ main (int argc, char **argv)
g_test_add_func ("/gtk3-theme-service/zip-import-nested-root", test_zip_import_nested_root); g_test_add_func ("/gtk3-theme-service/zip-import-nested-root", test_zip_import_nested_root);
g_test_add_func ("/gtk3-theme-service/invalid-archive-extract-error", test_invalid_archive_reports_extract_error); g_test_add_func ("/gtk3-theme-service/invalid-archive-extract-error", test_invalid_archive_reports_extract_error);
g_test_add_func ("/gtk3-theme-service/archive-without-theme-css-error", test_archive_without_theme_reports_css_error); g_test_add_func ("/gtk3-theme-service/archive-without-theme-css-error", test_archive_without_theme_reports_css_error);
g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_rejects_theme_missing_index_theme); g_test_add_func ("/gtk3-theme-service/import-missing-index-theme", test_import_accepts_theme_missing_index_theme);
g_test_add_func ("/gtk3-theme-service/import-missing-desktop-entry", test_import_rejects_index_without_desktop_entry); g_test_add_func ("/gtk3-theme-service/import-missing-desktop-entry", test_import_rejects_index_without_desktop_entry);
g_test_add_func ("/gtk3-theme-service/import-unresolved-inherits", test_import_rejects_unresolved_inherits); g_test_add_func ("/gtk3-theme-service/import-unresolved-inherits", test_import_rejects_unresolved_inherits);
return g_test_run (); return g_test_run ();

View File

@@ -472,6 +472,34 @@ get_cpu_arch (void)
return sysinfo_get_build_arch (); return sysinfo_get_build_arch ();
} }
/* RtlGetVersion is used because GetVersionEx reports a version capped by
* the compatibility manifest rather than the real one. */
gboolean
win32_is_windows_8_or_newer (void)
{
typedef LONG (WINAPI *rtl_get_version_func) (PRTL_OSVERSIONINFOW);
rtl_get_version_func rtl_get_version;
RTL_OSVERSIONINFOW version;
HMODULE ntdll;
ntdll = GetModuleHandleW (L"ntdll.dll");
if (ntdll == NULL)
return FALSE;
rtl_get_version = (rtl_get_version_func) GetProcAddress (ntdll, "RtlGetVersion");
if (rtl_get_version == NULL)
return FALSE;
memset (&version, 0, sizeof (version));
version.dwOSVersionInfoSize = sizeof (version);
if (rtl_get_version (&version) != 0)
return FALSE;
/* Windows 8 is 6.2; Windows 7 is 6.1 */
return version.dwMajorVersion > 6 ||
(version.dwMajorVersion == 6 && version.dwMinorVersion >= 2);
}
char * char *
get_sys_str (int with_cpu) get_sys_str (int with_cpu)
{ {

View File

@@ -65,6 +65,7 @@ int waitline (int sok, char *buf, int bufsize, int);
#ifdef WIN32 #ifdef WIN32
int waitline2 (GIOChannel *source, char *buf, int bufsize); int waitline2 (GIOChannel *source, char *buf, int bufsize);
int get_cpu_arch (void); int get_cpu_arch (void);
gboolean win32_is_windows_8_or_newer (void);
#else #else
#define waitline2(source,buf,size) waitline(serv->childread,buf,size,0) #define waitline2(source,buf,size) waitline(serv->childread,buf,size,0)
#endif #endif

View File

@@ -176,12 +176,12 @@ emoji_picker_ensure_css (void)
} }
static GtkWidget * static GtkWidget *
emoji_label_new (const char *sequence, gboolean small) emoji_label_new (const char *sequence, gboolean small_cell)
{ {
GtkWidget *label = gtk_label_new (sequence); GtkWidget *label = gtk_label_new (sequence);
gtk_style_context_add_class (gtk_widget_get_style_context (label), gtk_style_context_add_class (gtk_widget_get_style_context (label),
small ? "zc-emoji-cell-small" : "zc-emoji-cell"); small_cell ? "zc-emoji-cell-small" : "zc-emoji-cell");
return label; return label;
} }

View File

@@ -34,10 +34,6 @@
#include <unistd.h> #include <unistd.h>
#endif #endif
#ifdef HAVE_FONTCONFIG
#include <fontconfig/fontconfig.h>
#endif
#include "../common/zoitechat.h" #include "../common/zoitechat.h"
#include "../common/fe.h" #include "../common/fe.h"
#include "../common/util.h" #include "../common/util.h"
@@ -106,9 +102,11 @@ static const GOptionEntry gopt_entries[] =
{NULL} {NULL}
}; };
/* The emoji font is only bundled with Windows builds; other platforms
* rely on the operating system's emoji font (e.g. the distribution's
* Noto Color Emoji package on Linux). */
#ifdef WIN32 #ifdef WIN32
static gunichar2 *win32_private_emoji_font; static gunichar2 *win32_private_emoji_font;
#endif
static char * static char *
fe_bundled_emoji_font_path (void) fe_bundled_emoji_font_path (void)
@@ -119,7 +117,6 @@ fe_bundled_emoji_font_path (void)
override_path = g_getenv ("ZOITECHAT_EMOJI_FONT"); override_path = g_getenv ("ZOITECHAT_EMOJI_FONT");
if (override_path && *override_path) if (override_path && *override_path)
font_path = g_strdup (override_path); font_path = g_strdup (override_path);
#ifdef WIN32
else else
{ {
char *base_path; char *base_path;
@@ -132,19 +129,6 @@ fe_bundled_emoji_font_path (void)
g_free (base_path); g_free (base_path);
} }
} }
#elif defined(HAVE_FONTCONFIG)
else
{
const char *appdir;
appdir = g_getenv ("APPDIR");
if (appdir && *appdir)
font_path = g_build_filename (appdir, "usr", "share", "fonts",
"zoitechat", "NotoColorEmoji.ttf", NULL);
else
font_path = g_strdup (ZOITECHAT_EMOJI_FONT_FILE);
}
#endif
if (font_path && !g_file_test (font_path, G_FILE_TEST_IS_REGULAR)) if (font_path && !g_file_test (font_path, G_FILE_TEST_IS_REGULAR))
{ {
@@ -160,6 +144,15 @@ fe_register_bundled_emoji_font (void)
{ {
char *font_path; char *font_path;
/* Windows 7 cannot render color emoji fonts, so the bundled Noto
* Color Emoji font is not registered there; emoji rendering falls
* back to the monochrome Segoe UI Symbol glyphs instead. */
if (!win32_is_windows_8_or_newer ())
{
g_debug ("Windows 7 detected; using Segoe UI Symbol for emoji");
return;
}
font_path = fe_bundled_emoji_font_path (); font_path = fe_bundled_emoji_font_path ();
if (!font_path) if (!font_path)
{ {
@@ -167,7 +160,6 @@ fe_register_bundled_emoji_font (void)
return; return;
} }
#ifdef WIN32
win32_private_emoji_font = g_utf8_to_utf16 (font_path, -1, NULL, NULL, NULL); win32_private_emoji_font = g_utf8_to_utf16 (font_path, -1, NULL, NULL, NULL);
if (!win32_private_emoji_font || if (!win32_private_emoji_font ||
AddFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL) == 0) AddFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL) == 0)
@@ -175,10 +167,6 @@ fe_register_bundled_emoji_font (void)
g_warning ("Unable to register bundled emoji font: %s", font_path); g_warning ("Unable to register bundled emoji font: %s", font_path);
g_clear_pointer (&win32_private_emoji_font, g_free); g_clear_pointer (&win32_private_emoji_font, g_free);
} }
#elif defined(HAVE_FONTCONFIG)
if (!FcConfigAppFontAddFile (NULL, (const FcChar8 *) font_path))
g_warning ("Unable to register bundled emoji font: %s", font_path);
#endif
g_free (font_path); g_free (font_path);
} }
@@ -186,14 +174,13 @@ fe_register_bundled_emoji_font (void)
static void static void
fe_unregister_bundled_emoji_font (void) fe_unregister_bundled_emoji_font (void)
{ {
#ifdef WIN32
if (win32_private_emoji_font) if (win32_private_emoji_font)
{ {
RemoveFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL); RemoveFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL);
g_clear_pointer (&win32_private_emoji_font, g_free); g_clear_pointer (&win32_private_emoji_font, g_free);
} }
#endif
} }
#endif
#ifdef WIN32 #ifdef WIN32
static void static void
@@ -532,9 +519,9 @@ fe_args (int argc, char *argv[])
return 0; return 0;
} }
#ifdef WIN32
fe_register_bundled_emoji_font (); fe_register_bundled_emoji_font ();
#ifdef WIN32
win32_set_gsettings_schema_dir (); win32_set_gsettings_schema_dir ();
win32_set_appusermodelid (); win32_set_appusermodelid ();
win32_configure_pixbuf_loaders (); win32_configure_pixbuf_loaders ();
@@ -709,7 +696,9 @@ fe_main (void)
#endif #endif
gtk_main (); gtk_main ();
#ifdef WIN32
fe_unregister_bundled_emoji_font (); fe_unregister_bundled_emoji_font ();
#endif
/* sleep for 2 seconds so any QUIT messages are not lost. The */ /* sleep for 2 seconds so any QUIT messages are not lost. The */
/* GUI is closed at this point, so the user doesn't even know! */ /* GUI is closed at this point, so the user doesn't even know! */

View File

@@ -168,7 +168,28 @@ gtkutil_append_font_css (GString *css, const PangoFontDescription *font_desc)
const char *family = pango_font_description_get_family (font_desc); const char *family = pango_font_description_get_family (font_desc);
if (family && *family) if (family && *family)
g_string_append_printf (css, " font-family: \"%s\";", family); {
char **families = g_strsplit (family, ",", -1);
gboolean appended = FALSE;
int i;
g_string_append (css, " font-family:");
for (i = 0; families[i]; i++)
{
char *name = g_strstrip (families[i]);
if (!*name)
continue;
if (appended)
g_string_append (css, ",");
g_string_append_printf (css, " \"%s\"", name);
appended = TRUE;
}
if (appended)
g_string_append (css, ";");
g_strfreev (families);
}
} }
if (mask & PANGO_FONT_MASK_STYLE) if (mask & PANGO_FONT_MASK_STYLE)

View File

@@ -201,7 +201,7 @@ enum
#define TAG_IRC 0 /* server, channel, dialog */ #define TAG_IRC 0 /* server, channel, dialog */
#define TAG_UTIL 1 /* dcc, notify, chanlist */ #define TAG_UTIL 1 /* dcc, notify, chanlist */
static void mg_apply_emoji_fallback_widget (GtkWidget *widget); static void mg_apply_emoji_fallback_widget (GtkWidget *widget, const PangoFontDescription *font);
static void mg_reply_show_child (GtkWidget *widget, gpointer data); static void mg_reply_show_child (GtkWidget *widget, gpointer data);
#define MG_CONFIG_SAVE_DEBOUNCE_MS 250 #define MG_CONFIG_SAVE_DEBOUNCE_MS 250
@@ -3232,7 +3232,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23); gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23);
gtk_widget_set_size_request (gui->key_entry, 58, 11); gtk_widget_set_size_request (gui->key_entry, 58, 11);
gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0); gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0);
mg_apply_emoji_fallback_widget (gui->key_entry); mg_apply_emoji_fallback_widget (gui->key_entry, NULL);
mg_apply_compact_mode_css (gui->key_entry); mg_apply_compact_mode_css (gui->key_entry);
g_signal_connect (G_OBJECT (gui->key_entry), "activate", g_signal_connect (G_OBJECT (gui->key_entry), "activate",
G_CALLBACK (mg_key_entry_cb), NULL); G_CALLBACK (mg_key_entry_cb), NULL);
@@ -3250,7 +3250,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5); gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5);
gtk_widget_set_size_request (gui->limit_entry, 45, 11); gtk_widget_set_size_request (gui->limit_entry, 45, 11);
gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0); gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0);
mg_apply_emoji_fallback_widget (gui->limit_entry); mg_apply_emoji_fallback_widget (gui->limit_entry, NULL);
mg_apply_compact_mode_css (gui->limit_entry); mg_apply_compact_mode_css (gui->limit_entry);
g_signal_connect (G_OBJECT (gui->limit_entry), "activate", g_signal_connect (G_OBJECT (gui->limit_entry), "activate",
G_CALLBACK (mg_limit_entry_cb), NULL); G_CALLBACK (mg_limit_entry_cb), NULL);
@@ -3515,8 +3515,13 @@ mg_apply_session_font_prefs (session_gui *gui)
mg_topicbar_update_height (gui->topic_entry); mg_topicbar_update_height (gui->topic_entry);
} }
if (gui->input_box && prefs.hex_gui_input_style) if (gui->input_box)
{
if (prefs.hex_gui_input_style)
theme_manager_apply_entry_palette (gui->input_box, font); theme_manager_apply_entry_palette (gui->input_box, font);
mg_apply_emoji_fallback_widget (gui->input_box,
prefs.hex_gui_input_style ? font : NULL);
}
if (gui->chanview) if (gui->chanview)
chanview_apply_theme (gui->chanview); chanview_apply_theme (gui->chanview);
@@ -4363,11 +4368,62 @@ mg_inputbox_rightclick (GtkEntry *entry, GtkWidget *menu)
* font size/style/weight, and without breaking user-configured fonts. * font size/style/weight, and without breaking user-configured fonts.
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
static const char *mg_emoji_family_fallback = static const char *
mg_emoji_family_fallback (void)
{
#ifdef G_OS_WIN32 #ifdef G_OS_WIN32
"Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color"; static char family_list[sizeof prefs.hex_text_font_alternative + 32];
if (!win32_is_windows_8_or_newer ())
{
if (prefs.hex_text_font_alternative[0])
{
g_snprintf (family_list, sizeof family_list, "%s, Segoe UI Symbol", prefs.hex_text_font_alternative);
return family_list;
}
return "Segoe UI Symbol";
}
return "Noto Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
#else #else
"Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color"; return "Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
#endif
}
#ifdef G_OS_WIN32
/* Only used by the Windows 7 fallback to GTK's built-in emoji chooser */
static const char *
mg_find_available_icon_name (const char *const *icon_names)
{
GtkIconTheme *theme;
int i;
theme = gtk_icon_theme_get_default ();
if (!theme || !icon_names)
return NULL;
for (i = 0; icon_names[i] != NULL; i++)
{
int action;
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
return icon_names[i];
if (icon_resolver_menu_action_from_name (icon_names[i], &action))
{
char *resource_path = icon_resolver_resolve_path (ICON_RESOLVER_ROLE_MENU_ACTION, action, GTK_ICON_SIZE_MENU, "menu", ICON_RESOLVER_THEME_SYSTEM, NULL);
if (resource_path)
{
g_free (resource_path);
return icon_names[i];
}
}
}
return NULL;
}
#endif #endif
static gboolean static gboolean
@@ -4403,14 +4459,14 @@ mg_fontdesc_with_fallback (const PangoFontDescription *base_desc, gboolean emoji
if (emoji_first) if (emoji_first)
{ {
family_list = g_strdup_printf ("%s, %s", family_list = g_strdup_printf ("%s, %s",
mg_emoji_family_fallback, mg_emoji_family_fallback (),
(base_family && *base_family) ? base_family : "Sans"); (base_family && *base_family) ? base_family : "Sans");
} }
else else
{ {
family_list = g_strdup_printf ("%s, %s", family_list = g_strdup_printf ("%s, %s",
(base_family && *base_family) ? base_family : "Sans", (base_family && *base_family) ? base_family : "Sans",
mg_emoji_family_fallback); mg_emoji_family_fallback ());
} }
pango_font_description_set_family (desc, family_list); pango_font_description_set_family (desc, family_list);
@@ -4420,7 +4476,7 @@ mg_fontdesc_with_fallback (const PangoFontDescription *base_desc, gboolean emoji
} }
static void static void
mg_apply_emoji_fallback_widget (GtkWidget *widget) mg_apply_emoji_fallback_widget (GtkWidget *widget, const PangoFontDescription *font)
{ {
PangoFontDescription *desc; PangoFontDescription *desc;
GtkStyleContext *context; GtkStyleContext *context;
@@ -4433,6 +4489,9 @@ mg_apply_emoji_fallback_widget (GtkWidget *widget)
if (!context) if (!context)
return; return;
if (font)
base_desc = pango_font_description_copy (font);
else
gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL, gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL,
"font", &base_desc, "font", &base_desc,
NULL); NULL);
@@ -4640,7 +4699,7 @@ mg_create_search(session *sess, GtkWidget *box)
gui->shentry = entry = gtk_entry_new(); gui->shentry = entry = gtk_entry_new();
gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0);
gtk_widget_set_size_request (gui->shentry, 180, -1); gtk_widget_set_size_request (gui->shentry, 180, -1);
mg_apply_emoji_fallback_widget (entry); mg_apply_emoji_fallback_widget (entry, NULL);
mg_apply_entry_scroll_artifact_fix (entry); mg_apply_entry_scroll_artifact_fix (entry);
gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess); gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess);
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_esc), sess); g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_esc), sess);
@@ -4748,8 +4807,40 @@ mg_create_entry (session *sess, GtkWidget *box)
if (prefs.hex_gui_input_style) if (prefs.hex_gui_input_style)
mg_apply_entry_style (entry); mg_apply_entry_style (entry);
mg_apply_emoji_fallback_widget (entry,
prefs.hex_gui_input_style && input_style ? input_style->font_desc : NULL);
mg_apply_entry_scroll_artifact_fix (entry); mg_apply_entry_scroll_artifact_fix (entry);
#ifdef G_OS_WIN32
/* Windows 7 cannot render the color emoji font that ZoiteChat's own
* picker is designed around, so it keeps the GTK built-in emoji
* chooser that was used before the picker existed (the HexChat-era
* behavior, which works on Windows 7). */
if (!win32_is_windows_8_or_newer ())
{
const char *emoji_fallback_icon_names[] = {
"face-smile-symbolic",
"face-smile",
"insert-emoticon-symbolic",
"insert-emoticon",
"zc-menu-emoji",
NULL
};
g_object_set (G_OBJECT (entry), "show-emoji-icon", TRUE, NULL);
if (gtk_entry_get_icon_storage_type (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY) == GTK_IMAGE_EMPTY)
{
const char *icon_name = mg_find_available_icon_name (emoji_fallback_icon_names);
if (icon_name)
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY, icon_name);
}
return;
}
#endif
/* ZoiteChat's own emoji picker; deliberately not GTK's /* ZoiteChat's own emoji picker; deliberately not GTK's
* "show-emoji-icon" machinery, so the catalog is the same on every * "show-emoji-icon" machinery, so the catalog is the same on every
* GTK runtime and the button always has a working callback */ * GTK runtime and the button always has a working callback */
@@ -4861,7 +4952,11 @@ mg_create_menu (session_gui *gui, GtkWidget *table, int away_state)
gui->menu_item); gui->menu_item);
gtk_widget_set_hexpand (gui->menu, TRUE); gtk_widget_set_hexpand (gui->menu, TRUE);
gtk_widget_set_vexpand (gui->menu, FALSE); gtk_widget_set_vexpand (gui->menu, FALSE);
gtk_widget_set_halign (gui->menu, GTK_ALIGN_START); /* Fill the row: with GTK_ALIGN_START the menu bar stops after its
* last item and the rest of the row shows bare window background,
* which reads as a two-colored menu bar whenever the theme styles
* menubar and window differently. */
gtk_widget_set_halign (gui->menu, GTK_ALIGN_FILL);
gtk_widget_set_valign (gui->menu, GTK_ALIGN_FILL); gtk_widget_set_valign (gui->menu, GTK_ALIGN_FILL);
gtk_grid_attach (GTK_GRID (table), gui->menu, 0, 0, 3, 1); gtk_grid_attach (GTK_GRID (table), gui->menu, 0, 0, 3, 1);
} }

View File

@@ -49,10 +49,6 @@ zoitechat_gtk_deps = [
libgmodule_dep, # used by libsexy libgmodule_dep, # used by libsexy
] ]
if fontconfig_dep.found()
zoitechat_gtk_deps += fontconfig_dep
endif
gtk_dep = dependency('gtk+-3.0', version: '>= 3.22') gtk_dep = dependency('gtk+-3.0', version: '>= 3.22')
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep] zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep]
@@ -232,6 +228,20 @@ test('Theme Access Routing Tests', theme_access_tests,
timeout: 120, timeout: 120,
) )
theme_gtk3_settings_tests = executable('theme_gtk3_settings_tests',
[
'theme/tests/test-theme-gtk3-settings.c',
'theme/theme-gtk3.c',
],
include_directories: [config_h_include],
dependencies: [gtk_dep],
)
test('Theme GTK3 Settings Tests', theme_gtk3_settings_tests,
protocol: 'tap',
timeout: 120,
)
theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests', theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests',
'theme/tests/test-theme-preferences-gtk3-populate.c', 'theme/tests/test-theme-preferences-gtk3-populate.c',
include_directories: [config_h_include], include_directories: [config_h_include],

View File

@@ -1539,6 +1539,11 @@ tray_cleanup (void)
void void
tray_apply_setup (void) tray_apply_setup (void)
{ {
/* Theme/setup events can be dispatched before the tray plugin has
* been registered; there is nothing to (de)activate yet. */
if (!ph)
return;
if (tray_backend_active) if (tray_backend_active)
{ {
if (!prefs.hex_gui_tray) if (!prefs.hex_gui_tray)

View File

@@ -23,6 +23,7 @@
#include <math.h> #include <math.h>
#include "../theme-access.h" #include "../theme-access.h"
#include "../theme-css.h"
#include "../theme-manager.h" #include "../theme-manager.h"
#include "../theme-runtime.h" #include "../theme-runtime.h"
#include "../../xtext-color.h" #include "../../xtext-color.h"
@@ -54,6 +55,13 @@ setup_apply_real (const ThemeChangedEvent *event)
(void) event; (void) event;
} }
void
gtkutil_append_font_css (GString *css, const PangoFontDescription *font_desc)
{
(void) css;
(void) font_desc;
}
gboolean gboolean
fe_dark_mode_is_enabled_for (unsigned int mode) fe_dark_mode_is_enabled_for (unsigned int mode)
{ {
@@ -173,6 +181,14 @@ theme_gtk3_is_active (void)
return stub_gtk3_active; return stub_gtk3_active;
} }
gboolean
theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color)
{
(void) name;
(void) out_color;
return FALSE;
}
static gboolean static gboolean
rgba_equal (const GdkRGBA *a, const GdkRGBA *b) rgba_equal (const GdkRGBA *a, const GdkRGBA *b)
{ {
@@ -336,6 +352,79 @@ test_access_xtext_palette_widget_mapping_when_gtk3_active (void)
g_object_unref (provider); g_object_unref (provider);
} }
static void
test_access_palette_css_does_not_feed_back_into_sampling (void)
{
GtkWidget *window;
GtkWidget *label;
GtkCssProvider *theme1;
GtkCssProvider *theme2;
GdkScreen *screen;
XTextColor palette[2] = { 0 };
GdkRGBA old_bg;
GdkRGBA old_fg;
GdkRGBA expected;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
reset_stubs ();
stub_gtk3_active = TRUE;
screen = gdk_screen_get_default ();
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
label = gtk_label_new ("mapped");
gtk_container_add (GTK_CONTAINER (window), label);
gtk_widget_realize (window);
/* Old GTK3 theme active at screen level. */
theme1 = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme1,
"label { color: #112233; background-color: #445566; }"
"label:selected { color: #778899; background-color: #aabbcc; }",
-1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme1), GTK_STYLE_PROVIDER_PRIORITY_USER);
/* The app paints the sampled theme colors onto the widget, like the
* user list / channel tree / input box palette provider does. */
g_assert_true (gdk_rgba_parse (&old_bg, "#445566"));
g_assert_true (gdk_rgba_parse (&old_fg, "#112233"));
theme_css_apply_palette_widget (label, &old_bg, &old_fg, NULL);
/* The user switches to a new GTK3 theme. */
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (theme1));
theme2 = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme2,
"label { color: #00ff00; background-color: #ff0000; }"
"label:selected { color: #0000ff; background-color: #ffff00; }",
-1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme2), GTK_STYLE_PROVIDER_PRIORITY_USER);
gtk_style_context_reset_widgets (screen);
/* Re-sampling must see the new theme, not the palette colors the
* app applied for the old theme. */
theme_get_xtext_colors_for_widget (label, palette, G_N_ELEMENTS (palette));
g_assert_true (stub_last_gtk_map_valid);
g_assert_true (gdk_rgba_parse (&expected, "#00ff00"));
g_assert_true (rgba_close (&stub_last_gtk_map.text_foreground, &expected));
g_assert_true (gdk_rgba_parse (&expected, "#ff0000"));
g_assert_true (rgba_close (&stub_last_gtk_map.text_background, &expected));
/* Sampling must not permanently strip the palette class. */
g_assert_true (gtk_style_context_has_class (gtk_widget_get_style_context (label),
theme_css_palette_class_name ()));
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (theme2));
gtk_widget_destroy (window);
g_object_unref (theme1);
g_object_unref (theme2);
}
static void static void
test_access_dark_light_switch_affects_token_consumers (void) test_access_dark_light_switch_affects_token_consumers (void)
{ {
@@ -363,6 +452,8 @@ main (int argc, char **argv)
g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding); g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding);
g_test_add_func ("/theme/access/xtext_palette_widget_mapping_when_gtk3_active", g_test_add_func ("/theme/access/xtext_palette_widget_mapping_when_gtk3_active",
test_access_xtext_palette_widget_mapping_when_gtk3_active); test_access_xtext_palette_widget_mapping_when_gtk3_active);
g_test_add_func ("/theme/access/palette_css_does_not_feed_back_into_sampling",
test_access_palette_css_does_not_feed_back_into_sampling);
g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding); g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding);
g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers", g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers",
test_access_dark_light_switch_affects_token_consumers); test_access_dark_light_switch_affects_token_consumers);

View File

@@ -21,6 +21,7 @@
#include "../../fe-gtk.h" #include "../../fe-gtk.h"
#include "../theme-application.h" #include "../theme-application.h"
#include "../theme-policy.h"
#include "../../maingui.h" #include "../../maingui.h"
#include "../../../common/zoitechat.h" #include "../../../common/zoitechat.h"
#include "../../../common/zoitechatc.h" #include "../../../common/zoitechatc.h"
@@ -54,6 +55,19 @@ theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font
css_reload_calls++; css_reload_calls++;
} }
gboolean
theme_policy_system_prefers_dark (void)
{
return FALSE;
}
gboolean
theme_policy_is_dark_mode_active (unsigned int mode)
{
(void) mode;
return FALSE;
}
void void
theme_runtime_load (void) theme_runtime_load (void)
{ {
@@ -104,7 +118,9 @@ test_invalid_font_falls_back_to_sans_11 (void)
g_assert_cmpint (pango_font_description_get_size (style->font_desc), ==, 11 * PANGO_SCALE); g_assert_cmpint (pango_font_description_get_size (style->font_desc), ==, 11 * PANGO_SCALE);
g_assert_cmpint (message_calls, ==, 1); g_assert_cmpint (message_calls, ==, 1);
g_assert_cmpint (css_reload_calls, ==, 1); g_assert_cmpint (css_reload_calls, ==, 1);
g_assert_true (css_enabled); /* Input boxes are themed per-widget; the app-global input CSS stays
* off so entries render natively under the active GTK3 theme. */
g_assert_false (css_enabled);
g_assert_nonnull (css_font_desc); g_assert_nonnull (css_font_desc);
if (style->font_desc) if (style->font_desc)

View File

@@ -24,6 +24,7 @@
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <gio/gio.h>
#include "../theme-gtk3.h" #include "../theme-gtk3.h"
#include "../../../common/gtk3-theme-service.h" #include "../../../common/gtk3-theme-service.h"
@@ -39,11 +40,14 @@ static char *xdg_data_home;
static char *theme_parent_root; static char *theme_parent_root;
static char *theme_child_root; static char *theme_child_root;
static char *theme_switch_root; static char *theme_switch_root;
static char *theme_chrome_root;
static gboolean stub_system_prefers_dark;
gboolean gboolean
theme_policy_system_prefers_dark (void) theme_policy_system_prefers_dark (void)
{ {
return FALSE; return stub_system_prefers_dark;
} }
static void static void
@@ -131,6 +135,8 @@ zoitechat_gtk3_theme_find_by_id (const char *theme_id)
return make_theme (theme_id, theme_child_root); return make_theme (theme_id, theme_child_root);
if (g_strcmp0 (theme_id, "switch") == 0) if (g_strcmp0 (theme_id, "switch") == 0)
return make_theme (theme_id, theme_switch_root); return make_theme (theme_id, theme_switch_root);
if (g_strcmp0 (theme_id, "chrome") == 0)
return make_theme (theme_id, theme_chrome_root);
return NULL; return NULL;
} }
@@ -177,6 +183,11 @@ zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root)
g_ptr_array_add (chain, g_strdup (theme_switch_root)); g_ptr_array_add (chain, g_strdup (theme_switch_root));
return chain; return chain;
} }
if (g_strcmp0 (theme_root, theme_chrome_root) == 0)
{
g_ptr_array_add (chain, g_strdup (theme_chrome_root));
return chain;
}
g_ptr_array_unref (chain); g_ptr_array_unref (chain);
return NULL; return NULL;
} }
@@ -247,6 +258,22 @@ setup_themes (void)
"gtk-enable-animations=false\n" "gtk-enable-animations=false\n"
"gtk-cursor-blink-time=444\n"); "gtk-cursor-blink-time=444\n");
theme_chrome_root = g_build_filename (temp_root, "chrome", NULL);
g_assert_cmpint (g_mkdir_with_parents (theme_chrome_root, 0700), ==, 0);
ensure_css_dir (theme_chrome_root, "gtk-3.24");
/* Defines named colors but no window/menubar/menu rules of its own,
* so all chrome styling must come from the backfill provider. */
{
char *css = g_build_filename (theme_chrome_root, "gtk-3.24", "gtk.css", NULL);
write_file (css,
"@define-color theme_bg_color #e8f4e8;\n"
"@define-color theme_fg_color #102010;\n"
"@define-color theme_selected_bg_color #3465a4;\n"
"@define-color theme_selected_fg_color #ffffff;\n"
"label { color: #102010; }\n");
g_free (css);
}
path = g_build_filename (theme_parent_root, "index.theme", NULL); path = g_build_filename (theme_parent_root, "index.theme", NULL);
write_file (path, "[Desktop Entry]\nName=parent\n"); write_file (path, "[Desktop Entry]\nName=parent\n");
g_free (path); g_free (path);
@@ -266,11 +293,13 @@ teardown_themes (void)
g_free (theme_parent_root); g_free (theme_parent_root);
g_free (theme_child_root); g_free (theme_child_root);
g_free (theme_switch_root); g_free (theme_switch_root);
g_free (theme_chrome_root);
g_free (xdg_data_home); g_free (xdg_data_home);
g_free (temp_root); g_free (temp_root);
theme_parent_root = NULL; theme_parent_root = NULL;
theme_child_root = NULL; theme_child_root = NULL;
theme_switch_root = NULL; theme_switch_root = NULL;
theme_chrome_root = NULL;
xdg_data_home = NULL; xdg_data_home = NULL;
temp_root = NULL; temp_root = NULL;
} }
@@ -343,6 +372,205 @@ test_settings_restored_on_disable_and_switch (void)
g_assert_false (theme_gtk3_is_active ()); g_assert_false (theme_gtk3_is_active ());
} }
static void
test_follow_system_variant_resolves_to_theme_variant (void)
{
GError *error = NULL;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
/* An explicitly selected in-app theme must not follow the OS
* light/dark preference: the legacy FOLLOW_SYSTEM variant resolves
* to the variant the theme itself provides, so the window chrome
* (menu bar) stays on the theme even when the system prefers dark. */
stub_system_prefers_dark = TRUE;
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_FOLLOW_SYSTEM, &error));
g_assert_no_error (error);
g_assert_true (theme_gtk3_is_active ());
g_assert_cmpint (theme_gtk3_active_variant (), ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
g_assert_false (get_bool_setting ("gtk-application-prefer-dark-theme"));
stub_system_prefers_dark = FALSE;
theme_gtk3_disable ();
}
static void
test_menu_bar_follows_active_theme_colors (void)
{
GError *error = NULL;
GtkWidget *window;
GtkWidget *menubar;
GtkStyleContext *context;
GdkRGBA bg;
GdkRGBA expected;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
/* The chrome theme defines no menubar rules; without the backfill
* provider the menu bar would fall through to the base (OS) theme
* and follow the OS dark/light mode instead of the selected theme. */
g_assert_true (theme_gtk3_apply ("chrome", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
g_assert_no_error (error);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
menubar = gtk_menu_bar_new ();
gtk_container_add (GTK_CONTAINER (window), menubar);
gtk_widget_show_all (window);
context = gtk_widget_get_style_context (menubar);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_true (gdk_rgba_parse (&expected, "#e8f4e8"));
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
/* The window background must follow the theme too, or the space
* around and beside the menu bar keeps the OS theme's colors. */
context = gtk_widget_get_style_context (window);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
gtk_widget_destroy (window);
theme_gtk3_disable ();
}
static void
test_desktop_color_sync_does_not_leak_into_chrome (void)
{
GError *error = NULL;
GtkWidget *window;
GtkWidget *menubar;
GtkStyleContext *context;
GtkCssProvider *desktop_css;
GdkScreen *screen;
GdkRGBA bg;
GdkRGBA expected;
GdkRGBA injected;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
/* KDE's GTK color-scheme sync (~/.config/gtk-3.0/gtk.css) redefines
* the standard named colors with the OS palette at USER priority.
* The switch theme defines no named colors of its own, so a naive
* @theme_bg_color reference in the chrome backfill would resolve to
* the injected OS dark palette; the backfill must use its neutral
* variant fallback instead. */
screen = gdk_screen_get_default ();
desktop_css = gtk_css_provider_new ();
gtk_css_provider_load_from_data (desktop_css,
"@define-color theme_bg_color #1e1e1e;\n"
"@define-color theme_fg_color #eeeeee;\n"
"menubar { background-color: #1e1e1e; }\n",
-1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (desktop_css), GTK_STYLE_PROVIDER_PRIORITY_USER);
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
g_assert_no_error (error);
{
GtkWidget *item;
GdkRGBA item_bg;
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
menubar = gtk_menu_bar_new ();
item = gtk_menu_item_new_with_label ("File");
gtk_menu_shell_append (GTK_MENU_SHELL (menubar), item);
gtk_container_add (GTK_CONTAINER (window), menubar);
gtk_widget_show_all (window);
context = gtk_widget_get_style_context (menubar);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_true (gdk_rgba_parse (&expected, "#f6f5f4"));
g_assert_true (gdk_rgba_parse (&injected, "#1e1e1e"));
g_assert_cmpfloat (ABS (bg.red - injected.red), >, 0.1);
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
/* The menu bar body and the menu items must be one surface. */
context = gtk_widget_get_style_context (item);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &item_bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_cmpfloat (ABS (item_bg.red - bg.red), <, 0.01);
g_assert_cmpfloat (ABS (item_bg.green - bg.green), <, 0.01);
g_assert_cmpfloat (ABS (item_bg.blue - bg.blue), <, 0.01);
}
gtk_widget_destroy (window);
theme_gtk3_disable ();
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (desktop_css));
g_object_unref (desktop_css);
}
static void
test_stale_theme_symlink_is_replaced (void)
{
GError *error = NULL;
char *themes_dir;
char *link_path;
char *target;
char *active_theme_name = NULL;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
if (!has_default_seat ())
{
g_test_message ("No default seat available");
return;
}
/* A dangling symlink left behind by a removed theme must not block
* pinning gtk-theme-name to the newly applied theme. */
themes_dir = g_build_filename (xdg_data_home, "themes", NULL);
g_assert_cmpint (g_mkdir_with_parents (themes_dir, 0700), ==, 0);
link_path = g_build_filename (themes_dir, "switch", NULL);
g_unlink (link_path);
{
GFile *link_file = g_file_new_for_path (link_path);
g_assert_true (g_file_make_symbolic_link (link_file, "/nonexistent/switch", NULL, NULL));
g_object_unref (link_file);
}
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
g_assert_no_error (error);
target = g_file_read_link (link_path, NULL);
g_assert_cmpstr (target, ==, theme_switch_root);
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
g_assert_cmpstr (active_theme_name, ==, "switch");
g_free (active_theme_name);
g_free (target);
g_free (link_path);
g_free (themes_dir);
theme_gtk3_disable ();
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
@@ -354,6 +582,14 @@ main (int argc, char **argv)
g_test_add_func ("/theme/gtk3/settings_layer_precedence", test_settings_layer_precedence); g_test_add_func ("/theme/gtk3/settings_layer_precedence", test_settings_layer_precedence);
g_test_add_func ("/theme/gtk3/settings_restored_on_disable_and_switch", test_settings_restored_on_disable_and_switch); g_test_add_func ("/theme/gtk3/settings_restored_on_disable_and_switch", test_settings_restored_on_disable_and_switch);
g_test_add_func ("/theme/gtk3/follow_system_variant_resolves_to_theme_variant",
test_follow_system_variant_resolves_to_theme_variant);
g_test_add_func ("/theme/gtk3/menu_bar_follows_active_theme_colors",
test_menu_bar_follows_active_theme_colors);
g_test_add_func ("/theme/gtk3/desktop_color_sync_does_not_leak_into_chrome",
test_desktop_color_sync_does_not_leak_into_chrome);
g_test_add_func ("/theme/gtk3/stale_theme_symlink_is_replaced",
test_stale_theme_symlink_is_replaced);
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT; prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;

View File

@@ -89,3 +89,9 @@ theme_gtk3_is_active (void)
{ {
return FALSE; return FALSE;
} }
ThemeGtk3Variant
theme_gtk3_active_variant (void)
{
return THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
}

View File

@@ -22,6 +22,7 @@
#include "../theme-manager.h" #include "../theme-manager.h"
#include "../theme-gtk3.h" #include "../theme-gtk3.h"
#include "../theme-policy.h"
#include "../../../common/zoitechat.h" #include "../../../common/zoitechat.h"
#include "../../../common/zoitechatc.h" #include "../../../common/zoitechatc.h"
@@ -84,6 +85,15 @@ gboolean theme_policy_system_prefers_dark (void)
return stub_system_prefers_dark; return stub_system_prefers_dark;
} }
void theme_policy_init (void)
{
}
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
{
(void) callback;
}
gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
{ {
(void) mode; (void) mode;
@@ -203,6 +213,7 @@ reset_state (void)
idle_add_calls = 0; idle_add_calls = 0;
next_idle_source_id = 33; next_idle_source_id = 33;
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM; prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
prefs.hex_gui_gtk3_theme[0] = '\0';
test_theme_gtk3_stub_reset (); test_theme_gtk3_stub_reset ();
} }
@@ -235,12 +246,14 @@ test_auto_refresh_dispatches_mode_palette_and_style_reasons (void)
} }
static void static void
test_auto_refresh_ignores_non_auto_mode (void) test_auto_refresh_ignores_locked_in_app_theme (void)
{ {
guint listener_id; guint listener_id;
reset_state (); reset_state ();
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
g_strlcpy (prefs.hex_gui_gtk3_theme, "locked-theme", sizeof (prefs.hex_gui_gtk3_theme));
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
stub_apply_mode_palette_changed = TRUE; stub_apply_mode_palette_changed = TRUE;
listener_id = theme_listener_register ("auto.nonauto", auto_listener, NULL); listener_id = theme_listener_register ("auto.nonauto", auto_listener, NULL);
theme_manager_set_idle_add_func (immediate_idle_add); theme_manager_set_idle_add_func (immediate_idle_add);
@@ -254,6 +267,7 @@ test_auto_refresh_ignores_non_auto_mode (void)
theme_manager_set_idle_add_func (NULL); theme_manager_set_idle_add_func (NULL);
theme_listener_unregister (listener_id); theme_listener_unregister (listener_id);
prefs.hex_gui_gtk3_theme[0] = '\0';
} }
static void static void
@@ -263,6 +277,7 @@ test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void)
reset_state (); reset_state ();
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK; prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
g_strlcpy (prefs.hex_gui_gtk3_theme, "system-follower", sizeof (prefs.hex_gui_gtk3_theme));
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM; prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
listener_id = theme_listener_register ("auto.gtk3", auto_listener, NULL); listener_id = theme_listener_register ("auto.gtk3", auto_listener, NULL);
theme_manager_set_idle_add_func (immediate_idle_add); theme_manager_set_idle_add_func (immediate_idle_add);
@@ -274,6 +289,39 @@ test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void)
g_assert_cmpint (listener_calls, ==, 0); g_assert_cmpint (listener_calls, ==, 0);
g_assert_cmpint (test_theme_gtk3_stub_apply_current_calls (), ==, 1); g_assert_cmpint (test_theme_gtk3_stub_apply_current_calls (), ==, 1);
theme_manager_set_idle_add_func (NULL);
theme_listener_unregister (listener_id);
prefs.hex_gui_gtk3_theme[0] = '\0';
}
static void
test_auto_refresh_follows_system_theme_without_auto_mode (void)
{
guint listener_id;
reset_state ();
prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
listener_id = theme_listener_register ("auto.system", auto_listener, NULL);
theme_manager_set_idle_add_func (immediate_idle_add);
theme_manager_refresh_auto_mode ();
/* No in-app theme is selected, so a system theme change must
* re-resolve the mapped palette even outside auto dark mode. */
g_assert_cmpint (idle_add_calls, ==, 1);
g_assert_cmpint (auto_state_calls, ==, 0);
g_assert_cmpint (listener_calls, ==, 1);
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_PALETTE));
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_WIDGET_STYLE));
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_USERLIST));
g_assert_true (theme_changed_event_has_reason (&last_event, THEME_CHANGED_REASON_MODE));
/* Unchanged system state must not dispatch again. */
theme_manager_refresh_auto_mode ();
g_assert_cmpint (idle_add_calls, ==, 2);
g_assert_cmpint (listener_calls, ==, 1);
theme_manager_set_idle_add_func (NULL); theme_manager_set_idle_add_func (NULL);
theme_listener_unregister (listener_id); theme_listener_unregister (listener_id);
} }
@@ -284,9 +332,11 @@ main (int argc, char **argv)
g_test_init (&argc, &argv, NULL); g_test_init (&argc, &argv, NULL);
g_test_add_func ("/theme/manager/auto_refresh_dispatches_mode_palette_and_style_reasons", g_test_add_func ("/theme/manager/auto_refresh_dispatches_mode_palette_and_style_reasons",
test_auto_refresh_dispatches_mode_palette_and_style_reasons); test_auto_refresh_dispatches_mode_palette_and_style_reasons);
g_test_add_func ("/theme/manager/auto_refresh_ignores_non_auto_mode", g_test_add_func ("/theme/manager/auto_refresh_ignores_locked_in_app_theme",
test_auto_refresh_ignores_non_auto_mode); test_auto_refresh_ignores_locked_in_app_theme);
g_test_add_func ("/theme/manager/auto_refresh_reapplies_gtk3_for_follow_system_variant", g_test_add_func ("/theme/manager/auto_refresh_reapplies_gtk3_for_follow_system_variant",
test_auto_refresh_reapplies_gtk3_for_follow_system_variant); test_auto_refresh_reapplies_gtk3_for_follow_system_variant);
g_test_add_func ("/theme/manager/auto_refresh_follows_system_theme_without_auto_mode",
test_auto_refresh_follows_system_theme_without_auto_mode);
return g_test_run (); return g_test_run ();
} }

View File

@@ -23,6 +23,7 @@
#include <string.h> #include <string.h>
#include "../theme-manager.h" #include "../theme-manager.h"
#include "../theme-policy.h"
#include "../../../common/zoitechat.h" #include "../../../common/zoitechat.h"
#include "../../../common/zoitechatc.h" #include "../../../common/zoitechatc.h"
@@ -77,6 +78,15 @@ gboolean theme_policy_system_prefers_dark (void)
return FALSE; return FALSE;
} }
void theme_policy_init (void)
{
}
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
{
(void) callback;
}
gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
{ {
(void) mode; (void) mode;

View File

@@ -22,6 +22,7 @@
#include "../theme-palette.h" #include "../theme-palette.h"
#include "../theme-manager.h" #include "../theme-manager.h"
#include "../theme-policy.h"
#include "../../../common/zoitechat.h" #include "../../../common/zoitechat.h"
#include "../../../common/zoitechatc.h" #include "../../../common/zoitechatc.h"
@@ -30,6 +31,7 @@ struct session *current_tab;
struct session *lastact_sess; struct session *lastact_sess;
struct zoitechatprefs prefs; struct zoitechatprefs prefs;
static gboolean gtk_available;
static gboolean stub_policy_dark; static gboolean stub_policy_dark;
static unsigned int stub_policy_mode; static unsigned int stub_policy_mode;
static gboolean stub_apply_mode_result; static gboolean stub_apply_mode_result;
@@ -79,7 +81,18 @@ void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callbac
gboolean theme_policy_is_dark_mode_active (unsigned int mode) gboolean theme_policy_is_dark_mode_active (unsigned int mode)
{ {
stub_policy_mode = mode; stub_policy_mode = mode;
if (mode == ZOITECHAT_DARK_MODE_AUTO)
return stub_policy_dark; return stub_policy_dark;
return fe_dark_mode_is_enabled_for (mode);
}
void theme_policy_init (void)
{
}
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
{
(void) callback;
} }
gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed) gboolean theme_application_apply_mode (unsigned int mode, gboolean *palette_changed)
@@ -109,11 +122,12 @@ void theme_runtime_reset_mode_colors (gboolean dark_mode)
(void) dark_mode; (void) dark_mode;
} }
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active) gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed)
{ {
(void) mode; (void) mode;
(void) dark_active;
stub_apply_mode_calls++; stub_apply_mode_calls++;
if (palette_changed)
*palette_changed = TRUE;
return TRUE; return TRUE;
} }
@@ -249,10 +263,11 @@ test_manager_set_token_color_routes_by_mode (void)
GdkRGBA color = { 0.1, 0.2, 0.3, 1.0 }; GdkRGBA color = { 0.1, 0.2, 0.3, 1.0 };
gboolean palette_changed = FALSE; gboolean palette_changed = FALSE;
/* All edits target the single user palette; the mode argument is
* accepted for compatibility but no longer selects a dark palette. */
reset_manager_stubs (); reset_manager_stubs ();
stub_policy_dark = FALSE; stub_policy_dark = FALSE;
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, THEME_TOKEN_MIRC_2, &color, &palette_changed); theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, THEME_TOKEN_MIRC_2, &color, &palette_changed);
g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_LIGHT);
g_assert_cmpint (stub_user_set_calls, ==, 1); g_assert_cmpint (stub_user_set_calls, ==, 1);
g_assert_cmpint (stub_dark_set_calls, ==, 0); g_assert_cmpint (stub_dark_set_calls, ==, 0);
g_assert_cmpint (stub_apply_mode_calls, ==, 1); g_assert_cmpint (stub_apply_mode_calls, ==, 1);
@@ -262,16 +277,14 @@ test_manager_set_token_color_routes_by_mode (void)
reset_manager_stubs (); reset_manager_stubs ();
stub_policy_dark = TRUE; stub_policy_dark = TRUE;
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, THEME_TOKEN_MIRC_2, &color, &palette_changed); theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, THEME_TOKEN_MIRC_2, &color, &palette_changed);
g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_DARK); g_assert_cmpint (stub_user_set_calls, ==, 1);
g_assert_cmpint (stub_user_set_calls, ==, 0); g_assert_cmpint (stub_dark_set_calls, ==, 0);
g_assert_cmpint (stub_dark_set_calls, ==, 1);
reset_manager_stubs (); reset_manager_stubs ();
stub_policy_dark = TRUE; stub_policy_dark = TRUE;
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_AUTO, THEME_TOKEN_MIRC_2, &color, &palette_changed); theme_manager_set_token_color (ZOITECHAT_DARK_MODE_AUTO, THEME_TOKEN_MIRC_2, &color, &palette_changed);
g_assert_cmpint (stub_policy_mode, ==, ZOITECHAT_DARK_MODE_AUTO); g_assert_cmpint (stub_user_set_calls, ==, 1);
g_assert_cmpint (stub_user_set_calls, ==, 0); g_assert_cmpint (stub_dark_set_calls, ==, 0);
g_assert_cmpint (stub_dark_set_calls, ==, 1);
} }
@@ -300,9 +313,9 @@ test_manager_set_token_color_routes_setup_indexes (void)
stub_policy_dark = TRUE; stub_policy_dark = TRUE;
palette_changed = FALSE; palette_changed = FALSE;
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, def->token, &color, &palette_changed); theme_manager_set_token_color (ZOITECHAT_DARK_MODE_DARK, def->token, &color, &palette_changed);
g_assert_cmpint (stub_dark_set_calls, ==, 1); g_assert_cmpint (stub_user_set_calls, ==, 1);
g_assert_cmpint (stub_last_dark_token, ==, def->token); g_assert_cmpint (stub_last_user_token, ==, def->token);
g_assert_cmpint (stub_user_set_calls, ==, 0); g_assert_cmpint (stub_dark_set_calls, ==, 0);
g_assert_true (palette_changed); g_assert_true (palette_changed);
} }
} }
@@ -345,6 +358,12 @@ test_manager_window_attach_detach_idempotence (void)
gulong *second_handler_ptr; gulong *second_handler_ptr;
gulong second_handler_id; gulong second_handler_id;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
window = gtk_window_new (GTK_WINDOW_TOPLEVEL); window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
g_assert_nonnull (window); g_assert_nonnull (window);
@@ -395,5 +414,6 @@ main (int argc, char **argv)
test_manager_listener_registration_dispatch_and_unregister); test_manager_listener_registration_dispatch_and_unregister);
g_test_add_func ("/theme/manager/window_attach_detach_idempotence", g_test_add_func ("/theme/manager/window_attach_detach_idempotence",
test_manager_window_attach_detach_idempotence); test_manager_window_attach_detach_idempotence);
gtk_available = gtk_init_check (&argc, &argv);
return g_test_run (); return g_test_run ();
} }

View File

@@ -33,6 +33,7 @@ InputStyle *input_style;
static gboolean gtk_available; static gboolean gtk_available;
static int apply_current_calls; static int apply_current_calls;
static int set_token_color_calls;
static char applied_theme_id[256]; static char applied_theme_id[256];
static ThemeGtk3Variant applied_variant; static ThemeGtk3Variant applied_variant;
static gboolean removed_selected; static gboolean removed_selected;
@@ -104,6 +105,7 @@ theme_manager_set_token_color (unsigned int dark_mode, ThemeSemanticToken token,
(void)dark_mode; (void)dark_mode;
(void)token; (void)token;
(void)color; (void)color;
set_token_color_calls++;
if (changed) if (changed)
*changed = FALSE; *changed = FALSE;
} }
@@ -309,7 +311,7 @@ theme_gtk3_is_active (void)
#include "../theme-preferences.c" #include "../theme-preferences.c"
static void static void
test_removed_selected_theme_commits_fallback_and_applies (void) test_removed_selected_theme_falls_back_to_system_default (void)
{ {
GtkWidget *page; GtkWidget *page;
theme_preferences_ui *ui; theme_preferences_ui *ui;
@@ -326,23 +328,26 @@ test_removed_selected_theme_commits_fallback_and_applies (void)
g_strlcpy (prefs.hex_gui_gtk3_theme, "removed-theme", sizeof (prefs.hex_gui_gtk3_theme)); g_strlcpy (prefs.hex_gui_gtk3_theme, "removed-theme", sizeof (prefs.hex_gui_gtk3_theme));
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_DARK; prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_DARK;
removed_selected = FALSE; removed_selected = FALSE;
apply_current_calls = 0;
applied_theme_id[0] = '\0';
page = theme_preferences_create_page (NULL, &setup_prefs, NULL); page = theme_preferences_create_page (NULL, &setup_prefs, NULL);
ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui"); ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
g_assert_nonnull (ui); g_assert_nonnull (ui);
apply_current_calls = 0;
applied_theme_id[0] = '\0';
g_assert_nonnull (ui->gtk3_remove); g_assert_nonnull (ui->gtk3_remove);
gtk_button_clicked (GTK_BUTTON (ui->gtk3_remove)); gtk_button_clicked (GTK_BUTTON (ui->gtk3_remove));
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme"); /* Removing the active in-app theme returns to the system GTK3
g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "fallback-theme"); * theme ("None") instead of forcing another theme. */
g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT); g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "");
g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT); g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "");
g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
g_assert_cmpint (apply_current_calls, ==, 1); g_assert_cmpint (apply_current_calls, ==, 1);
g_assert_cmpstr (applied_theme_id, ==, "fallback-theme"); g_assert_cmpstr (applied_theme_id, ==, "");
g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT); g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
gtk_widget_destroy (page); gtk_widget_destroy (page);
} }
@@ -416,16 +421,55 @@ test_select_none_resets_theme_and_applies (void)
gtk_widget_destroy (page); gtk_widget_destroy (page);
} }
static void
test_select_theme_does_not_pin_palette_tokens (void)
{
GtkWidget *page;
theme_preferences_ui *ui;
struct zoitechatprefs setup_prefs;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
memset (&setup_prefs, 0, sizeof (setup_prefs));
memset (&prefs, 0, sizeof (prefs));
removed_selected = FALSE;
apply_current_calls = 0;
set_token_color_calls = 0;
page = theme_preferences_create_page (NULL, &setup_prefs, NULL);
ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
g_assert_nonnull (ui);
/* 0 = None, 1 = removed-theme, 2 = fallback-theme */
gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), 2);
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
g_assert_cmpint (apply_current_calls, ==, 1);
/* Selecting a GTK3 theme must leave the mapped palette tokens
* unpinned so the user list, channel tree and input box keep
* resolving their colors from the active theme instead of being
* frozen until the next restart. */
g_assert_cmpint (set_token_color_calls, ==, 0);
gtk_widget_destroy (page);
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
g_test_init (&argc, &argv, NULL); g_test_init (&argc, &argv, NULL);
gtk_available = gtk_init_check (&argc, &argv); gtk_available = gtk_init_check (&argc, &argv);
g_test_add_func ("/theme/preferences/gtk3_removed_selection_applies_fallback", g_test_add_func ("/theme/preferences/gtk3_removed_selection_falls_back_to_system",
test_removed_selected_theme_commits_fallback_and_applies); test_removed_selected_theme_falls_back_to_system_default);
g_test_add_func ("/theme/preferences/gtk3_unset_keeps_system_default", g_test_add_func ("/theme/preferences/gtk3_unset_keeps_system_default",
test_unset_theme_keeps_system_default_without_apply); test_unset_theme_keeps_system_default_without_apply);
g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme", g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme",
test_select_none_resets_theme_and_applies); test_select_none_resets_theme_and_applies);
g_test_add_func ("/theme/preferences/gtk3_select_theme_does_not_pin_palette_tokens",
test_select_theme_does_not_pin_palette_tokens);
return g_test_run (); return g_test_run ();
} }

View File

@@ -290,12 +290,12 @@ test_ui_edits_persist_without_legacy_array_mutation (void)
theme_runtime_save (); theme_runtime_save ();
theme_runtime_load (); theme_runtime_load ();
theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL); theme_runtime_apply_dark_mode (FALSE);
g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &light_loaded)); g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &light_loaded));
g_assert_true (gdk_rgba_parse (&light_expected, "#224466")); g_assert_true (gdk_rgba_parse (&light_expected, "#224466"));
g_assert_true (colors_equal (&light_loaded, &light_expected)); g_assert_true (colors_equal (&light_loaded, &light_expected));
theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_DARK, NULL); theme_runtime_apply_dark_mode (TRUE);
g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &dark_loaded)); g_assert_true (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &dark_loaded));
g_assert_true (gdk_rgba_parse (&dark_expected, "#88aacc")); g_assert_true (gdk_rgba_parse (&dark_expected, "#88aacc"));
g_assert_true (colors_equal (&dark_loaded, &dark_expected)); g_assert_true (colors_equal (&dark_loaded, &dark_expected));

View File

@@ -20,8 +20,9 @@
#include "theme-access.h" #include "theme-access.h"
#include "theme-runtime.h" #include "theme-css.h"
#include "theme-gtk3.h" #include "theme-gtk3.h"
#include "theme-runtime.h"
@@ -32,7 +33,8 @@ enum
THEME_XTEXT_MARK_BG_INDEX = 100, THEME_XTEXT_MARK_BG_INDEX = 100,
THEME_XTEXT_FG_INDEX = 101, THEME_XTEXT_FG_INDEX = 101,
THEME_XTEXT_BG_INDEX = 102, THEME_XTEXT_BG_INDEX = 102,
THEME_XTEXT_MARKER_INDEX = 103 THEME_XTEXT_MARKER_INDEX = 103,
THEME_XTEXT_MARKER_LEGACY_INDEX = 36
}; };
static const guint8 theme_default_99_mirc_colors[THEME_XTEXT_MIRC_COLS][3] = { static const guint8 theme_default_99_mirc_colors[THEME_XTEXT_MIRC_COLS][3] = {
@@ -111,28 +113,95 @@ theme_access_context_get_background_color (GtkStyleContext *context, GtkStateFla
gtk_style_context_restore (context); gtk_style_context_restore (context);
} }
static gboolean
theme_access_lookup_named_color (GtkStyleContext *context, const char *const *names, GdkRGBA *out_color)
{
size_t i;
/* With an in-app GTK3 theme selected, prefer the colors resolved from
* the theme itself: desktop integration CSS (e.g. KDE's color-scheme
* sync) redefines the same named colors with the OS palette in the
* widget cascade, which would drag OS dark/light colors into the
* mapped palette. */
for (i = 0; names[i] != NULL; i++)
{
if (theme_gtk3_lookup_theme_color (names[i], out_color))
return TRUE;
}
for (i = 0; names[i] != NULL; i++)
{
if (gtk_style_context_lookup_color (context, names[i], out_color))
return TRUE;
}
return FALSE;
}
static void
theme_access_resolve_map_background (GtkStyleContext *context, GtkStateFlags state,
const char *const *named_fallbacks, GdkRGBA *out_color)
{
GdkRGBA named;
theme_access_context_get_background_color (context, state, out_color);
if (out_color->alpha > 0.0)
return;
/* Unstyled CSS nodes (e.g. the custom chat text widget) report a fully
* transparent background; fall back to the theme's named colors so the
* mapped palette still follows the active GTK3 theme. */
if (theme_access_lookup_named_color (context, named_fallbacks, &named))
*out_color = named;
}
static gboolean static gboolean
theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map) theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map)
{ {
static const char *const base_fallbacks[] = { "theme_base_color", "theme_bg_color", NULL };
static const char *const selected_bg_fallbacks[] = { "theme_selected_bg_color", NULL };
static const char *const selected_fg_fallbacks[] = { "theme_selected_fg_color", NULL };
GtkStyleContext *context; GtkStyleContext *context;
const char *palette_class;
gboolean had_palette_class;
GdkRGBA named;
GdkRGBA accent; GdkRGBA accent;
g_return_val_if_fail (out_map != NULL, FALSE); g_return_val_if_fail (out_map != NULL, FALSE);
if (!theme_gtk3_is_active () || widget == NULL || !GTK_IS_WIDGET (widget)) if (widget == NULL || !GTK_IS_WIDGET (widget))
return FALSE; return FALSE;
context = gtk_widget_get_style_context (widget); context = gtk_widget_get_style_context (widget);
if (context == NULL) if (context == NULL)
return FALSE; return FALSE;
/* The application paints these widgets with fixed colors through the
* zoitechat-palette provider. Sampling must see the underlying GTK3
* theme, not the colors we applied earlier: otherwise every theme
* change reads back the previous palette and the user list, channel
* tree and input box keep their stale colors until a restart. */
palette_class = theme_css_palette_class_name ();
had_palette_class = gtk_style_context_has_class (context, palette_class);
if (had_palette_class)
gtk_style_context_remove_class (context, palette_class);
theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground); theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground);
theme_access_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_background); theme_access_resolve_map_background (context, GTK_STATE_FLAG_NORMAL, base_fallbacks,
&out_map->text_background);
theme_access_context_get_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_foreground); theme_access_context_get_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_foreground);
theme_access_context_get_background_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_background); theme_access_resolve_map_background (context, GTK_STATE_FLAG_SELECTED, selected_bg_fallbacks,
&out_map->selection_background);
if (gdk_rgba_equal (&out_map->selection_foreground, &out_map->text_foreground) &&
theme_access_lookup_named_color (context, selected_fg_fallbacks, &named))
out_map->selection_foreground = named;
theme_access_context_get_color (context, GTK_STATE_FLAG_LINK, &accent); theme_access_context_get_color (context, GTK_STATE_FLAG_LINK, &accent);
if (accent.alpha <= 0.0) if (accent.alpha <= 0.0)
accent = out_map->selection_background; accent = out_map->selection_background;
out_map->accent = accent; out_map->accent = accent;
if (had_palette_class)
gtk_style_context_add_class (context, palette_class);
out_map->enabled = TRUE; out_map->enabled = TRUE;
return TRUE; return TRUE;
} }
@@ -223,14 +292,35 @@ theme_get_xtext_colors (XTextColor *palette, size_t palette_len)
void void
theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len) theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len)
{ {
ThemeGtkPaletteMap gtk_map = { 0 };
ThemeWidgetStyleValues style_values; ThemeWidgetStyleValues style_values;
GdkRGBA marker_color; GdkRGBA marker_color;
gboolean have_marker = FALSE;
if (!palette) if (!palette)
return; return;
theme_get_widget_style_values_for_widget (widget, &style_values); if (theme_access_get_gtk_palette_map (widget, &gtk_map))
{
theme_runtime_get_widget_style_values_mapped (&gtk_map, &style_values);
theme_runtime_get_xtext_colors_mapped (&gtk_map, palette, palette_len);
}
else
{
theme_runtime_get_widget_style_values (&style_values);
theme_runtime_get_xtext_colors (palette, palette_len); theme_runtime_get_xtext_colors (palette, palette_len);
}
if (palette_len > THEME_XTEXT_MARKER_LEGACY_INDEX)
{
/* The marker token lives at its legacy slot before the extended
* mIRC palette overwrites it; keep the (possibly theme-mapped)
* value for the dedicated marker index below. */
marker_color.red = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].red;
marker_color.green = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].green;
marker_color.blue = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].blue;
marker_color.alpha = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].alpha;
have_marker = TRUE;
}
if (palette_len >= THEME_XTEXT_MIRC_COLS) if (palette_len >= THEME_XTEXT_MIRC_COLS)
theme_access_apply_default_99_palette (palette, palette_len); theme_access_apply_default_99_palette (palette, palette_len);
if (palette_len > THEME_XTEXT_MARK_FG_INDEX) if (palette_len > THEME_XTEXT_MARK_FG_INDEX)
@@ -249,7 +339,7 @@ theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_
} }
if (palette_len > THEME_XTEXT_MARKER_INDEX) if (palette_len > THEME_XTEXT_MARKER_INDEX)
{ {
if (!theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color)) if (!have_marker && !theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color))
marker_color = style_values.selection_background; marker_color = style_values.selection_background;
palette[THEME_XTEXT_MARKER_INDEX].red = marker_color.red; palette[THEME_XTEXT_MARKER_INDEX].red = marker_color.red;
palette[THEME_XTEXT_MARKER_INDEX].green = marker_color.green; palette[THEME_XTEXT_MARKER_INDEX].green = marker_color.green;

View File

@@ -25,6 +25,7 @@
#include "theme-css.h" #include "theme-css.h"
#include "theme-runtime.h" #include "theme-runtime.h"
#include "theme-gtk3.h" #include "theme-gtk3.h"
#include "theme-policy.h"
#include "../maingui.h" #include "../maingui.h"
#include <gtk/gtk.h> #include <gtk/gtk.h>
@@ -55,10 +56,19 @@ theme_application_apply_toplevel_theme (gboolean dark)
if (theme_gtk3_is_active ()) if (theme_gtk3_is_active ())
{ {
if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK) /* An explicitly selected in-app theme controls its own
prefer_dark = TRUE; * variant; the OS light/dark preference must not leak into
else if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_LIGHT) * the window chrome (menu bar, dialogs). The active variant
prefer_dark = FALSE; * is always resolved to light or dark. */
prefer_dark = theme_gtk3_active_variant () == THEME_GTK3_VARIANT_PREFER_DARK;
}
else
{
/* Following the system GTK3 theme: only request the dark
* variant when the dark-mode preference (or the system, in
* auto mode) asks for it, instead of clobbering the value
* that came from the user's GTK configuration. */
prefer_dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
} }
if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings), if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings),

View File

@@ -29,8 +29,6 @@
static const char *theme_css_selector_input = "#zoitechat-inputbox"; static const char *theme_css_selector_input = "#zoitechat-inputbox";
static const char *theme_css_selector_input_text = "#zoitechat-inputbox text"; static const char *theme_css_selector_input_text = "#zoitechat-inputbox text";
static const char *theme_css_selector_palette_class = "zoitechat-palette"; static const char *theme_css_selector_palette_class = "zoitechat-palette";
static const char *theme_css_selector_dark_class = "zoitechat-dark";
static const char *theme_css_selector_light_class = "zoitechat-light";
static const char *theme_css_palette_provider_key = "zoitechat-palette-provider"; static const char *theme_css_palette_provider_key = "zoitechat-palette-provider";
static const guint theme_css_provider_priority = GTK_STYLE_PROVIDER_PRIORITY_USER; static const guint theme_css_provider_priority = GTK_STYLE_PROVIDER_PRIORITY_USER;
static const char *theme_css_palette_scope_selectors = static const char *theme_css_palette_scope_selectors =
@@ -62,6 +60,12 @@ typedef struct
static GtkCssProvider *theme_css_input_provider; static GtkCssProvider *theme_css_input_provider;
static ThemeCssInputFingerprint theme_css_input_fp; static ThemeCssInputFingerprint theme_css_input_fp;
const char *
theme_css_palette_class_name (void)
{
return theme_css_selector_palette_class;
}
void void
theme_css_apply_app_provider (GtkStyleProvider *provider) theme_css_apply_app_provider (GtkStyleProvider *provider)
{ {
@@ -362,61 +366,9 @@ theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkR
char * char *
theme_css_build_toplevel_classes (void) theme_css_build_toplevel_classes (void)
{ {
return g_strdup_printf ( /* Windows, dialogs and menus must render with the colors of the active
"window.%s, window.%s:backdrop, .%s {" * GTK3 theme (system or in-app selected). zoitechat-dark and
"background-color: #202020;" * zoitechat-light are marker classes only; painting fixed colors here
"color: #f0f0f0;" * overrides every theme (issue #334). */
"border-color: #202020;" return g_strdup ("");
"}"
"window.%s menubar, window.%s menubar:backdrop, window.%s menubar box, window.%s menubar box:backdrop, window.%s menuitem, window.%s menuitem:backdrop {"
"background-color: @theme_bg_color;"
"background-image: none;"
"color: @theme_fg_color;"
"border-color: @theme_bg_color;"
"}"
"window.%s menuitem:hover, window.%s menuitem:selected {"
"background-color: @theme_selected_bg_color;"
"background-image: none;"
"color: @theme_selected_fg_color;"
"border-color: @theme_selected_bg_color;"
"}"
"window.%s, window.%s:backdrop, .%s {"
"background-color: #f6f6f6;"
"color: #101010;"
"border-color: #f6f6f6;"
"}"
"window.%s menubar, window.%s menubar:backdrop, window.%s menubar box, window.%s menubar box:backdrop, window.%s menuitem, window.%s menuitem:backdrop {"
"background-color: @theme_bg_color;"
"background-image: none;"
"color: @theme_fg_color;"
"border-color: @theme_bg_color;"
"}"
"window.%s menuitem:hover, window.%s menuitem:selected {"
"background-color: @theme_selected_bg_color;"
"background-image: none;"
"color: @theme_selected_fg_color;"
"border-color: @theme_selected_bg_color;"
"}",
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_dark_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class,
theme_css_selector_light_class);
} }

View File

@@ -30,6 +30,7 @@
* theme_css_apply_widget_provider: * theme_css_apply_widget_provider:
* Use for widget-local CSS providers attached to a specific widget context. * Use for widget-local CSS providers attached to a specific widget context.
*/ */
const char *theme_css_palette_class_name (void);
void theme_css_apply_app_provider (GtkStyleProvider *provider); void theme_css_apply_app_provider (GtkStyleProvider *provider);
void theme_css_remove_app_provider (GtkStyleProvider *provider); void theme_css_remove_app_provider (GtkStyleProvider *provider);
void theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider); void theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider);

View File

@@ -33,11 +33,60 @@
static GPtrArray *theme_gtk3_providers_base; static GPtrArray *theme_gtk3_providers_base;
static GPtrArray *theme_gtk3_providers_variant; static GPtrArray *theme_gtk3_providers_variant;
static GtkCssProvider *theme_gtk3_chrome_provider;
static GHashTable *theme_gtk3_theme_colors;
static GHashTable *theme_gtk3_provider_cache; static GHashTable *theme_gtk3_provider_cache;
static gboolean theme_gtk3_active; static gboolean theme_gtk3_active;
static char *theme_gtk3_current_id; static char *theme_gtk3_current_id;
static ThemeGtk3Variant theme_gtk3_current_variant; static ThemeGtk3Variant theme_gtk3_current_variant;
/* Menu bar and menus fall back to the base gtk-theme-name theme (the OS
* theme) for any property the selected in-app theme's CSS does not define,
* which mixes OS dark/light colors into the chrome (issue: dark menu bar on
* a light in-app theme when the OS is in dark mode). While an in-app theme
* is active, backfill those widgets with the theme's own named colors. The
* provider sits just below the theme providers, so a theme that styles its
* menus keeps full control. */
#define THEME_GTK3_CHROME_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_USER - 1)
/* Concrete color values are formatted in: desktop integration CSS (e.g.
* KDE's color-scheme sync in ~/.config/gtk-3.0/gtk.css) redefines the
* standard named colors with the OS palette at USER priority, so a
* "@theme_bg_color" reference here would resolve to the OS dark/light
* colors instead of the selected theme's. */
static const char theme_gtk3_chrome_css_template[] =
".background, .background:backdrop {"
" background-color: %s;"
" color: %s;"
"}"
"headerbar, headerbar:backdrop, .titlebar, .titlebar:backdrop {"
" background-color: %s;"
" background-image: none;"
" color: %s;"
"}";
/* The menu bar body and the menu items are separate CSS surfaces; desktop
* integration CSS or a theme can style one but not the other, splitting
* the menu bar row into two colors. These rules sit above the theme and
* the desktop CSS so body and items always render as one surface. */
#define THEME_GTK3_MENU_UNIFORM_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_USER + 40)
static const char theme_gtk3_menu_uniform_template[] =
"menubar, menubar:backdrop, menubar > menuitem, menubar > menuitem:backdrop,"
"menu, menu:backdrop, .menu, .menu:backdrop, menu > menuitem, menu > menuitem:backdrop {"
" background-color: %s;"
" background-image: none;"
" color: %s;"
"}"
"menubar > menuitem:hover, menubar > menuitem:selected,"
"menu > menuitem:hover, menu > menuitem:selected,"
".menu menuitem:hover, .menu menuitem:selected {"
" background-color: %s;"
" background-image: none;"
" color: %s;"
"}"
"menubar > menuitem:disabled, menu > menuitem:disabled {"
" color: alpha(%s, 0.5);"
"}";
typedef struct typedef struct
{ {
GHashTable *defaults; GHashTable *defaults;
@@ -49,6 +98,7 @@ typedef struct
static ThemeGtk3SettingsState theme_gtk3_settings_state; static ThemeGtk3SettingsState theme_gtk3_settings_state;
static gboolean settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value); static gboolean settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value);
static void theme_gtk3_menu_uniform_apply (const char *bg, const char *fg, const char *sel_bg, const char *sel_fg);
static gboolean static gboolean
theme_gtk3_theme_name_is_dark (const char *name) theme_gtk3_theme_name_is_dark (const char *name)
@@ -399,6 +449,21 @@ settings_theme_link_search_path (const char *theme_root, const char *theme_name)
} }
link_path = g_build_filename (themes_root, theme_name, NULL); link_path = g_build_filename (themes_root, theme_name, NULL);
/* A leftover symlink from a removed or relocated theme would satisfy
* the EXISTS check while pointing at the wrong (or no) theme, leaving
* the base gtk-theme-name on the OS theme. Replace it. */
{
char *link_target = g_file_read_link (link_path, NULL);
if (link_target)
{
if (g_strcmp0 (link_target, theme_root) != 0 ||
!g_file_test (link_path, G_FILE_TEST_IS_DIR))
g_unlink (link_path);
g_free (link_target);
}
}
if (!g_file_test (link_path, G_FILE_TEST_EXISTS)) if (!g_file_test (link_path, G_FILE_TEST_EXISTS))
{ {
GFile *link_file = g_file_new_for_path (link_path); GFile *link_file = g_file_new_for_path (link_path);
@@ -757,12 +822,176 @@ settings_apply_from_file (const char *theme_root, const char *css_dir)
g_ptr_array_unref (settings_paths); g_ptr_array_unref (settings_paths);
} }
static gboolean
theme_gtk3_providers_define_color (const char *name)
{
char *needle = g_strdup_printf ("@define-color %s ", name);
gboolean found = FALSE;
guint i;
for (i = 0; !found && theme_gtk3_providers_base && i < theme_gtk3_providers_base->len; i++)
{
char *dump = gtk_css_provider_to_string (g_ptr_array_index (theme_gtk3_providers_base, i));
found = dump && strstr (dump, needle) != NULL;
g_free (dump);
}
for (i = 0; !found && theme_gtk3_providers_variant && i < theme_gtk3_providers_variant->len; i++)
{
char *dump = gtk_css_provider_to_string (g_ptr_array_index (theme_gtk3_providers_variant, i));
found = dump && strstr (dump, needle) != NULL;
g_free (dump);
}
g_free (needle);
return found;
}
/* Resolve a named color from the selected theme itself. The lookup
* context layers the theme's providers over the screen cascade, so the
* theme's own @define-color wins; when the theme does not define the
* name at all, fall back to a neutral palette matching the theme's
* variant instead of whatever the desktop injected into the cascade. */
static char *
theme_gtk3_chrome_resolve_color (GtkStyleContext *lookup, const char *name, const char *fallback)
{
GdkRGBA color;
if (!theme_gtk3_providers_define_color (name) ||
!gtk_style_context_lookup_color (lookup, name, &color))
g_assert (gdk_rgba_parse (&color, fallback));
if (!theme_gtk3_theme_colors)
theme_gtk3_theme_colors = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
{
GdkRGBA *stored = g_new (GdkRGBA, 1);
*stored = color;
g_hash_table_insert (theme_gtk3_theme_colors, g_strdup (name), stored);
}
return gdk_rgba_to_string (&color);
}
gboolean
theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color)
{
GdkRGBA *stored;
if (!theme_gtk3_active || !theme_gtk3_theme_colors || !name || !out_color)
return FALSE;
stored = g_hash_table_lookup (theme_gtk3_theme_colors, name);
if (!stored)
return FALSE;
*out_color = *stored;
return TRUE;
}
static void
theme_gtk3_chrome_provider_apply (gboolean prefer_dark)
{
GdkScreen *screen = gdk_screen_get_default ();
GtkStyleContext *lookup;
char *bg;
char *fg;
char *sel_bg;
char *sel_fg;
char *css;
guint i;
if (!screen)
return;
lookup = gtk_style_context_new ();
for (i = 0; theme_gtk3_providers_base && i < theme_gtk3_providers_base->len; i++)
gtk_style_context_add_provider (lookup,
GTK_STYLE_PROVIDER (g_ptr_array_index (theme_gtk3_providers_base, i)),
GTK_STYLE_PROVIDER_PRIORITY_USER);
for (i = 0; theme_gtk3_providers_variant && i < theme_gtk3_providers_variant->len; i++)
gtk_style_context_add_provider (lookup,
GTK_STYLE_PROVIDER (g_ptr_array_index (theme_gtk3_providers_variant, i)),
GTK_STYLE_PROVIDER_PRIORITY_USER);
if (theme_gtk3_theme_colors)
g_hash_table_remove_all (theme_gtk3_theme_colors);
bg = theme_gtk3_chrome_resolve_color (lookup, "theme_bg_color",
prefer_dark ? "#242424" : "#f6f5f4");
fg = theme_gtk3_chrome_resolve_color (lookup, "theme_fg_color",
prefer_dark ? "#eeeeee" : "#2e3436");
sel_bg = theme_gtk3_chrome_resolve_color (lookup, "theme_selected_bg_color", "#3584e4");
sel_fg = theme_gtk3_chrome_resolve_color (lookup, "theme_selected_fg_color", "#ffffff");
/* Not used by the chrome CSS itself, but resolved and stored for the
* mapped-palette sampling fallback (chat area and friends). */
g_free (theme_gtk3_chrome_resolve_color (lookup, "theme_base_color",
prefer_dark ? "#1e1e1e" : "#ffffff"));
g_object_unref (lookup);
css = g_strdup_printf (theme_gtk3_chrome_css_template,
bg, fg,
bg, fg);
if (!theme_gtk3_chrome_provider)
theme_gtk3_chrome_provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme_gtk3_chrome_provider, css, -1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_chrome_provider),
THEME_GTK3_CHROME_PRIORITY);
theme_gtk3_menu_uniform_apply (bg, fg, sel_bg, sel_fg);
g_free (css);
g_free (sel_fg);
g_free (sel_bg);
g_free (fg);
g_free (bg);
}
static GtkCssProvider *theme_gtk3_menu_uniform_provider;
static void
theme_gtk3_menu_uniform_apply (const char *bg, const char *fg, const char *sel_bg, const char *sel_fg)
{
GdkScreen *screen = gdk_screen_get_default ();
char *css;
if (!screen)
return;
css = g_strdup_printf (theme_gtk3_menu_uniform_template, bg, fg, sel_bg, sel_fg, fg);
if (!theme_gtk3_menu_uniform_provider)
theme_gtk3_menu_uniform_provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme_gtk3_menu_uniform_provider, css, -1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_menu_uniform_provider),
THEME_GTK3_MENU_UNIFORM_PRIORITY);
g_free (css);
}
static void
theme_gtk3_chrome_provider_remove (void)
{
GdkScreen *screen = gdk_screen_get_default ();
if (!screen)
return;
if (theme_gtk3_chrome_provider)
gtk_style_context_remove_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_chrome_provider));
if (theme_gtk3_menu_uniform_provider)
gtk_style_context_remove_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_menu_uniform_provider));
}
static void static void
theme_gtk3_remove_provider (void) theme_gtk3_remove_provider (void)
{ {
GdkScreen *screen = gdk_screen_get_default (); GdkScreen *screen = gdk_screen_get_default ();
guint i; guint i;
theme_gtk3_chrome_provider_remove ();
if (screen && theme_gtk3_providers_variant) if (screen && theme_gtk3_providers_variant)
{ {
for (i = 0; i < theme_gtk3_providers_variant->len; i++) for (i = 0; i < theme_gtk3_providers_variant->len; i++)
@@ -866,6 +1095,7 @@ load_css_with_variant (ZoitechatGtk3Theme *theme, ThemeGtk3Variant variant, GErr
} }
g_ptr_array_unref (chain); g_ptr_array_unref (chain);
theme_gtk3_chrome_provider_apply (prefer_dark);
settings_apply_for_variant (variant); settings_apply_for_variant (variant);
theme_gtk3_reset_widgets (); theme_gtk3_reset_widgets ();
theme_gtk3_active = TRUE; theme_gtk3_active = TRUE;
@@ -876,24 +1106,35 @@ static gboolean
theme_gtk3_apply_internal (const char *theme_id, ThemeGtk3Variant variant, gboolean force_reload, GError **error) theme_gtk3_apply_internal (const char *theme_id, ThemeGtk3Variant variant, gboolean force_reload, GError **error)
{ {
ZoitechatGtk3Theme *theme; ZoitechatGtk3Theme *theme;
char *previous_id = g_strdup (theme_gtk3_current_id); char *previous_id;
ThemeGtk3Variant previous_variant = theme_gtk3_current_variant; ThemeGtk3Variant previous_variant;
gboolean had_previous = theme_gtk3_active && previous_id && previous_id[0]; gboolean had_previous;
gboolean ok; gboolean ok;
theme = zoitechat_gtk3_theme_find_by_id (theme_id);
if (!theme)
return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE;
/* An explicitly selected in-app theme pins the rendering to that
* theme: it must not flip with the OS light/dark preference. Legacy
* configs still carry FOLLOW_SYSTEM (the pre-inference default), so
* resolve it to the variant the theme itself provides. */
if (variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
variant = theme_gtk3_infer_variant (theme);
if (!force_reload && if (!force_reload &&
theme_gtk3_active && theme_gtk3_active &&
g_strcmp0 (theme_gtk3_current_id, theme_id) == 0 && g_strcmp0 (theme_gtk3_current_id, theme_id) == 0 &&
theme_gtk3_current_variant == variant) theme_gtk3_current_variant == variant)
return TRUE;
theme = zoitechat_gtk3_theme_find_by_id (theme_id);
if (!theme)
{ {
g_free (previous_id); zoitechat_gtk3_theme_free (theme);
return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE; return TRUE;
} }
previous_id = g_strdup (theme_gtk3_current_id);
previous_variant = theme_gtk3_current_variant;
had_previous = theme_gtk3_active && previous_id && previous_id[0];
theme_gtk3_remove_provider (); theme_gtk3_remove_provider ();
if (force_reload) if (force_reload)
theme_gtk3_invalidate_provider_cache (); theme_gtk3_invalidate_provider_cache ();
@@ -961,6 +1202,12 @@ void
theme_gtk3_disable (void) theme_gtk3_disable (void)
{ {
theme_gtk3_remove_provider (); theme_gtk3_remove_provider ();
g_clear_object (&theme_gtk3_chrome_provider);
g_clear_pointer (&theme_gtk3_theme_colors, g_hash_table_destroy);
/* Following the system theme: keep the menu bar body and the menu
* items on one surface too, using the system's own colors. */
theme_gtk3_menu_uniform_apply ("@theme_bg_color", "@theme_fg_color",
"@theme_selected_bg_color", "@theme_selected_fg_color");
g_clear_pointer (&theme_gtk3_current_id, g_free); g_clear_pointer (&theme_gtk3_current_id, g_free);
theme_gtk3_invalidate_provider_cache (); theme_gtk3_invalidate_provider_cache ();
g_clear_pointer (&theme_gtk3_provider_cache, g_hash_table_destroy); g_clear_pointer (&theme_gtk3_provider_cache, g_hash_table_destroy);
@@ -990,3 +1237,13 @@ theme_gtk3_is_active (void)
{ {
return theme_gtk3_active; return theme_gtk3_active;
} }
ThemeGtk3Variant
theme_gtk3_active_variant (void)
{
/* Always resolved to PREFER_LIGHT or PREFER_DARK while a theme is
* active; FOLLOW_SYSTEM only when no in-app theme is applied. */
if (!theme_gtk3_active)
return THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
return theme_gtk3_current_variant;
}

View File

@@ -22,6 +22,7 @@
#define ZOITECHAT_THEME_GTK3_H #define ZOITECHAT_THEME_GTK3_H
#include <glib.h> #include <glib.h>
#include <gdk/gdk.h>
typedef enum typedef enum
{ {
@@ -38,5 +39,7 @@ ThemeGtk3Variant theme_gtk3_variant_for_theme (const char *theme_id);
void theme_gtk3_invalidate_provider_cache (void); void theme_gtk3_invalidate_provider_cache (void);
void theme_gtk3_disable (void); void theme_gtk3_disable (void);
gboolean theme_gtk3_is_active (void); gboolean theme_gtk3_is_active (void);
ThemeGtk3Variant theme_gtk3_active_variant (void);
gboolean theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color);
#endif #endif

View File

@@ -58,10 +58,52 @@ typedef struct
gboolean resolved_dark_preference; gboolean resolved_dark_preference;
char gtk3_theme_id[sizeof prefs.hex_gui_gtk3_theme]; char gtk3_theme_id[sizeof prefs.hex_gui_gtk3_theme];
int gtk3_variant; int gtk3_variant;
char *gtk_theme_name;
} ThemeManagerAutoRefreshCache; } ThemeManagerAutoRefreshCache;
static ThemeManagerAutoRefreshCache theme_manager_auto_refresh_cache; static ThemeManagerAutoRefreshCache theme_manager_auto_refresh_cache;
static void
theme_manager_auto_refresh_cache_reset (void)
{
g_free (theme_manager_auto_refresh_cache.gtk_theme_name);
memset (&theme_manager_auto_refresh_cache, 0, sizeof (theme_manager_auto_refresh_cache));
}
static void
theme_manager_auto_refresh_cache_store (gboolean resolved_dark_preference, char *gtk_theme_name)
{
theme_manager_auto_refresh_cache.initialized = TRUE;
theme_manager_auto_refresh_cache.resolved_dark_preference = resolved_dark_preference;
theme_manager_auto_refresh_cache.gtk3_variant = prefs.hex_gui_gtk3_variant;
g_strlcpy (theme_manager_auto_refresh_cache.gtk3_theme_id,
prefs.hex_gui_gtk3_theme,
sizeof (theme_manager_auto_refresh_cache.gtk3_theme_id));
g_free (theme_manager_auto_refresh_cache.gtk_theme_name);
theme_manager_auto_refresh_cache.gtk_theme_name = gtk_theme_name;
}
static char *
theme_manager_dup_gtk_theme_name (void)
{
GtkSettings *settings = gtk_settings_get_default ();
char *gtk_theme_name = NULL;
if (settings)
g_object_get (settings, "gtk-theme-name", &gtk_theme_name, NULL);
return gtk_theme_name;
}
/* Record the state the application started up with so that the first queued
* auto refresh only reacts to real changes; startup already applied the
* current theme, and consumers such as the tray are not ready yet. */
static void
theme_manager_auto_refresh_cache_prime (void)
{
theme_manager_auto_refresh_cache_store (theme_policy_system_prefers_dark (),
theme_manager_dup_gtk_theme_name ());
}
static void theme_manager_apply_platform_window_theme (GtkWidget *window); static void theme_manager_apply_platform_window_theme (GtkWidget *window);
static void static void
@@ -145,7 +187,9 @@ theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec,
gboolean color_change = FALSE; gboolean color_change = FALSE;
gboolean should_refresh_gtk3; gboolean should_refresh_gtk3;
gboolean gtk3_refresh; gboolean gtk3_refresh;
gboolean follows_system_theme;
gboolean resolved_dark_preference; gboolean resolved_dark_preference;
char *gtk_theme_name = NULL;
static gboolean in_handler = FALSE; static gboolean in_handler = FALSE;
(void) settings; (void) settings;
@@ -154,39 +198,47 @@ theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec,
resolved_dark_preference = theme_policy_system_prefers_dark (); resolved_dark_preference = theme_policy_system_prefers_dark ();
gtk3_refresh = theme_manager_should_refresh_gtk3 (); gtk3_refresh = theme_manager_should_refresh_gtk3 ();
follows_system_theme = prefs.hex_gui_gtk3_theme[0] == '\0';
should_refresh_gtk3 = gtk3_refresh || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO; should_refresh_gtk3 = gtk3_refresh || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO;
gtk_theme_name = theme_manager_dup_gtk_theme_name ();
if (theme_manager_auto_refresh_cache.initialized && if (theme_manager_auto_refresh_cache.initialized &&
theme_manager_auto_refresh_cache.resolved_dark_preference == resolved_dark_preference && theme_manager_auto_refresh_cache.resolved_dark_preference == resolved_dark_preference &&
theme_manager_auto_refresh_cache.gtk3_variant == prefs.hex_gui_gtk3_variant && theme_manager_auto_refresh_cache.gtk3_variant == prefs.hex_gui_gtk3_variant &&
g_strcmp0 (theme_manager_auto_refresh_cache.gtk3_theme_id, prefs.hex_gui_gtk3_theme) == 0) g_strcmp0 (theme_manager_auto_refresh_cache.gtk3_theme_id, prefs.hex_gui_gtk3_theme) == 0 &&
g_strcmp0 (theme_manager_auto_refresh_cache.gtk_theme_name, gtk_theme_name) == 0)
{
g_free (gtk_theme_name);
return; return;
}
theme_manager_auto_refresh_cache.initialized = TRUE; theme_manager_auto_refresh_cache_store (resolved_dark_preference, gtk_theme_name);
theme_manager_auto_refresh_cache.resolved_dark_preference = resolved_dark_preference;
theme_manager_auto_refresh_cache.gtk3_variant = prefs.hex_gui_gtk3_variant;
g_strlcpy (theme_manager_auto_refresh_cache.gtk3_theme_id,
prefs.hex_gui_gtk3_theme,
sizeof (theme_manager_auto_refresh_cache.gtk3_theme_id));
if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh) if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh && !follows_system_theme)
return; return;
if (in_handler) if (in_handler)
return; return;
in_handler = TRUE; in_handler = TRUE;
if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO) if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO || follows_system_theme)
{ {
fe_set_auto_dark_mode_state (resolved_dark_preference);
theme_manager_commit_preferences (prefs.hex_gui_dark_mode, &color_change); theme_manager_commit_preferences (prefs.hex_gui_dark_mode, &color_change);
if (color_change) if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO)
theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE); fe_set_auto_dark_mode_state (resolved_dark_preference);
} }
if (should_refresh_gtk3) if (should_refresh_gtk3)
theme_gtk3_apply_current (NULL); theme_gtk3_apply_current (NULL);
if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO || follows_system_theme)
{
/* The system theme feeds the mapped palette (chat area, user
* list, channel tree), so consumers must re-resolve colors. */
theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE);
}
in_handler = FALSE; in_handler = FALSE;
} }
@@ -204,18 +256,28 @@ theme_manager_run_auto_refresh (gpointer data)
static void static void
theme_manager_queue_auto_refresh (GtkSettings *settings, GParamSpec *pspec, gpointer data) theme_manager_queue_auto_refresh (GtkSettings *settings, GParamSpec *pspec, gpointer data)
{ {
guint source_id;
(void) settings; (void) settings;
(void) pspec; (void) pspec;
if (theme_manager_auto_refresh_source != 0) if (theme_manager_auto_refresh_source != 0)
return; return;
theme_manager_auto_refresh_source = theme_manager_idle_add_func (theme_manager_run_auto_refresh, data); theme_manager_auto_refresh_source = 1;
source_id = theme_manager_idle_add_func (theme_manager_run_auto_refresh, data);
/* An injected idle-add may run the refresh synchronously and already
* have cleared the pending marker; only keep the id while pending. */
if (theme_manager_auto_refresh_source != 0)
theme_manager_auto_refresh_source = source_id;
} }
void void
theme_manager_init (void) theme_manager_init (void)
{ {
static gboolean settings_signals_connected = FALSE;
GtkSettings *settings;
if (!theme_manager_listeners) if (!theme_manager_listeners)
theme_manager_listeners = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, theme_manager_listeners = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
theme_listener_free); theme_listener_free);
@@ -223,10 +285,25 @@ theme_manager_init (void)
if (!theme_manager_setup_listener_id) if (!theme_manager_setup_listener_id)
theme_manager_setup_listener_id = theme_listener_register ("setup.apply", theme_manager_setup_apply_listener, NULL); theme_manager_setup_listener_id = theme_listener_register ("setup.apply", theme_manager_setup_apply_listener, NULL);
fe_set_auto_dark_mode_state (FALSE); theme_policy_init ();
theme_policy_set_appearance_changed_callback (theme_manager_refresh_auto_mode);
settings = gtk_settings_get_default ();
if (settings && !settings_signals_connected)
{
g_signal_connect (settings, "notify::gtk-theme-name",
G_CALLBACK (theme_manager_queue_auto_refresh), NULL);
g_signal_connect (settings, "notify::gtk-application-prefer-dark-theme",
G_CALLBACK (theme_manager_queue_auto_refresh), NULL);
settings_signals_connected = TRUE;
}
fe_set_auto_dark_mode_state (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO ?
theme_policy_system_prefers_dark () : FALSE);
theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL); theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL);
theme_gtk3_init (); theme_gtk3_init ();
zoitechat_set_theme_post_apply_callback (theme_manager_handle_theme_applied); zoitechat_set_theme_post_apply_callback (theme_manager_handle_theme_applied);
theme_manager_auto_refresh_cache_prime ();
} }
gboolean gboolean
@@ -513,13 +590,9 @@ theme_manager_apply_platform_window_theme (GtkWidget *window)
context = gtk_widget_get_style_context (window); context = gtk_widget_get_style_context (window);
if (theme_gtk3_is_active ()) if (theme_gtk3_is_active ())
{ dark = theme_gtk3_active_variant () == THEME_GTK3_VARIANT_PREFER_DARK;
dark = prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK;
if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
dark = theme_policy_system_prefers_dark ();
}
else else
dark = theme_runtime_is_dark_active (); dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
if (context) if (context)
{ {
gtk_style_context_remove_class (context, "zoitechat-dark"); gtk_style_context_remove_class (context, "zoitechat-dark");
@@ -712,4 +785,5 @@ theme_manager_set_idle_add_func (ThemeManagerIdleAddFunc idle_add_func)
{ {
theme_manager_idle_add_func = idle_add_func ? idle_add_func : g_idle_add; theme_manager_idle_add_func = idle_add_func ? idle_add_func : g_idle_add;
theme_manager_auto_refresh_source = 0; theme_manager_auto_refresh_source = 0;
theme_manager_auto_refresh_cache_reset ();
} }

View File

@@ -20,14 +20,379 @@
#include "theme-policy.h" #include "theme-policy.h"
#include <string.h>
#include <gtk/gtk.h>
#include <gio/gio.h>
#include "theme-gtk3.h"
#include "../fe-gtk.h" #include "../fe-gtk.h"
#include "../../common/zoitechat.h" #include "../../common/zoitechat.h"
#include "../../common/zoitechatc.h" #include "../../common/zoitechatc.h"
/* org.freedesktop.appearance color-scheme values (XDG desktop portal) */
enum
{
THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE = 0,
THEME_POLICY_COLOR_SCHEME_PREFER_DARK = 1,
THEME_POLICY_COLOR_SCHEME_PREFER_LIGHT = 2
};
static ThemePolicyAppearanceChangedFunc policy_appearance_changed_cb;
/* GtkSettings state captured before ZoiteChat writes to it, so that values
* coming from the user's own GTK configuration are not mistaken for values
* this application set. */
static gboolean policy_initial_gtk_state_captured;
static gboolean policy_initial_prefer_dark;
static char *policy_initial_theme_name;
#ifndef G_OS_WIN32
static gboolean policy_portal_watch_started;
static gboolean policy_portal_value_valid;
static guint32 policy_portal_color_scheme;
static gboolean policy_gsettings_checked;
static GSettings *policy_gsettings_interface;
#endif
static void
policy_capture_initial_gtk_state (void)
{
GtkSettings *settings;
if (policy_initial_gtk_state_captured)
return;
settings = gtk_settings_get_default ();
if (!settings)
return;
g_object_get (settings,
"gtk-application-prefer-dark-theme", &policy_initial_prefer_dark,
"gtk-theme-name", &policy_initial_theme_name,
NULL);
policy_initial_gtk_state_captured = TRUE;
}
static gboolean
policy_theme_name_is_dark (const char *name)
{
char *lower;
gboolean dark;
if (!name || !name[0])
return FALSE;
lower = g_ascii_strdown (name, -1);
dark = strstr (lower, "dark") != NULL || strstr (lower, "noir") != NULL ||
strstr (lower, "black") != NULL;
g_free (lower);
return dark;
}
#ifndef G_OS_WIN32
static void
policy_notify_appearance_changed (void)
{
if (policy_appearance_changed_cb)
policy_appearance_changed_cb ();
}
static gboolean
policy_color_scheme_from_variant (GVariant *value, guint32 *color_scheme)
{
GVariant *inner;
gboolean ok = FALSE;
if (!value || !color_scheme)
return FALSE;
inner = g_variant_ref (value);
while (g_variant_is_of_type (inner, G_VARIANT_TYPE_VARIANT))
{
GVariant *unwrapped = g_variant_get_variant (inner);
g_variant_unref (inner);
inner = unwrapped;
}
if (g_variant_is_of_type (inner, G_VARIANT_TYPE_UINT32))
{
*color_scheme = g_variant_get_uint32 (inner);
ok = TRUE;
}
g_variant_unref (inner);
return ok;
}
static void
policy_portal_store_color_scheme (guint32 color_scheme)
{
gboolean changed;
changed = !policy_portal_value_valid || policy_portal_color_scheme != color_scheme;
policy_portal_value_valid = TRUE;
policy_portal_color_scheme = color_scheme;
if (changed)
policy_notify_appearance_changed ();
}
static void
policy_portal_setting_changed_cb (GDBusConnection *connection, const char *sender_name,
const char *object_path, const char *interface_name,
const char *signal_name, GVariant *parameters,
gpointer user_data)
{
const char *setting_namespace = NULL;
const char *setting_key = NULL;
GVariant *value = NULL;
guint32 color_scheme = THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE;
(void) connection;
(void) sender_name;
(void) object_path;
(void) interface_name;
(void) signal_name;
(void) user_data;
if (!parameters || !g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ssv)")))
return;
g_variant_get (parameters, "(&s&sv)", &setting_namespace, &setting_key, &value);
if (g_strcmp0 (setting_namespace, "org.freedesktop.appearance") == 0 &&
g_strcmp0 (setting_key, "color-scheme") == 0 &&
policy_color_scheme_from_variant (value, &color_scheme))
policy_portal_store_color_scheme (color_scheme);
if (value)
g_variant_unref (value);
}
static void
policy_portal_read_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GVariant *reply;
GVariant *value = NULL;
guint32 color_scheme = THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE;
(void) user_data;
reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, NULL);
if (!reply)
return;
if (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(v)")))
{
g_variant_get (reply, "(v)", &value);
if (policy_color_scheme_from_variant (value, &color_scheme))
policy_portal_store_color_scheme (color_scheme);
if (value)
g_variant_unref (value);
}
g_variant_unref (reply);
}
static void
policy_portal_bus_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
GDBusConnection *connection;
(void) source;
(void) user_data;
connection = g_bus_get_finish (result, NULL);
if (!connection)
return;
g_dbus_connection_signal_subscribe (connection,
"org.freedesktop.portal.Desktop",
"org.freedesktop.portal.Settings",
"SettingChanged",
"/org/freedesktop/portal/desktop",
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
policy_portal_setting_changed_cb,
NULL,
NULL);
g_dbus_connection_call (connection,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read",
g_variant_new ("(ss)", "org.freedesktop.appearance", "color-scheme"),
G_VARIANT_TYPE ("(v)"),
G_DBUS_CALL_FLAGS_NONE,
3000,
NULL,
policy_portal_read_ready_cb,
NULL);
g_object_unref (connection);
}
static void
policy_portal_watch_start (void)
{
if (policy_portal_watch_started)
return;
policy_portal_watch_started = TRUE;
g_bus_get (G_BUS_TYPE_SESSION, NULL, policy_portal_bus_ready_cb, NULL);
}
static void
policy_gsettings_changed_cb (GSettings *settings, const char *key, gpointer user_data)
{
(void) settings;
(void) key;
(void) user_data;
policy_notify_appearance_changed ();
}
static GSettings *
policy_get_interface_gsettings (void)
{
GSettingsSchemaSource *source;
GSettingsSchema *schema;
gboolean has_key;
if (policy_gsettings_checked)
return policy_gsettings_interface;
policy_gsettings_checked = TRUE;
source = g_settings_schema_source_get_default ();
if (!source)
return NULL;
schema = g_settings_schema_source_lookup (source, "org.gnome.desktop.interface", TRUE);
if (!schema)
return NULL;
/* Requires GLib 2.40, always satisfied by the GTK 3.22 dependency. */
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
has_key = g_settings_schema_has_key (schema, "color-scheme");
G_GNUC_END_IGNORE_DEPRECATIONS
g_settings_schema_unref (schema);
if (!has_key)
return NULL;
policy_gsettings_interface = g_settings_new ("org.gnome.desktop.interface");
g_signal_connect (policy_gsettings_interface, "changed::color-scheme",
G_CALLBACK (policy_gsettings_changed_cb), NULL);
return policy_gsettings_interface;
}
static gboolean
policy_gsettings_color_scheme (gboolean *prefers_dark)
{
GSettings *settings;
char *scheme;
gboolean explicit_preference = FALSE;
settings = policy_get_interface_gsettings ();
if (!settings)
return FALSE;
scheme = g_settings_get_string (settings, "color-scheme");
if (g_strcmp0 (scheme, "prefer-dark") == 0)
{
*prefers_dark = TRUE;
explicit_preference = TRUE;
}
else if (g_strcmp0 (scheme, "prefer-light") == 0)
{
*prefers_dark = FALSE;
explicit_preference = TRUE;
}
g_free (scheme);
return explicit_preference;
}
static gboolean
policy_gtk_heuristic_prefers_dark (void)
{
GtkSettings *settings;
char *theme_name = NULL;
gboolean dark;
policy_capture_initial_gtk_state ();
/* gtk-application-prefer-dark-theme is overwritten by ZoiteChat at
* runtime, so only its captured startup value (settings.ini, XSETTINGS)
* reflects the user's own configuration. */
if (policy_initial_gtk_state_captured && policy_initial_prefer_dark)
return TRUE;
if (theme_gtk3_is_active ())
{
/* gtk-theme-name currently holds the in-app theme override;
* fall back to the captured system theme name. */
theme_name = g_strdup (policy_initial_theme_name);
}
else
{
settings = gtk_settings_get_default ();
if (settings)
g_object_get (settings, "gtk-theme-name", &theme_name, NULL);
}
dark = policy_theme_name_is_dark (theme_name);
g_free (theme_name);
return dark;
}
#endif /* !G_OS_WIN32 */
void
theme_policy_init (void)
{
policy_capture_initial_gtk_state ();
#ifndef G_OS_WIN32
policy_portal_watch_start ();
#endif
}
void
theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback)
{
policy_appearance_changed_cb = callback;
}
gboolean gboolean
theme_policy_system_prefers_dark (void) theme_policy_system_prefers_dark (void)
{ {
#ifdef G_OS_WIN32
gboolean prefer_dark = FALSE;
policy_capture_initial_gtk_state ();
if (fe_win32_high_contrast_is_enabled ())
return FALSE; return FALSE;
if (fe_win32_try_get_system_dark (&prefer_dark))
return prefer_dark;
return policy_theme_name_is_dark (policy_initial_theme_name);
#else
gboolean prefers_dark = FALSE;
policy_capture_initial_gtk_state ();
policy_portal_watch_start ();
if (policy_portal_value_valid &&
policy_portal_color_scheme != THEME_POLICY_COLOR_SCHEME_NO_PREFERENCE)
return policy_portal_color_scheme == THEME_POLICY_COLOR_SCHEME_PREFER_DARK;
if (policy_gsettings_color_scheme (&prefers_dark))
return prefers_dark;
return policy_gtk_heuristic_prefers_dark ();
#endif
} }
gboolean gboolean

View File

@@ -23,6 +23,10 @@
#include "../fe-gtk.h" #include "../fe-gtk.h"
typedef void (*ThemePolicyAppearanceChangedFunc) (void);
void theme_policy_init (void);
void theme_policy_set_appearance_changed_callback (ThemePolicyAppearanceChangedFunc callback);
gboolean theme_policy_system_prefers_dark (void); gboolean theme_policy_system_prefers_dark (void);
gboolean theme_policy_is_dark_mode_active (unsigned int mode); gboolean theme_policy_is_dark_mode_active (unsigned int mode);
gboolean theme_policy_is_app_dark_mode_active (void); gboolean theme_policy_is_app_dark_mode_active (void);

View File

@@ -101,6 +101,9 @@ typedef struct
gboolean changed; gboolean changed;
gboolean snapshot_valid[THEME_TOKEN_COUNT]; gboolean snapshot_valid[THEME_TOKEN_COUNT];
gboolean staged_valid[THEME_TOKEN_COUNT]; gboolean staged_valid[THEME_TOKEN_COUNT];
/* Only explicitly edited tokens are written back to the runtime
* palette; untouched tokens keep following the GTK3 theme. */
gboolean staged_dirty[THEME_TOKEN_COUNT];
GdkRGBA snapshot[THEME_TOKEN_COUNT]; GdkRGBA snapshot[THEME_TOKEN_COUNT];
GdkRGBA staged[THEME_TOKEN_COUNT]; GdkRGBA staged[THEME_TOKEN_COUNT];
} theme_preferences_stage_state; } theme_preferences_stage_state;
@@ -147,7 +150,8 @@ theme_preferences_stage_sync_runtime_to_snapshot (void)
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{ {
if (theme_preferences_stage.snapshot_valid[token]) if (theme_preferences_stage.snapshot_valid[token] &&
theme_preferences_stage.staged_dirty[token])
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token,
&theme_preferences_stage.snapshot[token], NULL); &theme_preferences_stage.snapshot[token], NULL);
} }
@@ -160,7 +164,8 @@ theme_preferences_stage_sync_runtime_to_staged (void)
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{ {
if (theme_preferences_stage.staged_valid[token]) if (theme_preferences_stage.staged_valid[token] &&
theme_preferences_stage.staged_dirty[token])
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token, theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token,
&theme_preferences_stage.staged[token], NULL); &theme_preferences_stage.staged[token], NULL);
} }
@@ -179,6 +184,7 @@ theme_preferences_staged_set_color (ThemeSemanticToken token, const GdkRGBA *rgb
{ {
theme_preferences_stage.staged[token] = *rgba; theme_preferences_stage.staged[token] = *rgba;
theme_preferences_stage.staged_valid[token] = TRUE; theme_preferences_stage.staged_valid[token] = TRUE;
theme_preferences_stage.staged_dirty[token] = TRUE;
theme_preferences_stage_recompute_changed (); theme_preferences_stage_recompute_changed ();
if (color_change_flag) if (color_change_flag)
*color_change_flag = theme_preferences_stage.changed; *color_change_flag = theme_preferences_stage.changed;
@@ -841,6 +847,10 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i
ThemeSemanticToken token; ThemeSemanticToken token;
ThemeWidgetStyleValues style_values; ThemeWidgetStyleValues style_values;
/* The runtime palette was just reset; drop any
* pending edits so the reset is not re-pinned. */
memset (theme_preferences_stage.staged_dirty, 0,
sizeof (theme_preferences_stage.staged_dirty));
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{ {
GdkRGBA rgba; GdkRGBA rgba;
@@ -855,7 +865,6 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i
theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_FOREGROUND] = TRUE; theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_FOREGROUND] = TRUE;
theme_preferences_stage.staged[THEME_TOKEN_TEXT_BACKGROUND] = style_values.background; theme_preferences_stage.staged[THEME_TOKEN_TEXT_BACKGROUND] = style_values.background;
theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_BACKGROUND] = TRUE; theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_BACKGROUND] = TRUE;
theme_preferences_stage_sync_runtime_to_staged ();
theme_preferences_stage_recompute_changed (); theme_preferences_stage_recompute_changed ();
if (ui->color_change_flag) if (ui->color_change_flag)
*ui->color_change_flag = theme_preferences_stage.changed; *ui->color_change_flag = theme_preferences_stage.changed;
@@ -1534,6 +1543,25 @@ theme_preferences_gtk3_sync_remove_state (theme_preferences_ui *ui)
gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER); gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER);
} }
static void
theme_preferences_stage_rebase_color (ThemeSemanticToken token, const GdkRGBA *rgba)
{
if (token < 0 || token >= THEME_TOKEN_COUNT || !rgba)
return;
if (!theme_preferences_stage.active)
return;
/* The theme changed underneath the dialog: shift the baseline so the
* new theme-provided value is neither treated as a user edit nor
* pinned into the runtime palette. */
theme_preferences_stage.snapshot[token] = *rgba;
theme_preferences_stage.staged[token] = *rgba;
theme_preferences_stage.snapshot_valid[token] = TRUE;
theme_preferences_stage.staged_valid[token] = TRUE;
theme_preferences_stage.staged_dirty[token] = FALSE;
theme_preferences_stage_recompute_changed ();
}
static void static void
theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui) theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
{ {
@@ -1549,22 +1577,16 @@ theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
theme_get_widget_style_values_for_widget (style_source, &style_values); theme_get_widget_style_values_for_widget (style_source, &style_values);
theme_preferences_staged_set_color (THEME_TOKEN_TEXT_FOREGROUND, /* Leave the mapped tokens unpinned so the user list, channel tree and
&style_values.foreground, * input box keep resolving their colors from whatever GTK3 theme is
NULL, * active (system or in-app selected), and only refresh what the
TRUE); * dialog shows. Pinning theme-derived values here freezes them into
theme_preferences_staged_set_color (THEME_TOKEN_TEXT_BACKGROUND, * the runtime palette (and colors.conf), which left those widgets
&style_values.background, * with stale colors until the next restart. */
NULL, theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground);
TRUE); theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_BACKGROUND, &style_values.background);
theme_preferences_staged_set_color (THEME_TOKEN_SELECTION_FOREGROUND, theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_FOREGROUND, &style_values.selection_foreground);
&style_values.selection_foreground, theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_BACKGROUND, &style_values.selection_background);
NULL,
TRUE);
theme_preferences_staged_set_color (THEME_TOKEN_SELECTION_BACKGROUND,
&style_values.selection_background,
NULL,
TRUE);
} }
static gboolean static gboolean

View File

@@ -47,6 +47,7 @@
#include "fe-gtk.h" #include "fe-gtk.h"
#include "xtext.h" #include "xtext.h"
#include "fkeys.h" #include "fkeys.h"
#include "gtkutil.h"
#include "theme/theme-access.h" #include "theme/theme-access.h"
#define charlen(str) g_utf8_skip[*(guchar *)(str)] #define charlen(str) g_utf8_skip[*(guchar *)(str)]
@@ -657,6 +658,93 @@ backend_font_open (GtkXText *xtext, char *name)
pango_font_metrics_unref (metrics); pango_font_metrics_unref (metrics);
} }
#ifdef WIN32
static gboolean
xtext_win7_is_emoji_char (gunichar ch)
{
return (ch >= 0x1f000 && ch <= 0x1faff) ||
(ch >= 0x2600 && ch <= 0x27bf) ||
(ch >= 0x2300 && ch <= 0x23ff) ||
(ch >= 0x2b00 && ch <= 0x2bff) ||
(ch >= 0xfe00 && ch <= 0xfe0f) ||
ch == 0x200d;
}
static const char *
xtext_win7_emoji_font_family (void)
{
static char family[FONTNAMELEN + 1];
static char source[sizeof prefs.hex_text_font_alternative];
char **families;
int i;
if (win32_is_windows_8_or_newer ())
return NULL;
if (strcmp (source, prefs.hex_text_font_alternative) == 0 && family[0])
return family;
family[0] = 0;
g_strlcpy (source, prefs.hex_text_font_alternative, sizeof source);
families = g_strsplit (prefs.hex_text_font_alternative, ",", -1);
for (i = 0; families[i]; i++)
{
char *name = g_strstrip (families[i]);
if (*name && gtkutil_find_font (name))
{
g_strlcpy (family, name, sizeof family);
break;
}
}
g_strfreev (families);
if (!family[0] && gtkutil_find_font ("Segoe UI Symbol"))
g_strlcpy (family, "Segoe UI Symbol", sizeof family);
return family[0] ? family : NULL;
}
static PangoAttrList *
xtext_win7_emoji_attrs (const char *str, int len, int emphasis)
{
const char *family;
const char *p;
const char *end;
PangoAttrList *attrs = NULL;
family = xtext_win7_emoji_font_family ();
if (!family)
return attr_lists[emphasis];
p = str;
end = str + len;
while (p < end)
{
const char *next = g_utf8_next_char (p);
gunichar ch = g_utf8_get_char_validated (p, end - p);
if (ch != (gunichar)-1 && ch != (gunichar)-2 && xtext_win7_is_emoji_char (ch))
{
PangoAttribute *attr;
if (!attrs)
attrs = pango_attr_list_copy (attr_lists[emphasis]);
attr = pango_attr_family_new (family);
attr->start_index = p - str;
attr->end_index = next - str;
pango_attr_list_insert (attrs, attr);
}
p = next;
}
return attrs ? attrs : attr_lists[emphasis];
}
#endif
static int static int
backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis) backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis)
{ {
@@ -680,8 +768,18 @@ backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis
deltaw = fontwidths[emphasis][*str]; deltaw = fontwidths[emphasis][*str];
else else
{ {
#ifdef WIN32
PangoAttrList *attrs = xtext_win7_emoji_attrs ((const char *)str, mbl, emphasis);
pango_layout_set_attributes (xtext->layout, attrs);
#endif
pango_layout_set_text (xtext->layout, str, mbl); pango_layout_set_text (xtext->layout, str, mbl);
pango_layout_get_pixel_size (xtext->layout, &deltaw, NULL); pango_layout_get_pixel_size (xtext->layout, &deltaw, NULL);
#ifdef WIN32
if (attrs != attr_lists[emphasis])
pango_attr_list_unref (attrs);
pango_layout_set_attributes (xtext->layout, attr_lists[emphasis]);
#endif
} }
width += deltaw; width += deltaw;
str += mbl; str += mbl;
@@ -715,10 +813,18 @@ backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y,
{ {
cairo_t *cr; cairo_t *cr;
PangoLayoutLine *line; PangoLayoutLine *line;
#ifdef WIN32
PangoAttrList *attrs;
#endif
cr = xtext_create_context (xtext); cr = xtext_create_context (xtext);
#ifdef WIN32
attrs = xtext_win7_emoji_attrs (str, len, emphasis);
pango_layout_set_attributes (xtext->layout, attrs);
#else
pango_layout_set_attributes (xtext->layout, attr_lists[emphasis]); pango_layout_set_attributes (xtext->layout, attr_lists[emphasis]);
#endif
pango_layout_set_text (xtext->layout, str, len); pango_layout_set_text (xtext->layout, str, len);
if (dofill) if (dofill)
@@ -734,7 +840,10 @@ backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y,
cairo_move_to (cr, x, y); cairo_move_to (cr, x, y);
pango_cairo_show_layout_line (cr, line); pango_cairo_show_layout_line (cr, line);
cairo_restore (cr); cairo_restore (cr);
#ifdef WIN32
if (attrs != attr_lists[emphasis])
pango_attr_list_unref (attrs);
#endif
cairo_destroy (cr); cairo_destroy (cr);
} }