diff --git a/.github/workflows/appimage-build.yml b/.github/workflows/appimage-build.yml
index cd6f9dc4..741e6e80 100644
--- a/.github/workflows/appimage-build.yml
+++ b/.github/workflows/appimage-build.yml
@@ -29,6 +29,7 @@ jobs:
build-essential pkg-config meson ninja-build cmake \
gettext \
libcanberra-dev libdbus-glib-1-dev libglib2.0-dev \
+ libarchive-dev \
libgtk-3-dev \
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
libxkbcommon0 \
diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml
index 66067322..16c02cde 100644
--- a/.github/workflows/windows-build.yml
+++ b/.github/workflows/windows-build.yml
@@ -36,30 +36,74 @@ jobs:
run: |
New-Item -Name "deps" -ItemType "Directory" -Force | Out-Null
- Invoke-WebRequest https://files.jrsoftware.org/is/6/innosetup-6.7.0.exe -OutFile deps\innosetup-unicode.exe
+ python -m pip install --upgrade pip
+ python -m pip install cffi
+ python -m pip install zstandard
+
+ $ProgressPreference = 'SilentlyContinue'
+ function Download-WithRetry {
+ param(
+ [string]$Url,
+ [string]$OutFile,
+ [int]$MaxAttempts = 5,
+ [int]$InitialDelaySeconds = 2
+ )
+ for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
+ try {
+ Invoke-WebRequest -Uri $Url -OutFile $OutFile -ErrorAction Stop
+ return
+ }
+ catch {
+ if ($attempt -eq $MaxAttempts) {
+ throw
+ }
+ Start-Sleep -Seconds ($InitialDelaySeconds * [math]::Pow(2, $attempt - 1))
+ }
+ }
+ }
+
+ Download-WithRetry -Url https://files.jrsoftware.org/is/6/innosetup-6.7.0.exe -OutFile deps\innosetup-unicode.exe
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
- Invoke-WebRequest https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/idpsetup-1.5.1.exe -OutFile deps\idpsetup.exe
+ Download-WithRetry -Url https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/idpsetup-1.5.1.exe -OutFile deps\idpsetup.exe
& deps\idpsetup.exe /VERYSILENT
- Invoke-WebRequest https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.0-pre1/GTK3_Gvsbuild_zoitechat-2.18.0-pre1_${{ matrix.platform }}.7z -OutFile deps\gtk-${{ matrix.arch }}.7z
+ Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.18.0-pre1/GTK3_Gvsbuild_zoitechat-2.18.0-pre1_${{ matrix.platform }}.7z -OutFile deps\gtk-${{ matrix.arch }}.7z
& 7z.exe x deps\gtk-${{ matrix.arch }}.7z -oC:\gtk-build\gtk\x64\release
- Invoke-WebRequest https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
+ Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\hicolor-icon-theme.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith('mingw64/share/icons/hicolor/')];tf.close();reader.close();f.close()"
+
+ Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-libarchive-3.8.1-1-any.pkg.tar.zst -OutFile deps\libarchive.pkg.tar.zst
+ python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\libarchive.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith(('mingw64/include/archive','mingw64/lib/libarchive','mingw64/bin/libarchive'))];tf.close();reader.close();f.close()"
+
if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\share\icons\hicolor) {
New-Item -Path C:\gtk-build\gtk\x64\release\share\icons -ItemType Directory -Force | Out-Null
Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\share\icons\hicolor -Destination C:\gtk-build\gtk\x64\release\share\icons\hicolor -Recurse -Force
+ }
+ if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\include) {
+ New-Item -Path C:\gtk-build\gtk\x64\release\include -ItemType Directory -Force | Out-Null
+ Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\include\archive* -Destination C:\gtk-build\gtk\x64\release\include -Recurse -Force
+ }
+ if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\lib) {
+ New-Item -Path C:\gtk-build\gtk\x64\release\lib -ItemType Directory -Force | Out-Null
+ Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\lib\libarchive* -Destination C:\gtk-build\gtk\x64\release\lib -Force
+ }
+ if (Test-Path C:\gtk-build\gtk\x64\release\mingw64\bin) {
+ New-Item -Path C:\gtk-build\gtk\x64\release\bin -ItemType Directory -Force | Out-Null
+ Copy-Item -Path C:\gtk-build\gtk\x64\release\mingw64\bin\libarchive*.dll -Destination C:\gtk-build\gtk\x64\release\bin -Force
+ }
+ if (Test-Path C:\gtk-build\gtk\x64\release\mingw64) {
Remove-Item -Path C:\gtk-build\gtk\x64\release\mingw64 -Recurse -Force
}
- Invoke-WebRequest https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/gendef-20111031.7z -OutFile deps\gendef.7z
+ Download-WithRetry -Url https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/gendef-20111031.7z -OutFile deps\gendef.7z
& 7z.exe x deps\gendef.7z -oC:\gtk-build
- Invoke-WebRequest https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/WinSparkle-20151011.7z -OutFile deps\WinSparkle.7z
+ Download-WithRetry -Url https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/WinSparkle-20151011.7z -OutFile deps\WinSparkle.7z
& 7z.exe x deps\WinSparkle.7z -oC:\gtk-build\WinSparkle
- Invoke-WebRequest https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/perl-5.20.0-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z
+ Download-WithRetry -Url https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.17.0/perl-5.20.0-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z
& 7z.exe x deps\perl-${{ matrix.arch }}.7z -oC:\gtk-build\perl-5.20\${{ matrix.platform }}
$pyRoot = $env:pythonLocation
@@ -72,10 +116,6 @@ jobs:
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
}
- python -m pip install --upgrade pip
- python -m pip install cffi
- python -m pip install zstandard
-
- name: Build
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -90,6 +130,8 @@ jobs:
set "LIB=%PYTHON_DIR%\libs;%LIB%"
set "INCLUDE=%PYTHON_DIR%\include;%INCLUDE%"
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "$archiveLib='C:\gtk-build\gtk\x64\release\lib\libarchive.lib'; if (-not (Test-Path $archiveLib)) { $archiveDll = Get-ChildItem 'C:\gtk-build\gtk\x64\release\bin\libarchive*.dll' | Select-Object -First 1; if ($archiveDll) { Push-Location 'C:\gtk-build\gtk\x64\release\lib'; & 'C:\gtk-build\gendef\gendef.exe' $archiveDll.FullName | Out-Null; $archiveDef = Get-ChildItem 'libarchive*.def' | Select-Object -First 1; if ($archiveDef) { & lib /def:$archiveDef.Name /machine:${{ matrix.platform }} /out:libarchive.lib | Out-Null }; Pop-Location } }"
+
msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
shell: cmd
diff --git a/data/misc/meson.build b/data/misc/meson.build
index a4d4ac70..1e9cdc91 100644
--- a/data/misc/meson.build
+++ b/data/misc/meson.build
@@ -43,10 +43,6 @@ if get_option('gtk-frontend')
install_dir: appdir
)
- install_data('net.zoite.Zoitechat.mime.xml',
- install_dir: mimedir
- )
-
if desktop_utils.found()
test('Validate net.zoite.Zoitechat.desktop', desktop_utils,
args: [zoitechat_desktop]
diff --git a/data/misc/net.zoite.Zoitechat.desktop.in.in b/data/misc/net.zoite.Zoitechat.desktop.in.in
index ecbb1678..fd2522c4 100644
--- a/data/misc/net.zoite.Zoitechat.desktop.in.in
+++ b/data/misc/net.zoite.Zoitechat.desktop.in.in
@@ -11,7 +11,7 @@ Categories=GTK;Network;IRCClient;
StartupNotify=true
StartupWMClass=net.zoite.Zoitechat
X-GNOME-UsesNotifications=true
-MimeType=x-scheme-handler/irc;x-scheme-handler/ircs;application/x-zoitechat-theme;application/x-hexchat-theme;
+MimeType=x-scheme-handler/irc;x-scheme-handler/ircs;
Actions=SafeMode;
[Desktop Action SafeMode]
diff --git a/data/misc/net.zoite.Zoitechat.mime.xml b/data/misc/net.zoite.Zoitechat.mime.xml
index a262c9c2..be4dcc22 100644
--- a/data/misc/net.zoite.Zoitechat.mime.xml
+++ b/data/misc/net.zoite.Zoitechat.mime.xml
@@ -1,11 +1,3 @@
-
- ZoiteChat Theme
-
-
-
- HexChat Theme
-
-
diff --git a/src/common/cfgfiles.c b/src/common/cfgfiles.c
index f6c91574..64d9ca4a 100644
--- a/src/common/cfgfiles.c
+++ b/src/common/cfgfiles.c
@@ -439,6 +439,7 @@ const struct prefs vars[] =
{"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL},
{"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL},
{"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_INT},
+ {"gui_gtk3_variant", P_OFFINT (hex_gui_gtk3_variant), TYPE_INT},
{"gui_tab_layout", P_OFFINT (hex_gui_tab_layout), TYPE_INT},
{"gui_tab_middleclose", P_OFFINT (hex_gui_tab_middleclose), TYPE_BOOL},
{"gui_tab_newtofront", P_OFFINT (hex_gui_tab_newtofront), TYPE_INT},
@@ -568,6 +569,7 @@ const struct prefs vars[] =
{"text_font", P_OFFSET (hex_text_font), TYPE_STR},
{"text_font_main", P_OFFSET (hex_text_font_main), TYPE_STR},
{"text_font_alternative", P_OFFSET (hex_text_font_alternative), TYPE_STR},
+ {"gui_gtk3_theme", P_OFFSET (hex_gui_gtk3_theme), TYPE_STR},
{"text_indent", P_OFFINT (hex_text_indent), TYPE_BOOL},
{"text_max_indent", P_OFFINT (hex_text_max_indent), TYPE_INT},
{"text_max_lines", P_OFFINT (hex_text_max_lines), TYPE_INT},
diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj
index c67f589a..287c159c 100644
--- a/src/common/common.vcxproj
+++ b/src/common/common.vcxproj
@@ -46,6 +46,7 @@
+
@@ -77,6 +78,7 @@
+
@@ -97,7 +99,7 @@
WIN32;_WIN64;_AMD64_;NDEBUG;_LIB;$(OwnFlags);%(PreprocessorDefinitions)
- $(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories)
+ $(ZoiteChatLib);$(DepsRoot)\include;$(ArchiveInclude);$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories)
4267;%(DisableSpecificWarnings)
diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters
index f220d9d3..b9d43fb0 100644
--- a/src/common/common.vcxproj.filters
+++ b/src/common/common.vcxproj.filters
@@ -122,6 +122,9 @@
Header Files
+
+ Header Files
+
@@ -208,6 +211,9 @@
Source Files
+
+ Source Files
+
diff --git a/src/common/gtk3-theme-service.c b/src/common/gtk3-theme-service.c
new file mode 100644
index 00000000..147981cf
--- /dev/null
+++ b/src/common/gtk3-theme-service.c
@@ -0,0 +1,1152 @@
+#include "gtk3-theme-service.h"
+
+#ifndef G_OS_WIN32
+#if defined(__has_include)
+#if __has_include()
+#include
+#include
+#elif __has_include()
+#include
+#include
+#elif __has_include()
+#include
+#include
+#else
+#error "libarchive headers not found"
+#endif
+#else
+#include
+#include
+#endif
+#endif
+#include
+#include
+#include
+#include
+
+#include "util.h"
+#include "cfgfiles.h"
+
+static void
+remove_tree (const char *path)
+{
+ GDir *dir;
+ const char *name;
+
+ if (!g_file_test (path, G_FILE_TEST_EXISTS))
+ return;
+ if (!g_file_test (path, G_FILE_TEST_IS_DIR))
+ {
+ g_remove (path);
+ return;
+ }
+
+ dir = g_dir_open (path, 0, NULL);
+ if (dir)
+ {
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ char *child = g_build_filename (path, name, NULL);
+ remove_tree (child);
+ g_free (child);
+ }
+ g_dir_close (dir);
+ }
+ g_rmdir (path);
+}
+
+static gboolean
+gtk3_css_dir_parse_minor (const char *name, gint *minor)
+{
+ gint parsed_minor = 0;
+
+ if (!g_str_has_prefix (name, "gtk-3"))
+ return FALSE;
+
+ if (name[5] == '\0')
+ {
+ *minor = 0;
+ return TRUE;
+ }
+
+ if (name[5] != '.')
+ return FALSE;
+
+ if (!name[6])
+ return FALSE;
+
+ for (const char *p = name + 6; *p; p++)
+ {
+ if (!g_ascii_isdigit (*p))
+ return FALSE;
+ parsed_minor = (parsed_minor * 10) + (*p - '0');
+ }
+
+ *minor = parsed_minor;
+ return TRUE;
+}
+
+char *
+zoitechat_gtk3_theme_pick_css_dir_for_minor (const char *theme_root, int preferred_minor)
+{
+ GDir *dir;
+ const char *name;
+ char *best_supported = NULL;
+ char *best_fallback = NULL;
+ gint best_supported_minor = G_MININT;
+ gint best_fallback_minor = G_MININT;
+
+ if (!theme_root || !g_file_test (theme_root, G_FILE_TEST_IS_DIR))
+ return NULL;
+
+ dir = g_dir_open (theme_root, 0, NULL);
+ if (!dir)
+ return NULL;
+
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ char *css_path;
+ gint minor = 0;
+
+ if (!gtk3_css_dir_parse_minor (name, &minor))
+ continue;
+
+ css_path = g_build_filename (theme_root, name, "gtk.css", NULL);
+ if (!g_file_test (css_path, G_FILE_TEST_IS_REGULAR))
+ {
+ g_free (css_path);
+ continue;
+ }
+ g_free (css_path);
+
+ if (preferred_minor >= 0 && minor <= preferred_minor)
+ {
+ if (minor > best_supported_minor)
+ {
+ g_free (best_supported);
+ best_supported = g_strdup (name);
+ best_supported_minor = minor;
+ }
+ }
+ if (minor > best_fallback_minor)
+ {
+ g_free (best_fallback);
+ best_fallback = g_strdup (name);
+ best_fallback_minor = minor;
+ }
+ }
+
+ g_dir_close (dir);
+
+ if (best_supported)
+ {
+ g_free (best_fallback);
+ return best_supported;
+ }
+
+ return best_fallback;
+}
+
+char *
+zoitechat_gtk3_theme_pick_css_dir (const char *theme_root)
+{
+ return zoitechat_gtk3_theme_pick_css_dir_for_minor (theme_root, -1);
+}
+
+static gboolean
+path_has_gtk3_css (const char *root)
+{
+ char *css_dir = zoitechat_gtk3_theme_pick_css_dir (root);
+ gboolean ok = css_dir != NULL;
+ g_free (css_dir);
+ return ok;
+}
+
+static char **
+path_read_inherits (const char *theme_root)
+{
+ char *index_theme;
+ GKeyFile *keyfile;
+ char *raw;
+ char **tokens;
+ GPtrArray *parents;
+ guint i;
+
+ if (!theme_root)
+ return NULL;
+
+ index_theme = g_build_filename (theme_root, "index.theme", NULL);
+ if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
+ {
+ g_free (index_theme);
+ return NULL;
+ }
+
+ keyfile = g_key_file_new ();
+ if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, NULL))
+ {
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ return NULL;
+ }
+
+ raw = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ if (!raw)
+ return NULL;
+
+ tokens = g_strsplit_set (raw, ",;", -1);
+ g_free (raw);
+ parents = g_ptr_array_new_with_free_func (g_free);
+
+ for (i = 0; tokens && tokens[i]; i++)
+ {
+ char *name = g_strstrip (tokens[i]);
+ if (name[0] == '\0')
+ continue;
+ g_ptr_array_add (parents, g_strdup (name));
+ }
+
+ g_strfreev (tokens);
+ g_ptr_array_add (parents, NULL);
+ return (char **) g_ptr_array_free (parents, FALSE);
+}
+
+static gboolean
+path_exists_as_dir (const char *path)
+{
+ return path && g_file_test (path, G_FILE_TEST_IS_DIR);
+}
+
+static char *
+resolve_parent_theme_root (const char *child_theme_root, const char *parent_name)
+{
+ char *candidate;
+ char *child_parent;
+ char *user_dir;
+ char *home_themes;
+ char *home_local;
+
+ if (!parent_name || !parent_name[0])
+ return NULL;
+
+ if (g_path_is_absolute (parent_name) && path_exists_as_dir (parent_name))
+ return g_strdup (parent_name);
+
+ child_parent = g_path_get_dirname (child_theme_root);
+ candidate = g_build_filename (child_parent, parent_name, NULL);
+ g_free (child_parent);
+ if (path_exists_as_dir (candidate))
+ return candidate;
+ g_free (candidate);
+
+ candidate = g_build_filename ("/usr/share/themes", parent_name, NULL);
+ if (path_exists_as_dir (candidate))
+ return candidate;
+ g_free (candidate);
+
+ home_themes = g_build_filename (g_get_home_dir (), ".themes", parent_name, NULL);
+ if (path_exists_as_dir (home_themes))
+ return home_themes;
+ g_free (home_themes);
+
+ home_local = g_build_filename (g_get_home_dir (), ".local", "share", "themes", parent_name, NULL);
+ if (path_exists_as_dir (home_local))
+ return home_local;
+ g_free (home_local);
+
+ user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
+ candidate = g_build_filename (user_dir, parent_name, NULL);
+ g_free (user_dir);
+ if (path_exists_as_dir (candidate))
+ return candidate;
+ g_free (candidate);
+
+ return NULL;
+}
+
+static void
+build_inheritance_chain_recursive (const char *theme_root,
+ GPtrArray *ordered_roots,
+ GHashTable *visited)
+{
+ char **parents;
+ guint i;
+
+ if (!theme_root || g_hash_table_contains (visited, theme_root))
+ return;
+
+ g_hash_table_add (visited, g_strdup (theme_root));
+ parents = path_read_inherits (theme_root);
+ for (i = 0; parents && parents[i]; i++)
+ {
+ char *parent_root = resolve_parent_theme_root (theme_root, parents[i]);
+ if (!parent_root)
+ continue;
+ build_inheritance_chain_recursive (parent_root, ordered_roots, visited);
+ g_free (parent_root);
+ }
+ g_strfreev (parents);
+
+ if (path_has_gtk3_css (theme_root))
+ g_ptr_array_add (ordered_roots, g_strdup (theme_root));
+}
+
+GPtrArray *
+zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root)
+{
+ GPtrArray *ordered_roots;
+ GHashTable *visited;
+
+ if (!theme_root || !path_exists_as_dir (theme_root))
+ return NULL;
+
+ ordered_roots = g_ptr_array_new_with_free_func (g_free);
+ visited = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ build_inheritance_chain_recursive (theme_root, ordered_roots, visited);
+ g_hash_table_destroy (visited);
+
+ if (ordered_roots->len == 0)
+ {
+ g_ptr_array_unref (ordered_roots);
+ return NULL;
+ }
+
+ return ordered_roots;
+}
+
+static char *
+path_build_id (const char *path, ZoitechatGtk3ThemeSource source)
+{
+ char *digest = g_compute_checksum_for_string (G_CHECKSUM_SHA1, path, -1);
+ char *id = g_strdup_printf ("%s:%s", source == ZOITECHAT_GTK3_THEME_SOURCE_USER ? "user" : "system", digest);
+ g_free (digest);
+ return id;
+}
+
+static char *
+path_pick_thumbnail (const char *root)
+{
+ static const char *const names[] = {
+ "thumbnail.png",
+ "preview.png",
+ "screenshot.png",
+ NULL
+ };
+ int i;
+
+ for (i = 0; names[i] != NULL; i++)
+ {
+ char *candidate = g_build_filename (root, names[i], NULL);
+ if (g_file_test (candidate, G_FILE_TEST_IS_REGULAR))
+ return candidate;
+ g_free (candidate);
+
+ char *css_dir = zoitechat_gtk3_theme_pick_css_dir (root);
+ if (css_dir)
+ {
+ candidate = g_build_filename (root, css_dir, names[i], NULL);
+ g_free (css_dir);
+ if (g_file_test (candidate, G_FILE_TEST_IS_REGULAR))
+ return candidate;
+ g_free (candidate);
+ }
+ }
+
+ return NULL;
+}
+
+static char *
+path_read_display_name (const char *root)
+{
+ char *index_theme = g_build_filename (root, "index.theme", NULL);
+ GKeyFile *keyfile = g_key_file_new ();
+ char *name = NULL;
+
+ if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR) &&
+ g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, NULL))
+ {
+ name = g_key_file_get_string (keyfile, "Desktop Entry", "Name", NULL);
+ if (!name)
+ name = g_key_file_get_string (keyfile, "X-GNOME-Metatheme", "Name", NULL);
+ }
+
+ if (!name)
+ name = g_path_get_basename (root);
+
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ return name;
+}
+
+void
+zoitechat_gtk3_theme_free (ZoitechatGtk3Theme *theme)
+{
+ if (!theme)
+ return;
+ g_free (theme->id);
+ g_free (theme->display_name);
+ g_free (theme->path);
+ g_free (theme->thumbnail_path);
+ g_free (theme);
+}
+
+char *
+zoitechat_gtk3_theme_service_get_user_themes_dir (void)
+{
+ return g_build_filename (get_xdir (), "gtk3-themes", NULL);
+}
+
+static char *path_normalize_theme_root (const char *path);
+
+static void
+discover_dir (GPtrArray *themes, GHashTable *seen_theme_roots, const char *base_dir, ZoitechatGtk3ThemeSource source)
+{
+ GDir *dir;
+ const char *name;
+
+ if (!g_file_test (base_dir, G_FILE_TEST_IS_DIR))
+ return;
+
+ dir = g_dir_open (base_dir, 0, NULL);
+ if (!dir)
+ return;
+
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ ZoitechatGtk3Theme *theme;
+ char *root;
+ char *dark;
+ char *css_dir;
+
+ if (name[0] == '.')
+ continue;
+
+ root = g_build_filename (base_dir, name, NULL);
+ if (!g_file_test (root, G_FILE_TEST_IS_DIR) || !path_has_gtk3_css (root))
+ {
+ g_free (root);
+ continue;
+ }
+
+ if (seen_theme_roots)
+ {
+ char *canonical_root = path_normalize_theme_root (root);
+ if (g_hash_table_contains (seen_theme_roots, canonical_root))
+ {
+ g_free (canonical_root);
+ g_free (root);
+ continue;
+ }
+ g_hash_table_add (seen_theme_roots, canonical_root);
+ }
+
+ theme = g_new0 (ZoitechatGtk3Theme, 1);
+ theme->path = root;
+ theme->source = source;
+ theme->id = path_build_id (root, source);
+ theme->display_name = path_read_display_name (root);
+ theme->thumbnail_path = path_pick_thumbnail (root);
+ css_dir = zoitechat_gtk3_theme_pick_css_dir (root);
+ dark = css_dir ? g_build_filename (root, css_dir, "gtk-dark.css", NULL) : NULL;
+ theme->has_dark_variant = g_file_test (dark, G_FILE_TEST_IS_REGULAR);
+ g_free (css_dir);
+ g_free (dark);
+ g_ptr_array_add (themes, theme);
+ }
+
+ g_dir_close (dir);
+}
+
+
+static char *
+path_normalize_theme_root (const char *path)
+{
+ char *canonical;
+ char *target;
+
+ if (!path || path[0] == '\0')
+ return NULL;
+
+ canonical = g_canonicalize_filename (path, NULL);
+ target = g_file_read_link (canonical, NULL);
+ if (target && target[0])
+ {
+ char *base = g_path_get_dirname (canonical);
+ char *resolved = g_path_is_absolute (target)
+ ? g_strdup (target)
+ : g_build_filename (base, target, NULL);
+ g_free (canonical);
+ canonical = g_canonicalize_filename (resolved, NULL);
+ g_free (resolved);
+ g_free (base);
+ }
+ g_free (target);
+ return canonical;
+}
+
+static gint
+theme_cmp (gconstpointer a, gconstpointer b)
+{
+ const ZoitechatGtk3Theme *ta = *(const ZoitechatGtk3Theme **) a;
+ const ZoitechatGtk3Theme *tb = *(const ZoitechatGtk3Theme **) b;
+ return g_ascii_strcasecmp (ta->display_name, tb->display_name);
+}
+
+static void
+add_theme_root (GPtrArray *roots, GHashTable *seen, const char *path)
+{
+ char *normalized;
+
+ if (!path || path[0] == '\0')
+ return;
+
+ normalized = g_canonicalize_filename (path, NULL);
+ if (g_hash_table_contains (seen, normalized))
+ {
+ g_free (normalized);
+ return;
+ }
+
+ g_hash_table_add (seen, normalized);
+ g_ptr_array_add (roots, g_strdup (path));
+}
+
+GPtrArray *
+zoitechat_gtk3_theme_service_discover (void)
+{
+ GPtrArray *themes = g_ptr_array_new_with_free_func ((GDestroyNotify) zoitechat_gtk3_theme_free);
+ GPtrArray *system_roots = g_ptr_array_new_with_free_func (g_free);
+ GHashTable *seen_system_roots = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ GPtrArray *user_roots = g_ptr_array_new_with_free_func (g_free);
+ GHashTable *seen_user_roots = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ GHashTable *seen_theme_roots = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ const gchar *const *system_data_dirs;
+ guint i;
+ char *user_data_themes;
+ char *user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
+ char *home_themes = g_build_filename (g_get_home_dir (), ".themes", NULL);
+
+ g_mkdir_with_parents (user_dir, 0700);
+
+ user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
+ add_theme_root (user_roots, seen_user_roots, 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, user_dir);
+
+ system_data_dirs = g_get_system_data_dirs ();
+ for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
+ {
+ char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL);
+ add_theme_root (system_roots, seen_system_roots, system_themes);
+ g_free (system_themes);
+ }
+
+ 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);
+ for (i = 0; i < user_roots->len; i++)
+ discover_dir (themes, seen_theme_roots, g_ptr_array_index (user_roots, i), ZOITECHAT_GTK3_THEME_SOURCE_USER);
+ g_ptr_array_sort (themes, theme_cmp);
+
+ g_hash_table_destroy (seen_theme_roots);
+ g_hash_table_destroy (seen_user_roots);
+ g_ptr_array_unref (user_roots);
+ g_hash_table_destroy (seen_system_roots);
+ g_ptr_array_unref (system_roots);
+ g_free (home_themes);
+ g_free (user_dir);
+ return themes;
+}
+
+ZoitechatGtk3Theme *
+zoitechat_gtk3_theme_find_by_id (const char *theme_id)
+{
+ GPtrArray *themes;
+ ZoitechatGtk3Theme *result = NULL;
+ guint i;
+
+ if (!theme_id || !*theme_id)
+ return NULL;
+
+ themes = zoitechat_gtk3_theme_service_discover ();
+ for (i = 0; i < themes->len; i++)
+ {
+ ZoitechatGtk3Theme *theme = g_ptr_array_index (themes, i);
+ if (g_strcmp0 (theme->id, theme_id) == 0)
+ {
+ result = g_new0 (ZoitechatGtk3Theme, 1);
+ result->id = g_strdup (theme->id);
+ result->display_name = g_strdup (theme->display_name);
+ result->path = g_strdup (theme->path);
+ result->thumbnail_path = g_strdup (theme->thumbnail_path);
+ result->has_dark_variant = theme->has_dark_variant;
+ result->source = theme->source;
+ break;
+ }
+ }
+ g_ptr_array_unref (themes);
+ return result;
+}
+
+static void
+collect_theme_roots (const char *root, GPtrArray *found, int depth)
+{
+ GDir *dir;
+ const char *name;
+
+ if (depth > 4)
+ return;
+
+ if (path_has_gtk3_css (root))
+ {
+ g_ptr_array_add (found, g_strdup (root));
+ return;
+ }
+
+ dir = g_dir_open (root, 0, NULL);
+ if (!dir)
+ return;
+
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ char *child;
+ if (name[0] == '.')
+ continue;
+ child = g_build_filename (root, name, NULL);
+ if (g_file_test (child, G_FILE_TEST_IS_DIR))
+ collect_theme_roots (child, found, depth + 1);
+ g_free (child);
+ }
+
+ g_dir_close (dir);
+}
+
+
+
+typedef struct
+{
+ char *path;
+ int depth;
+ gboolean has_index_theme;
+} ThemeRootCandidate;
+
+static void
+theme_root_candidate_free (ThemeRootCandidate *candidate)
+{
+ if (!candidate)
+ return;
+ g_free (candidate->path);
+ g_free (candidate);
+}
+
+static int
+path_depth_from_root (const char *base, const char *path)
+{
+ int depth = 0;
+ const char *cursor;
+ size_t base_len;
+
+ if (!base || !path)
+ return 0;
+
+ base_len = strlen (base);
+ cursor = path + base_len;
+ while (*cursor)
+ {
+ if (*cursor == G_DIR_SEPARATOR)
+ depth++;
+ cursor++;
+ }
+
+ return depth;
+}
+
+static gint
+theme_root_candidate_compare (gconstpointer a, gconstpointer b)
+{
+ const ThemeRootCandidate *ca = a;
+ const ThemeRootCandidate *cb = b;
+ if (ca->has_index_theme != cb->has_index_theme)
+ return ca->has_index_theme ? -1 : 1;
+ if (ca->depth != cb->depth)
+ return ca->depth - cb->depth;
+ return g_ascii_strcasecmp (ca->path, cb->path);
+}
+
+static char *
+select_theme_root (GPtrArray *roots, const char *input_root)
+{
+ GPtrArray *candidates;
+ guint i;
+ char *selected;
+
+ if (!roots || roots->len == 0)
+ return NULL;
+ if (roots->len == 1)
+ return g_strdup (g_ptr_array_index (roots, 0));
+
+ candidates = g_ptr_array_new_with_free_func ((GDestroyNotify) theme_root_candidate_free);
+ for (i = 0; i < roots->len; i++)
+ {
+ ThemeRootCandidate *candidate = g_new0 (ThemeRootCandidate, 1);
+ char *index_theme;
+
+ candidate->path = g_strdup (g_ptr_array_index (roots, i));
+ candidate->depth = path_depth_from_root (input_root, candidate->path);
+ index_theme = g_build_filename (candidate->path, "index.theme", NULL);
+ candidate->has_index_theme = g_file_test (index_theme, G_FILE_TEST_IS_REGULAR);
+ g_free (index_theme);
+ g_ptr_array_add (candidates, candidate);
+ }
+
+ g_ptr_array_sort (candidates, theme_root_candidate_compare);
+ selected = g_strdup (((ThemeRootCandidate *) g_ptr_array_index (candidates, 0))->path);
+ g_ptr_array_unref (candidates);
+ return selected;
+}
+
+static gboolean
+copy_tree (const char *src, const char *dest, GError **error)
+{
+ GDir *dir;
+ const char *name;
+
+ if (g_mkdir_with_parents (dest, 0700) != 0)
+ return g_set_error_literal (error, G_FILE_ERROR, g_file_error_from_errno (errno), "Failed to create theme directory."), FALSE;
+
+ dir = g_dir_open (src, 0, error);
+ if (!dir)
+ return FALSE;
+
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ char *s = g_build_filename (src, name, NULL);
+ char *d = g_build_filename (dest, name, NULL);
+ if (g_file_test (s, G_FILE_TEST_IS_DIR))
+ {
+ if (!copy_tree (s, d, error))
+ {
+ g_free (s);
+ g_free (d);
+ g_dir_close (dir);
+ return FALSE;
+ }
+ }
+ else
+ {
+ GFile *sf = g_file_new_for_path (s);
+ GFile *df = g_file_new_for_path (d);
+ if (!g_file_copy (sf, df, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error))
+ {
+ g_object_unref (sf);
+ g_object_unref (df);
+ g_free (s);
+ g_free (d);
+ g_dir_close (dir);
+ return FALSE;
+ }
+ g_object_unref (sf);
+ g_object_unref (df);
+ }
+ g_free (s);
+ g_free (d);
+ }
+
+ g_dir_close (dir);
+ return TRUE;
+}
+
+static gboolean
+validate_theme_root_for_import (const char *theme_root, GError **error)
+{
+ char *index_theme;
+ GKeyFile *keyfile;
+ char *css_dir;
+ char *css_path;
+ char *raw_inherits;
+ char **inherits;
+ guint i;
+ GError *load_error = NULL;
+
+ index_theme = g_build_filename (theme_root, "index.theme", NULL);
+ 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 ();
+ if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Invalid GTK3 theme at '%s': failed to parse index.theme '%s': %s.",
+ theme_root, index_theme, load_error->message);
+ g_error_free (load_error);
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ return FALSE;
+ }
+
+ if (!g_key_file_has_group (keyfile, "Desktop Entry"))
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Invalid GTK3 theme at '%s': index.theme '%s' is missing the [Desktop Entry] section.",
+ theme_root, index_theme);
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ return FALSE;
+ }
+
+ css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root);
+ if (!css_dir)
+ {
+ 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).",
+ theme_root);
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ return FALSE;
+ }
+
+ css_path = g_build_filename (theme_root, css_dir, "gtk.css", NULL);
+ if (!g_file_test (css_path, G_FILE_TEST_IS_REGULAR))
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Invalid GTK3 theme at '%s': missing primary gtk.css at '%s'.",
+ theme_root, css_path);
+ g_free (css_path);
+ g_free (css_dir);
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ return FALSE;
+ }
+ g_free (css_path);
+ g_free (css_dir);
+
+ raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
+ g_key_file_unref (keyfile);
+ g_free (index_theme);
+ if (!raw_inherits)
+ return TRUE;
+
+ inherits = g_strsplit_set (raw_inherits, ",;", -1);
+ g_free (raw_inherits);
+
+ for (i = 0; inherits && inherits[i]; i++)
+ {
+ char *parent_name = g_strstrip (inherits[i]);
+ char *parent_root;
+
+ if (parent_name[0] == '\0')
+ continue;
+
+ parent_root = resolve_parent_theme_root (theme_root, parent_name);
+ if (!parent_root)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Invalid GTK3 theme at '%s': parent theme '%s' from Inherits could not be resolved.",
+ theme_root, parent_name);
+ g_strfreev (inherits);
+ return FALSE;
+ }
+ g_free (parent_root);
+ }
+
+ g_strfreev (inherits);
+ return TRUE;
+}
+
+static char *
+extract_archive (const char *source, GError **error)
+{
+ char *tmp = g_dir_make_tmp ("zoitechat-gtk3-theme-XXXXXX", error);
+#ifdef G_OS_WIN32
+ char *stdout_text = NULL;
+ char *stderr_text = NULL;
+ char *system_tar = NULL;
+ char *system_root = NULL;
+ char *tar_program = NULL;
+ int status = 0;
+ gboolean extracted = FALSE;
+ const char *ext;
+
+ if (!tmp)
+ return NULL;
+
+ ext = strrchr (source, '.');
+ if (ext && g_ascii_strcasecmp (ext, ".zip") == 0)
+ {
+ char *argv[] = {
+ "powershell",
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force",
+ (char *)source,
+ tmp,
+ NULL
+ };
+
+ extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
+ &stdout_text, &stderr_text, &status, NULL);
+ }
+ else
+ {
+ tar_program = g_find_program_in_path ("tar.exe");
+ if (!tar_program)
+ {
+ system_root = g_strdup (g_getenv ("SystemRoot"));
+ if (system_root)
+ {
+ system_tar = g_build_filename (system_root, "System32", "tar.exe", NULL);
+ if (g_file_test (system_tar, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_EXECUTABLE))
+ tar_program = g_strdup (system_tar);
+ }
+ }
+
+ if (!tar_program)
+ {
+ char *argv[] = {
+ "powershell",
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ "tar -xf $args[0] -C $args[1]",
+ (char *)source,
+ tmp,
+ NULL
+ };
+
+ extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
+ &stdout_text, &stderr_text, &status, NULL);
+ }
+ else
+ {
+ char *argv[] = {
+ tar_program,
+ "-xf",
+ (char *)source,
+ "-C",
+ tmp,
+ NULL
+ };
+
+ extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
+ &stdout_text, &stderr_text, &status, NULL);
+ }
+ }
+
+ g_free (tar_program);
+ g_free (system_tar);
+ g_free (system_root);
+ g_free (stdout_text);
+ g_free (stderr_text);
+ if (!extracted || status != 0)
+ {
+ remove_tree (tmp);
+ g_free (tmp);
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to extract theme archive.");
+ return NULL;
+ }
+
+ return tmp;
+#else
+ struct archive *archive = NULL;
+ struct archive *disk = NULL;
+ struct archive_entry *entry;
+ int r;
+
+ if (!tmp)
+ return NULL;
+
+ archive = archive_read_new ();
+ disk = archive_write_disk_new ();
+ archive_read_support_filter_all (archive);
+ archive_read_support_format_all (archive);
+ archive_write_disk_set_options (disk, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS);
+ archive_write_disk_set_standard_lookup (disk);
+
+ r = archive_read_open_filename (archive, source, 10240);
+ if (r != ARCHIVE_OK)
+ {
+ archive_read_free (archive);
+ archive_write_free (disk);
+ remove_tree (tmp);
+ g_free (tmp);
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to extract theme archive.");
+ return NULL;
+ }
+
+ while ((r = archive_read_next_header (archive, &entry)) == ARCHIVE_OK)
+ {
+ const char *entry_path = archive_entry_pathname (entry);
+ char *dest;
+
+ if (!entry_path)
+ {
+ r = ARCHIVE_FAILED;
+ break;
+ }
+
+ dest = g_build_filename (tmp, entry_path, NULL);
+ archive_entry_set_pathname (entry, dest);
+ g_free (dest);
+
+ r = archive_write_header (disk, entry);
+ if (r < ARCHIVE_OK)
+ break;
+
+ if (archive_entry_size (entry) > 0)
+ {
+ const void *buff;
+ size_t size;
+ la_int64_t offset;
+
+ for (;;)
+ {
+ r = archive_read_data_block (archive, &buff, &size, &offset);
+ if (r == ARCHIVE_EOF)
+ break;
+ if (r != ARCHIVE_OK)
+ break;
+ r = archive_write_data_block (disk, buff, size, offset);
+ if (r != ARCHIVE_OK)
+ break;
+ }
+ if (r != ARCHIVE_EOF && r != ARCHIVE_OK)
+ break;
+ }
+
+ r = archive_write_finish_entry (disk);
+ if (r != ARCHIVE_OK)
+ break;
+ }
+
+ if (r == ARCHIVE_EOF)
+ r = ARCHIVE_OK;
+
+ archive_read_free (archive);
+ archive_write_free (disk);
+
+ if (r != ARCHIVE_OK)
+ {
+ remove_tree (tmp);
+ g_free (tmp);
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to extract theme archive.");
+ return NULL;
+ }
+
+ return tmp;
+#endif
+}
+
+gboolean
+zoitechat_gtk3_theme_service_import (const char *source_path, char **imported_id, GError **error)
+{
+ char *input_root = NULL;
+ gboolean cleanup_input = FALSE;
+ GPtrArray *roots;
+ char *selected = NULL;
+ char *base;
+ char *dest_root;
+ char *user_dir;
+ char *candidate;
+ int suffix = 0;
+ gboolean ok;
+
+ if (!source_path || !*source_path)
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "No theme path provided."), FALSE;
+
+ if (g_file_test (source_path, G_FILE_TEST_IS_DIR))
+ input_root = g_strdup (source_path);
+ else
+ {
+ input_root = extract_archive (source_path, error);
+ cleanup_input = TRUE;
+ if (!input_root)
+ return FALSE;
+ }
+
+ roots = g_ptr_array_new_with_free_func (g_free);
+ collect_theme_roots (input_root, roots, 0);
+ if (roots->len == 0)
+ {
+ if (cleanup_input)
+ remove_tree (input_root);
+ g_free (input_root);
+ g_ptr_array_unref (roots);
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "No GTK3 gtk.css file found in the selected theme."), FALSE;
+ }
+
+ selected = select_theme_root (roots, input_root);
+ if (!validate_theme_root_for_import (selected, error))
+ {
+ g_free (selected);
+ g_ptr_array_unref (roots);
+ if (cleanup_input)
+ remove_tree (input_root);
+ g_free (input_root);
+ return FALSE;
+ }
+
+ base = g_path_get_basename (selected);
+ user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
+ g_mkdir_with_parents (user_dir, 0700);
+
+ candidate = g_strdup (base);
+ dest_root = g_build_filename (user_dir, candidate, NULL);
+ while (g_file_test (dest_root, G_FILE_TEST_EXISTS))
+ {
+ suffix++;
+ g_free (candidate);
+ g_free (dest_root);
+ candidate = g_strdup_printf ("%s-%d", base, suffix);
+ dest_root = g_build_filename (user_dir, candidate, NULL);
+ }
+
+ ok = copy_tree (selected, dest_root, error);
+ if (ok && imported_id)
+ *imported_id = path_build_id (dest_root, ZOITECHAT_GTK3_THEME_SOURCE_USER);
+
+ g_free (dest_root);
+ g_free (candidate);
+ g_free (user_dir);
+ g_free (base);
+ g_free (selected);
+ g_ptr_array_unref (roots);
+ if (cleanup_input)
+ remove_tree (input_root);
+ g_free (input_root);
+ return ok;
+}
+
+gboolean
+zoitechat_gtk3_theme_service_remove_user_theme (const char *theme_id, GError **error)
+{
+ ZoitechatGtk3Theme *theme = zoitechat_gtk3_theme_find_by_id (theme_id);
+ gboolean ok;
+
+ if (!theme)
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "Theme not found."), FALSE;
+ if (theme->source != ZOITECHAT_GTK3_THEME_SOURCE_USER)
+ {
+ zoitechat_gtk3_theme_free (theme);
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_PERM, "Only user-imported themes can be removed."), FALSE;
+ }
+
+ remove_tree (theme->path);
+ ok = !g_file_test (theme->path, G_FILE_TEST_EXISTS);
+ zoitechat_gtk3_theme_free (theme);
+ if (!ok)
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to remove theme."), FALSE;
+ return TRUE;
+}
diff --git a/src/common/gtk3-theme-service.h b/src/common/gtk3-theme-service.h
new file mode 100644
index 00000000..82b0172f
--- /dev/null
+++ b/src/common/gtk3-theme-service.h
@@ -0,0 +1,32 @@
+#ifndef ZOITECHAT_GTK3_THEME_SERVICE_H
+#define ZOITECHAT_GTK3_THEME_SERVICE_H
+
+#include
+
+typedef enum
+{
+ ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM = 0,
+ ZOITECHAT_GTK3_THEME_SOURCE_USER = 1
+} ZoitechatGtk3ThemeSource;
+
+typedef struct
+{
+ char *id;
+ char *display_name;
+ char *path;
+ gboolean has_dark_variant;
+ char *thumbnail_path;
+ ZoitechatGtk3ThemeSource source;
+} ZoitechatGtk3Theme;
+
+char *zoitechat_gtk3_theme_service_get_user_themes_dir (void);
+GPtrArray *zoitechat_gtk3_theme_service_discover (void);
+void zoitechat_gtk3_theme_free (ZoitechatGtk3Theme *theme);
+ZoitechatGtk3Theme *zoitechat_gtk3_theme_find_by_id (const char *theme_id);
+gboolean zoitechat_gtk3_theme_service_import (const char *source_path, char **imported_id, GError **error);
+gboolean zoitechat_gtk3_theme_service_remove_user_theme (const char *theme_id, GError **error);
+char *zoitechat_gtk3_theme_pick_css_dir_for_minor (const char *theme_root, int preferred_minor);
+char *zoitechat_gtk3_theme_pick_css_dir (const char *theme_root);
+GPtrArray *zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root);
+
+#endif
diff --git a/src/common/meson.build b/src/common/meson.build
index 50e2422e..99eef515 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -3,7 +3,7 @@ common_sources = [
'chanopt.c',
'ctcp.c',
'dcc.c',
- 'theme-service.c',
+ 'gtk3-theme-service.c',
'zoitechat.c',
'history.c',
'ignore.c',
@@ -28,12 +28,17 @@ common_sources = [
]
common_sysinfo_deps = []
+libarchive_dep = dependency('libarchive', required: host_machine.system() != 'windows')
common_deps = [
libgio_dep,
libcanberra_dep,
] + global_deps
+if libarchive_dep.found()
+ common_deps += libarchive_dep
+endif
+
common_includes = [
config_h_include,
include_directories('.')
@@ -134,3 +139,18 @@ zoitechat_plugin_dep = declare_dependency(
compile_args: common_cflags,
dependencies: global_deps,
)
+
+
+gtk3_theme_service_tests = executable('gtk3_theme_service_tests',
+ [
+ 'tests/test-gtk3-theme-service.c',
+ 'gtk3-theme-service.c',
+ ],
+ include_directories: [config_h_include, include_directories('.')],
+ dependencies: [libgio_dep] + (libarchive_dep.found() ? [libarchive_dep] : []),
+)
+
+test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
+ protocol: 'tap',
+ timeout: 120,
+)
diff --git a/src/common/outbound.c b/src/common/outbound.c
index e166a672..05839696 100644
--- a/src/common/outbound.c
+++ b/src/common/outbound.c
@@ -3770,45 +3770,6 @@ cmd_url (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{
if (word[2][0])
{
- char *theme_path = NULL;
- if (zoitechat_theme_path_from_arg (word[2], &theme_path))
- {
- GError *error = NULL;
- char *basename = g_path_get_basename (theme_path);
- char *dot = strrchr (basename, '.');
- char *message;
-
- if (dot)
- *dot = '\0';
-
- if (zoitechat_import_theme (theme_path, &error))
- {
- if (zoitechat_apply_theme (basename, &error))
- {
- message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename);
- fe_message (message, FE_MSG_INFO);
- handle_command (sess, "gui apply", FALSE);
- g_free (message);
- }
- else
- {
- fe_message (error ? error->message : _("Theme imported, but failed to apply."),
- FE_MSG_ERROR);
- g_clear_error (&error);
- }
- }
- else
- {
- fe_message (error ? error->message : _("Failed to import theme."),
- FE_MSG_ERROR);
- g_clear_error (&error);
- }
-
- g_free (basename);
- g_free (theme_path);
- return TRUE;
- }
-
char *server_name = NULL;
char *port = NULL;
char *channel = NULL;
diff --git a/src/common/tests/test-gtk3-theme-service.c b/src/common/tests/test-gtk3-theme-service.c
new file mode 100644
index 00000000..ac332a55
--- /dev/null
+++ b/src/common/tests/test-gtk3-theme-service.c
@@ -0,0 +1,684 @@
+#include
+#include
+
+#include "../gtk3-theme-service.h"
+#include "../cfgfiles.h"
+
+char *xdir = NULL;
+
+char *
+get_xdir (void)
+{
+ return xdir;
+}
+
+static void
+write_text_file (const char *path, const char *contents)
+{
+ g_file_set_contents (path, contents, -1, NULL);
+}
+
+static char *
+make_theme_dir (const char *base, const char *name, gboolean dark, gboolean with_index)
+{
+ char *root = g_build_filename (base, name, NULL);
+ char *gtk_dir = g_build_filename (root, "gtk-3.0", NULL);
+ char *css = g_build_filename (gtk_dir, "gtk.css", NULL);
+
+ g_mkdir_with_parents (gtk_dir, 0700);
+ write_text_file (css, "button { background-image: url(\"../assets/a.png\"); }");
+ if (dark)
+ {
+ char *dark_css = g_build_filename (gtk_dir, "gtk-dark.css", NULL);
+ write_text_file (dark_css, "button { color: #eee; }");
+ g_free (dark_css);
+ }
+ if (with_index)
+ {
+ char *index = g_build_filename (root, "index.theme", NULL);
+ write_text_file (index, "[Desktop Entry]\nName=Indexed Theme\n");
+ g_free (index);
+ }
+ g_free (css);
+ g_free (gtk_dir);
+ return root;
+}
+
+static void
+setup_test_xdir (char **tmp_root)
+{
+ char *root = g_dir_make_tmp ("zoitechat-gtk3-service-test-XXXXXX", NULL);
+ xdir = g_build_filename (root, "config", NULL);
+ g_mkdir_with_parents (xdir, 0700);
+ *tmp_root = root;
+}
+
+static void
+teardown_test_xdir (char *tmp_root)
+{
+ char *cmd;
+ cmd = g_strdup_printf ("rm -rf %s", tmp_root);
+ g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL);
+ g_free (cmd);
+ g_free (xdir);
+ xdir = NULL;
+ g_free (tmp_root);
+}
+
+
+static guint
+count_extract_temp_dirs (void)
+{
+ GDir *dir;
+ const char *name;
+ guint count = 0;
+ const char *tmp_dir = g_get_tmp_dir ();
+
+ dir = g_dir_open (tmp_dir, 0, NULL);
+ if (!dir)
+ return 0;
+
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ if (g_str_has_prefix (name, "zoitechat-gtk3-theme-"))
+ count++;
+ }
+
+ g_dir_close (dir);
+ return count;
+}
+
+
+static char *
+make_theme_dir_with_inherits (const char *base, const char *name, const char *inherits)
+{
+ char *root = make_theme_dir (base, name, FALSE, FALSE);
+ char *index = g_build_filename (root, "index.theme", NULL);
+ char *contents;
+
+ if (inherits && inherits[0])
+ contents = g_strdup_printf ("[Desktop Entry]\nName=%s\nInherits=%s\n", name, inherits);
+ else
+ contents = g_strdup_printf ("[Desktop Entry]\nName=%s\n", name);
+ write_text_file (index, contents);
+ g_free (contents);
+ g_free (index);
+ return root;
+}
+
+static void
+test_inheritance_chain_single_parent (void)
+{
+ char *tmp_root;
+ char *themes_root;
+ char *adwaita;
+ char *child;
+ GPtrArray *chain;
+
+ setup_test_xdir (&tmp_root);
+ themes_root = g_build_filename (tmp_root, "themes", NULL);
+ g_mkdir_with_parents (themes_root, 0700);
+ adwaita = make_theme_dir_with_inherits (themes_root, "Adwaita", NULL);
+ child = make_theme_dir_with_inherits (themes_root, "Child", "Adwaita");
+
+ chain = zoitechat_gtk3_theme_build_inheritance_chain (child);
+ g_assert_nonnull (chain);
+ g_assert_cmpuint (chain->len, ==, 2);
+ g_assert_cmpstr (g_ptr_array_index (chain, 0), ==, adwaita);
+ g_assert_cmpstr (g_ptr_array_index (chain, 1), ==, child);
+
+ g_ptr_array_unref (chain);
+ g_free (child);
+ g_free (adwaita);
+ g_free (themes_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_inheritance_chain_multi_level (void)
+{
+ char *tmp_root;
+ char *themes_root;
+ char *base;
+ char *middle;
+ char *child;
+ GPtrArray *chain;
+
+ setup_test_xdir (&tmp_root);
+ themes_root = g_build_filename (tmp_root, "themes", NULL);
+ g_mkdir_with_parents (themes_root, 0700);
+ base = make_theme_dir_with_inherits (themes_root, "Base", NULL);
+ middle = make_theme_dir_with_inherits (themes_root, "Middle", "Base");
+ child = make_theme_dir_with_inherits (themes_root, "Child", "Middle");
+
+ chain = zoitechat_gtk3_theme_build_inheritance_chain (child);
+ g_assert_nonnull (chain);
+ g_assert_cmpuint (chain->len, ==, 3);
+ g_assert_cmpstr (g_ptr_array_index (chain, 0), ==, base);
+ g_assert_cmpstr (g_ptr_array_index (chain, 1), ==, middle);
+ g_assert_cmpstr (g_ptr_array_index (chain, 2), ==, child);
+
+ g_ptr_array_unref (chain);
+ g_free (child);
+ g_free (middle);
+ g_free (base);
+ g_free (themes_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_inheritance_chain_missing_parent (void)
+{
+ char *tmp_root;
+ char *themes_root;
+ char *child;
+ GPtrArray *chain;
+
+ setup_test_xdir (&tmp_root);
+ themes_root = g_build_filename (tmp_root, "themes", NULL);
+ g_mkdir_with_parents (themes_root, 0700);
+ child = make_theme_dir_with_inherits (themes_root, "Child", "MissingParent");
+
+ chain = zoitechat_gtk3_theme_build_inheritance_chain (child);
+ g_assert_nonnull (chain);
+ g_assert_cmpuint (chain->len, ==, 1);
+ g_assert_cmpstr (g_ptr_array_index (chain, 0), ==, child);
+
+ g_ptr_array_unref (chain);
+ g_free (child);
+ g_free (themes_root);
+ teardown_test_xdir (tmp_root);
+}
+static void
+test_inheritance_chain_parent_from_xdg_data_home (void)
+{
+ char *tmp_root;
+ char *child_root;
+ char *home_dir;
+ char *user_data_dir;
+ char *saved_home;
+ char *saved_xdg_data_home;
+ char *parent;
+ char *child;
+ GPtrArray *chain;
+
+ setup_test_xdir (&tmp_root);
+ child_root = g_build_filename (tmp_root, "themes", NULL);
+ home_dir = g_build_filename (tmp_root, "home", NULL);
+ user_data_dir = g_build_filename (tmp_root, "xdg-data-home", NULL);
+ g_mkdir_with_parents (child_root, 0700);
+ g_mkdir_with_parents (home_dir, 0700);
+ g_mkdir_with_parents (user_data_dir, 0700);
+
+ saved_home = g_strdup (g_getenv ("HOME"));
+ saved_xdg_data_home = g_strdup (g_getenv ("XDG_DATA_HOME"));
+
+ g_setenv ("HOME", home_dir, TRUE);
+ g_setenv ("XDG_DATA_HOME", user_data_dir, TRUE);
+
+ {
+ char *user_themes = g_build_filename (user_data_dir, "themes", NULL);
+ g_mkdir_with_parents (user_themes, 0700);
+ parent = make_theme_dir_with_inherits (user_themes, "ParentFromDataHome", NULL);
+ g_free (user_themes);
+ }
+ child = make_theme_dir_with_inherits (child_root, "Child", "ParentFromDataHome");
+
+ chain = zoitechat_gtk3_theme_build_inheritance_chain (child);
+ g_assert_nonnull (chain);
+ g_assert_cmpuint (chain->len, ==, 2);
+ g_assert_cmpstr (g_ptr_array_index (chain, 0), ==, parent);
+ g_assert_cmpstr (g_ptr_array_index (chain, 1), ==, child);
+ g_ptr_array_unref (chain);
+
+ if (saved_home)
+ g_setenv ("HOME", saved_home, TRUE);
+ else
+ g_unsetenv ("HOME");
+ if (saved_xdg_data_home)
+ g_setenv ("XDG_DATA_HOME", saved_xdg_data_home, TRUE);
+ else
+ g_unsetenv ("XDG_DATA_HOME");
+
+ g_free (child);
+ g_free (parent);
+ g_free (saved_xdg_data_home);
+ g_free (saved_home);
+ g_free (user_data_dir);
+ g_free (home_dir);
+ g_free (child_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_inheritance_chain_parent_from_xdg_data_dirs (void)
+{
+ char *tmp_root;
+ char *child_root;
+ char *home_dir;
+ char *system_data_dir;
+ char *system_data_dirs;
+ char *saved_home;
+ char *saved_xdg_data_dirs;
+ char *parent;
+ char *child;
+ GPtrArray *chain;
+
+ setup_test_xdir (&tmp_root);
+ child_root = g_build_filename (tmp_root, "themes", NULL);
+ home_dir = g_build_filename (tmp_root, "home", NULL);
+ system_data_dir = g_build_filename (tmp_root, "xdg-data-system", NULL);
+ system_data_dirs = g_strdup_printf ("%s:/usr/share", system_data_dir);
+ g_mkdir_with_parents (child_root, 0700);
+ g_mkdir_with_parents (home_dir, 0700);
+ g_mkdir_with_parents (system_data_dir, 0700);
+
+ saved_home = g_strdup (g_getenv ("HOME"));
+ saved_xdg_data_dirs = g_strdup (g_getenv ("XDG_DATA_DIRS"));
+
+ g_setenv ("HOME", home_dir, TRUE);
+ g_setenv ("XDG_DATA_DIRS", system_data_dirs, TRUE);
+
+ {
+ char *system_themes = g_build_filename (system_data_dir, "themes", NULL);
+ g_mkdir_with_parents (system_themes, 0700);
+ parent = make_theme_dir_with_inherits (system_themes, "ParentFromDataDirs", NULL);
+ g_free (system_themes);
+ }
+ child = make_theme_dir_with_inherits (child_root, "Child", "ParentFromDataDirs");
+
+ chain = zoitechat_gtk3_theme_build_inheritance_chain (child);
+ g_assert_nonnull (chain);
+ g_assert_cmpuint (chain->len, ==, 2);
+ g_assert_cmpstr (g_ptr_array_index (chain, 0), ==, parent);
+ g_assert_cmpstr (g_ptr_array_index (chain, 1), ==, child);
+ g_ptr_array_unref (chain);
+
+ if (saved_home)
+ g_setenv ("HOME", saved_home, TRUE);
+ else
+ g_unsetenv ("HOME");
+ if (saved_xdg_data_dirs)
+ g_setenv ("XDG_DATA_DIRS", saved_xdg_data_dirs, TRUE);
+ else
+ g_unsetenv ("XDG_DATA_DIRS");
+
+ g_free (child);
+ g_free (parent);
+ g_free (saved_xdg_data_dirs);
+ g_free (saved_home);
+ g_free (system_data_dirs);
+ g_free (system_data_dir);
+ g_free (home_dir);
+ g_free (child_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_invalid_archive_reports_extract_error (void)
+{
+ char *tmp_root;
+ char *bad_archive;
+ char *imported_id = NULL;
+ GError *error = NULL;
+ guint before_count;
+ guint after_count;
+
+ setup_test_xdir (&tmp_root);
+ bad_archive = g_build_filename (tmp_root, "bad-theme.tar.xz", NULL);
+ write_text_file (bad_archive, "this is not a real archive");
+ before_count = count_extract_temp_dirs ();
+
+ g_assert_false (zoitechat_gtk3_theme_service_import (bad_archive, &imported_id, &error));
+ g_assert_null (imported_id);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED);
+ g_assert_cmpstr (error->message, ==, "Failed to extract theme archive.");
+ g_error_free (error);
+ after_count = count_extract_temp_dirs ();
+ g_assert_cmpuint (after_count, ==, before_count);
+
+ g_free (bad_archive);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_archive_without_theme_reports_css_error (void)
+{
+ char *tmp_root;
+ char *archive_root;
+ char *archive_path;
+ char *command;
+ char *imported_id = NULL;
+ GError *error = NULL;
+
+ setup_test_xdir (&tmp_root);
+ archive_root = g_build_filename (tmp_root, "invalid-theme-root", NULL);
+ g_mkdir_with_parents (archive_root, 0700);
+ {
+ char *readme = g_build_filename (archive_root, "README.txt", NULL);
+ write_text_file (readme, "not a gtk theme");
+ g_free (readme);
+ }
+ archive_path = g_build_filename (tmp_root, "invalid-theme.zip", NULL);
+
+ command = g_strdup_printf ("cd %s && zip -qr %s .", archive_root, archive_path);
+ g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
+ g_free (command);
+
+ g_assert_false (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, &error));
+ g_assert_null (imported_id);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert_cmpstr (error->message, ==, "No GTK3 gtk.css file found in the selected theme.");
+ g_error_free (error);
+
+ g_free (archive_path);
+ g_free (archive_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_import_rejects_theme_missing_index_theme (void)
+{
+ char *tmp_root;
+ char *src_root;
+ char *theme_root;
+ char *imported_id = NULL;
+ GError *error = NULL;
+
+ setup_test_xdir (&tmp_root);
+ src_root = g_build_filename (tmp_root, "src", NULL);
+ g_mkdir_with_parents (src_root, 0700);
+ theme_root = make_theme_dir (src_root, "NoIndex", FALSE, FALSE);
+
+ g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
+ g_assert_null (imported_id);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert_nonnull (g_strstr_len (error->message, -1, "missing required index.theme"));
+ g_assert_nonnull (g_strstr_len (error->message, -1, "NoIndex"));
+ g_error_free (error);
+
+ g_free (theme_root);
+ g_free (src_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_import_rejects_index_without_desktop_entry (void)
+{
+ char *tmp_root;
+ char *src_root;
+ char *theme_root;
+ char *index_path;
+ char *imported_id = NULL;
+ GError *error = NULL;
+
+ setup_test_xdir (&tmp_root);
+ src_root = g_build_filename (tmp_root, "src", NULL);
+ g_mkdir_with_parents (src_root, 0700);
+ theme_root = make_theme_dir (src_root, "NoDesktopEntry", FALSE, FALSE);
+ index_path = g_build_filename (theme_root, "index.theme", NULL);
+ write_text_file (index_path, "[X-GNOME-Metatheme]\nName=Broken\n");
+
+ g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
+ g_assert_null (imported_id);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert_nonnull (g_strstr_len (error->message, -1, "missing the [Desktop Entry] section"));
+ g_assert_nonnull (g_strstr_len (error->message, -1, "index.theme"));
+ g_error_free (error);
+
+ g_free (index_path);
+ g_free (theme_root);
+ g_free (src_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_import_rejects_unresolved_inherits (void)
+{
+ char *tmp_root;
+ char *src_root;
+ char *theme_root;
+ char *imported_id = NULL;
+ GError *error = NULL;
+
+ setup_test_xdir (&tmp_root);
+ src_root = g_build_filename (tmp_root, "src", NULL);
+ g_mkdir_with_parents (src_root, 0700);
+ theme_root = make_theme_dir_with_inherits (src_root, "ChildTheme", "MissingParent");
+
+ g_assert_false (zoitechat_gtk3_theme_service_import (theme_root, &imported_id, &error));
+ g_assert_null (imported_id);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert_nonnull (g_strstr_len (error->message, -1, "MissingParent"));
+ g_assert_nonnull (g_strstr_len (error->message, -1, "could not be resolved"));
+ g_error_free (error);
+
+ g_free (theme_root);
+ g_free (src_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_import_collision_and_dark_detection (void)
+{
+ char *tmp_root;
+ char *src_root;
+ char *theme_one;
+ char *id_one = NULL;
+ char *id_two = NULL;
+ ZoitechatGtk3Theme *found;
+
+ setup_test_xdir (&tmp_root);
+ src_root = g_build_filename (tmp_root, "src", NULL);
+ g_mkdir_with_parents (src_root, 0700);
+ theme_one = make_theme_dir (src_root, "Ocean", TRUE, FALSE);
+
+ g_assert_true (zoitechat_gtk3_theme_service_import (theme_one, &id_one, NULL));
+ g_assert_true (zoitechat_gtk3_theme_service_import (theme_one, &id_two, NULL));
+ g_assert_nonnull (id_one);
+ g_assert_nonnull (id_two);
+ g_assert_cmpstr (id_one, !=, id_two);
+
+ found = zoitechat_gtk3_theme_find_by_id (id_two);
+ g_assert_nonnull (found);
+ g_assert_true (found->has_dark_variant);
+ g_assert_true (g_str_has_suffix (found->path, "Ocean-1"));
+
+ zoitechat_gtk3_theme_free (found);
+ g_free (id_one);
+ g_free (id_two);
+ g_free (theme_one);
+ g_free (src_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_discover_includes_user_and_system_data_dirs (void)
+{
+ char *tmp_root;
+ char *home_dir;
+ char *user_data_dir;
+ char *system_data_dir;
+ char *system_data_dirs;
+ char *saved_home;
+ char *saved_xdg_data_home;
+ char *saved_xdg_data_dirs;
+ char *user_themes_dir;
+ char *system_themes_dir;
+ char *user_theme;
+ char *system_theme;
+ GPtrArray *themes;
+ guint i;
+ gboolean found_user = FALSE;
+ gboolean found_system = FALSE;
+
+ setup_test_xdir (&tmp_root);
+ home_dir = g_build_filename (tmp_root, "home", NULL);
+ user_data_dir = g_build_filename (tmp_root, "xdg-data-home", NULL);
+ system_data_dir = g_build_filename (tmp_root, "xdg-data-system", NULL);
+ system_data_dirs = g_strdup_printf ("%s:/usr/share", system_data_dir);
+ user_themes_dir = g_build_filename (user_data_dir, "themes", NULL);
+ system_themes_dir = g_build_filename (system_data_dir, "themes", NULL);
+
+ g_mkdir_with_parents (home_dir, 0700);
+ g_mkdir_with_parents (user_themes_dir, 0700);
+ g_mkdir_with_parents (system_themes_dir, 0700);
+ user_theme = make_theme_dir (user_themes_dir, "UserDataTheme", FALSE, FALSE);
+ system_theme = make_theme_dir (system_themes_dir, "SystemDataTheme", FALSE, FALSE);
+
+ saved_home = g_strdup (g_getenv ("HOME"));
+ saved_xdg_data_home = g_strdup (g_getenv ("XDG_DATA_HOME"));
+ saved_xdg_data_dirs = g_strdup (g_getenv ("XDG_DATA_DIRS"));
+
+ g_setenv ("HOME", home_dir, TRUE);
+ g_setenv ("XDG_DATA_HOME", user_data_dir, TRUE);
+ g_setenv ("XDG_DATA_DIRS", system_data_dirs, TRUE);
+
+ themes = zoitechat_gtk3_theme_service_discover ();
+ g_assert_nonnull (themes);
+
+ for (i = 0; i < themes->len; i++)
+ {
+ ZoitechatGtk3Theme *theme = g_ptr_array_index (themes, i);
+ if (g_strcmp0 (theme->path, user_theme) == 0)
+ {
+ found_user = TRUE;
+ g_assert_cmpint (theme->source, ==, ZOITECHAT_GTK3_THEME_SOURCE_USER);
+ }
+ if (g_strcmp0 (theme->path, system_theme) == 0)
+ {
+ found_system = TRUE;
+ g_assert_cmpint (theme->source, ==, ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM);
+ }
+ }
+
+ g_assert_true (found_user);
+ g_assert_true (found_system);
+ g_ptr_array_unref (themes);
+
+ if (saved_home)
+ g_setenv ("HOME", saved_home, TRUE);
+ else
+ g_unsetenv ("HOME");
+ if (saved_xdg_data_home)
+ g_setenv ("XDG_DATA_HOME", saved_xdg_data_home, TRUE);
+ else
+ g_unsetenv ("XDG_DATA_HOME");
+ if (saved_xdg_data_dirs)
+ g_setenv ("XDG_DATA_DIRS", saved_xdg_data_dirs, TRUE);
+ else
+ g_unsetenv ("XDG_DATA_DIRS");
+
+ g_free (saved_xdg_data_dirs);
+ g_free (saved_xdg_data_home);
+ g_free (saved_home);
+ g_free (system_theme);
+ g_free (user_theme);
+ g_free (system_themes_dir);
+ g_free (user_themes_dir);
+ g_free (system_data_dirs);
+ g_free (system_data_dir);
+ g_free (user_data_dir);
+ g_free (home_dir);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_archive_root_detection_prefers_index (void)
+{
+ char *tmp_root;
+ char *archive_root;
+ char *theme_a;
+ char *theme_b_parent;
+ char *theme_b;
+ char *archive_path;
+ char *command;
+ char *imported_id = NULL;
+ ZoitechatGtk3Theme *found;
+
+ setup_test_xdir (&tmp_root);
+ archive_root = g_build_filename (tmp_root, "archive-root", NULL);
+ g_mkdir_with_parents (archive_root, 0700);
+ theme_a = make_theme_dir (archive_root, "Flat", FALSE, FALSE);
+ theme_b_parent = g_build_filename (archive_root, "nested", NULL);
+ g_mkdir_with_parents (theme_b_parent, 0700);
+ theme_b = make_theme_dir (theme_b_parent, "Indexed", FALSE, TRUE);
+ archive_path = g_build_filename (tmp_root, "themes.tar.xz", NULL);
+
+ command = g_strdup_printf ("tar -cJf %s -C %s .", archive_path, archive_root);
+ g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
+ g_free (command);
+
+ g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL));
+ found = zoitechat_gtk3_theme_find_by_id (imported_id);
+ g_assert_nonnull (found);
+ g_assert_true (g_str_has_suffix (found->path, "Indexed"));
+
+ zoitechat_gtk3_theme_free (found);
+ g_free (imported_id);
+ g_free (archive_path);
+ g_free (theme_b);
+ g_free (theme_b_parent);
+ g_free (theme_a);
+ g_free (archive_root);
+ teardown_test_xdir (tmp_root);
+}
+
+static void
+test_zip_import_nested_root (void)
+{
+ char *tmp_root;
+ char *zip_root;
+ char *nested;
+ char *theme;
+ char *archive_path;
+ char *command;
+ char *imported_id = NULL;
+ ZoitechatGtk3Theme *found;
+
+ setup_test_xdir (&tmp_root);
+ zip_root = g_build_filename (tmp_root, "zip-root", NULL);
+ nested = g_build_filename (zip_root, "bundle", "themes", NULL);
+ g_mkdir_with_parents (nested, 0700);
+ theme = make_theme_dir (nested, "Juno-ocean", TRUE, FALSE);
+ archive_path = g_build_filename (tmp_root, "themes.zip", NULL);
+
+ command = g_strdup_printf ("cd %s && zip -qr %s .", zip_root, archive_path);
+ g_assert_true (g_spawn_command_line_sync (command, NULL, NULL, NULL, NULL));
+ g_free (command);
+
+ g_assert_true (zoitechat_gtk3_theme_service_import (archive_path, &imported_id, NULL));
+ found = zoitechat_gtk3_theme_find_by_id (imported_id);
+ g_assert_nonnull (found);
+ g_assert_true (found->has_dark_variant);
+ g_assert_true (g_str_has_suffix (found->path, "Juno-ocean"));
+
+ zoitechat_gtk3_theme_free (found);
+ g_free (imported_id);
+ g_free (archive_path);
+ g_free (theme);
+ g_free (nested);
+ g_free (zip_root);
+ teardown_test_xdir (tmp_root);
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+ g_test_add_func ("/gtk3-theme-service/inheritance-single-parent", test_inheritance_chain_single_parent);
+ g_test_add_func ("/gtk3-theme-service/inheritance-multi-level", test_inheritance_chain_multi_level);
+ g_test_add_func ("/gtk3-theme-service/inheritance-missing-parent", test_inheritance_chain_missing_parent);
+ g_test_add_func ("/gtk3-theme-service/inheritance-parent-from-xdg-data-home", test_inheritance_chain_parent_from_xdg_data_home);
+ g_test_add_func ("/gtk3-theme-service/inheritance-parent-from-xdg-data-dirs", test_inheritance_chain_parent_from_xdg_data_dirs);
+ g_test_add_func ("/gtk3-theme-service/import-collision-dark", test_import_collision_and_dark_detection);
+ g_test_add_func ("/gtk3-theme-service/discover-user-and-system-data-dirs", test_discover_includes_user_and_system_data_dirs);
+ g_test_add_func ("/gtk3-theme-service/archive-root-detection", test_archive_root_detection_prefers_index);
+ 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/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-desktop-entry", test_import_rejects_index_without_desktop_entry);
+ g_test_add_func ("/gtk3-theme-service/import-unresolved-inherits", test_import_rejects_unresolved_inherits);
+ return g_test_run ();
+}
diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c
index e9a0fe74..96d769c2 100644
--- a/src/common/zoitechat.c
+++ b/src/common/zoitechat.c
@@ -54,7 +54,6 @@
#include "text.h"
#include "url.h"
#include "zoitechatc.h"
-#include "theme-service.h"
#if ! GLIB_CHECK_VERSION (2, 36, 0)
#include /* for g_type_init() */
@@ -113,36 +112,9 @@ struct zoitechatprefs prefs;
gboolean
zoitechat_theme_path_from_arg (const char *arg, char **path_out)
{
- char *path = NULL;
- const char *ext;
-
- if (!arg)
- return FALSE;
-
- if (g_str_has_prefix (arg, "file://"))
- path = g_filename_from_uri (arg, NULL, NULL);
- else
- path = g_strdup (arg);
-
- if (!path)
- return FALSE;
-
- ext = strrchr (path, '.');
- if (!g_file_test (path, G_FILE_TEST_IS_REGULAR) ||
- !ext ||
- (g_ascii_strcasecmp (ext, ".zct") != 0 &&
- g_ascii_strcasecmp (ext, ".hct") != 0))
- {
- g_free (path);
- return FALSE;
- }
-
- if (path_out)
- *path_out = path;
- else
- g_free (path);
-
- return TRUE;
+ (void) arg;
+ (void) path_out;
+ return FALSE;
}
#ifdef WIN32
@@ -252,176 +224,19 @@ zoitechat_remote_win32 (void)
}
#endif
+static zoitechat_theme_post_apply_callback zoitechat_theme_post_apply_cb;
+
void
zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback)
{
- zoitechat_theme_service_set_post_apply_callback (callback);
+ zoitechat_theme_post_apply_cb = callback;
}
void
zoitechat_run_theme_post_apply_callback (void)
{
- zoitechat_theme_service_run_post_apply_callback ();
-}
-
-gboolean
-zoitechat_apply_theme (const char *theme_name, GError **error)
-{
- return zoitechat_theme_service_apply (theme_name, error);
-}
-
-gboolean
-zoitechat_import_theme (const char *path, GError **error)
-{
- char *themes_dir;
- char *basename;
- char *dot;
- char *theme_dir;
- char *argv[] = {"unzip", "-o", (char *)path, "-d", NULL, NULL};
- int status = 0;
- gboolean ok;
-#ifdef WIN32
- char *command = NULL;
- char *powershell = NULL;
-#endif
-
- if (!path)
- {
- g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
- _("No theme file specified."));
- return FALSE;
- }
-
- themes_dir = zoitechat_theme_service_get_themes_dir ();
- basename = g_path_get_basename (path);
- if (!basename || basename[0] == '\0')
- {
- g_free (themes_dir);
- g_free (basename);
- g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
- _("Failed to determine theme name."));
- return FALSE;
- }
-
- dot = strrchr (basename, '.');
- if (dot)
- *dot = '\0';
-
- theme_dir = g_build_filename (themes_dir, basename, NULL);
- if (g_mkdir_with_parents (theme_dir, 0700) != 0)
- {
- g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
- _("Failed to create theme directory."));
- g_free (theme_dir);
- g_free (basename);
- g_free (themes_dir);
- return FALSE;
- }
-
-#ifdef WIN32
- powershell = g_find_program_in_path ("powershell.exe");
- if (!powershell)
- powershell = g_find_program_in_path ("powershell");
-
- if (!powershell)
- {
- g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT,
- _("No archive extractor was found."));
- ok = FALSE;
- }
- else
- {
- GString *escaped_path = g_string_new ("'");
- GString *escaped_dir = g_string_new ("'");
- const char *cursor;
-
- for (cursor = path; *cursor != '\0'; cursor++)
- {
- if (*cursor == '\'')
- g_string_append (escaped_path, "''");
- else
- g_string_append_c (escaped_path, *cursor);
- }
- g_string_append_c (escaped_path, '\'');
-
- for (cursor = theme_dir; *cursor != '\0'; cursor++)
- {
- if (*cursor == '\'')
- g_string_append (escaped_dir, "''");
- else
- g_string_append_c (escaped_dir, *cursor);
- }
- g_string_append_c (escaped_dir, '\'');
-
- command = g_strdup_printf (
- "Add-Type -AssemblyName WindowsBase; "
- "$ErrorActionPreference='Stop'; "
- "$package=[System.IO.Packaging.Package]::Open(%s); "
- "try { "
- "foreach ($part in $package.GetParts()) { "
- "$relative=$part.Uri.OriginalString.TrimStart('/'); "
- "if ([string]::IsNullOrEmpty($relative)) { continue }; "
- "$destPath=[System.IO.Path]::Combine(%s, $relative); "
- "$destDir=[System.IO.Path]::GetDirectoryName($destPath); "
- "if ($destDir -and -not (Test-Path -LiteralPath $destDir)) { "
- "[System.IO.Directory]::CreateDirectory($destDir) | Out-Null "
- "}; "
- "$partStream=$part.GetStream(); "
- "$fileStream=[System.IO.File]::Open($destPath,[System.IO.FileMode]::Create,[System.IO.FileAccess]::Write); "
- "$partStream.CopyTo($fileStream); "
- "$fileStream.Dispose(); "
- "$partStream.Dispose(); "
- "} "
- "} finally { $package.Close(); }",
- escaped_path->str,
- escaped_dir->str);
- g_string_free (escaped_path, TRUE);
- g_string_free (escaped_dir, TRUE);
-
- {
- char *ps_argv[] = {powershell, "-NoProfile", "-NonInteractive", "-Command", command, NULL};
- ok = g_spawn_sync (NULL, ps_argv, NULL, 0, NULL, NULL,
- NULL, NULL, &status, error);
- }
- }
-#else
- argv[4] = theme_dir;
- ok = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
- NULL, NULL, &status, error);
-#endif
- if (!ok)
- {
-#ifdef WIN32
- g_free (command);
- g_free (powershell);
-#endif
- g_free (theme_dir);
- g_free (basename);
- g_free (themes_dir);
- return FALSE;
- }
-
- if (!g_spawn_check_exit_status (status, error))
- {
-#ifdef WIN32
- g_free (command);
- g_free (powershell);
-#endif
- g_free (theme_dir);
- g_free (basename);
- g_free (themes_dir);
- return FALSE;
- }
-
-#ifdef WIN32
- g_free (command);
- g_free (powershell);
-#endif
-
- g_free (theme_dir);
- g_free (basename);
- g_free (themes_dir);
- return TRUE;
+ if (zoitechat_theme_post_apply_cb)
+ zoitechat_theme_post_apply_cb ();
}
/*
@@ -725,7 +540,6 @@ irc_init (session *sess)
{
static int done_init = FALSE;
char *buf;
- char *theme_path;
if (done_init)
return;
@@ -748,50 +562,10 @@ irc_init (session *sess)
if (arg_url != NULL)
{
- theme_path = NULL;
- if (zoitechat_theme_path_from_arg (arg_url, &theme_path))
- {
- GError *error = NULL;
- char *basename = g_path_get_basename (theme_path);
- char *dot = strrchr (basename, '.');
- char *message;
-
- if (dot)
- *dot = '\0';
-
- if (zoitechat_import_theme (theme_path, &error))
- {
- if (zoitechat_apply_theme (basename, &error))
- {
- message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename);
- fe_message (message, FE_MSG_INFO);
- g_free (message);
- }
- else
- {
- fe_message (error ? error->message : _("Theme imported, but failed to apply."),
- FE_MSG_ERROR);
- g_clear_error (&error);
- }
- }
- else
- {
- fe_message (error ? error->message : _("Failed to import theme."),
- FE_MSG_ERROR);
- g_clear_error (&error);
- }
-
- g_free (basename);
- }
- else
- {
- buf = g_strdup_printf ("server %s", arg_url);
- handle_command (sess, buf, FALSE);
- g_free (buf);
- }
-
- g_free (theme_path);
- g_free (arg_url); /* from GOption */
+ buf = g_strdup_printf ("server %s", arg_url);
+ handle_command (sess, buf, FALSE);
+ g_free (buf);
+ g_free (arg_url);
}
if (arg_urls != NULL)
@@ -799,49 +573,9 @@ irc_init (session *sess)
guint i;
for (i = 0; i < g_strv_length (arg_urls); i++)
{
- theme_path = NULL;
- if (zoitechat_theme_path_from_arg (arg_urls[i], &theme_path))
- {
- GError *error = NULL;
- char *basename = g_path_get_basename (theme_path);
- char *dot = strrchr (basename, '.');
- char *message;
-
- if (dot)
- *dot = '\0';
-
- if (zoitechat_import_theme (theme_path, &error))
- {
- if (zoitechat_apply_theme (basename, &error))
- {
- message = g_strdup_printf (_("Theme \"%s\" imported and applied."), basename);
- fe_message (message, FE_MSG_INFO);
- g_free (message);
- }
- else
- {
- fe_message (error ? error->message : _("Theme imported, but failed to apply."),
- FE_MSG_ERROR);
- g_clear_error (&error);
- }
- }
- else
- {
- fe_message (error ? error->message : _("Failed to import theme."),
- FE_MSG_ERROR);
- g_clear_error (&error);
- }
-
- g_free (basename);
- }
- else
- {
- buf = g_strdup_printf ("%s %s", i==0? "server" : "newserver", arg_urls[i]);
- handle_command (sess, buf, FALSE);
- g_free (buf);
- }
-
- g_free (theme_path);
+ buf = g_strdup_printf ("%s %s", i == 0 ? "server" : "newserver", arg_urls[i]);
+ handle_command (sess, buf, FALSE);
+ g_free (buf);
}
g_strfreev (arg_urls);
}
diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h
index 882476ce..7693f53b 100644
--- a/src/common/zoitechat.h
+++ b/src/common/zoitechat.h
@@ -30,8 +30,6 @@
#define ZOITECHAT_H
gboolean zoitechat_theme_path_from_arg (const char *arg, char **path_out);
-gboolean zoitechat_import_theme (const char *path, GError **error);
-gboolean zoitechat_apply_theme (const char *theme_name, GError **error);
typedef void (*zoitechat_theme_post_apply_callback) (void);
void zoitechat_set_theme_post_apply_callback (zoitechat_theme_post_apply_callback callback);
void zoitechat_run_theme_post_apply_callback (void);
@@ -143,6 +141,7 @@ struct zoitechatprefs
unsigned int hex_gui_tab_dots;
unsigned int hex_gui_tab_icons;
unsigned int hex_gui_dark_mode;
+ unsigned int hex_gui_gtk3_variant;
unsigned int hex_gui_tab_scrollchans;
unsigned int hex_gui_tab_server;
unsigned int hex_gui_tab_sort;
@@ -323,6 +322,7 @@ struct zoitechatprefs
char hex_text_font[4 * FONTNAMELEN + 1];
char hex_text_font_main[FONTNAMELEN + 1];
char hex_text_font_alternative[3 * FONTNAMELEN + 1];
+ char hex_gui_gtk3_theme[256];
char hex_text_spell_langs[64];
/* these are the private variables */
diff --git a/src/fe-gtk/banlist.c b/src/fe-gtk/banlist.c
index c8a21a02..cc20a1e1 100644
--- a/src/fe-gtk/banlist.c
+++ b/src/fe-gtk/banlist.c
@@ -27,6 +27,7 @@
#endif
#include "fe-gtk.h"
+#include "theme/theme-manager.h"
#include "../common/zoitechat.h"
#include "../common/fe.h"
@@ -563,6 +564,7 @@ banlist_clear (GtkWidget * wid, banlist_info *banl)
dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
_("Are you sure you want to remove all listed items in %s?"), banl->sess->channel);
+ theme_manager_attach_window (dialog);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (banlist_clear_cb), banl);
diff --git a/src/fe-gtk/dccgui.c b/src/fe-gtk/dccgui.c
index fa3803ae..5c7e59b3 100644
--- a/src/fe-gtk/dccgui.c
+++ b/src/fe-gtk/dccgui.c
@@ -624,15 +624,7 @@ clear_completed (GtkWidget * wid, gpointer none)
static void
browse_folder (char *dir)
{
-#ifdef WIN32
- /* no need for file:// in ShellExecute() */
fe_open_url (dir);
-#else
- char buf[512];
-
- g_snprintf (buf, sizeof (buf), "file://%s", dir);
- fe_open_url (buf);
-#endif
}
static void
diff --git a/src/fe-gtk/fe-gtk.c b/src/fe-gtk/fe-gtk.c
index 4126a543..c91927ff 100644
--- a/src/fe-gtk/fe-gtk.c
+++ b/src/fe-gtk/fe-gtk.c
@@ -107,6 +107,7 @@ create_msg_dialog (gchar *title, gchar *message)
GtkWidget *dialog;
dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
+ theme_manager_attach_window (dialog);
gtk_window_set_title (GTK_WINDOW (dialog), title);
/* On Win32 we automatically have the icon. If we try to load it explicitly, it will look ugly for some reason. */
@@ -697,6 +698,7 @@ fe_message (char *msg, int flags)
dialog = gtk_message_dialog_new (GTK_WINDOW (parent_window), 0, type,
GTK_BUTTONS_OK, "%s", msg);
+ theme_manager_attach_window (dialog);
if (flags & FE_MSG_MARKUP)
gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG (dialog), msg);
g_signal_connect (G_OBJECT (dialog), "response",
@@ -1221,61 +1223,6 @@ fe_set_inputbox_contents (session *sess, char *text)
}
}
-#ifdef __APPLE__
-static char *
-url_escape_hostname (const char *url)
-{
- char *host_start, *host_end, *ret, *hostname;
-
- host_start = strstr (url, "://");
- if (host_start != NULL)
- {
- *host_start = '\0';
- host_start += 3;
- host_end = strchr (host_start, '/');
-
- if (host_end != NULL)
- {
- *host_end = '\0';
- host_end++;
- }
-
- hostname = g_hostname_to_ascii (host_start);
- if (host_end != NULL)
- ret = g_strdup_printf ("%s://%s/%s", url, hostname, host_end);
- else
- ret = g_strdup_printf ("%s://%s", url, hostname);
-
- g_free (hostname);
- return ret;
- }
-
- return g_strdup (url);
-}
-
-static void
-osx_show_uri (const char *url)
-{
- char *escaped_url, *encoded_url, *open, *cmd;
-
- escaped_url = url_escape_hostname (url);
- encoded_url = g_filename_from_utf8 (escaped_url, -1, NULL, NULL, NULL);
- if (encoded_url)
- {
- open = g_find_program_in_path ("open");
- cmd = g_strjoin (" ", open, encoded_url, NULL);
-
- zoitechat_exec (cmd);
-
- g_free (encoded_url);
- g_free (cmd);
- }
-
- g_free (escaped_url);
-}
-
-#endif
-
static inline char *
escape_uri (const char *uri)
{
@@ -1318,48 +1265,39 @@ maybe_escape_uri (const char *uri)
static void
fe_open_url_inner (const char *url)
{
-#ifdef WIN32
- gunichar2 *url_utf16 = g_utf8_to_utf16 (url, -1, NULL, NULL, NULL);
-
- if (url_utf16 == NULL)
- {
- return;
- }
-
- ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL);
-
- g_free (url_utf16);
-#elif defined(__APPLE__)
- osx_show_uri (url);
-#else
GError *error = NULL;
char *escaped_url = maybe_escape_uri (url);
- gchar *xdg_open_argv[] = {(gchar *) "xdg-open", escaped_url, NULL};
- gchar **spawn_env = NULL;
- gboolean opened = FALSE;
- g_debug ("Opening URL \"%s\" (%s)", escaped_url, url);
+ gboolean opened = g_app_info_launch_default_for_uri (escaped_url, NULL, &error);
- /* AppImage runtime variables can point host binaries like /bin/sh at
- * bundled libraries, which may not be ABI-compatible with system tools. */
- spawn_env = g_get_environ ();
+ if (!opened)
{
- gchar **tmp_env = spawn_env;
- spawn_env = g_environ_unsetenv (tmp_env, "LD_LIBRARY_PATH");
- if (spawn_env != tmp_env)
- g_strfreev (tmp_env);
+ g_clear_error (&error);
+#ifdef WIN32
+ gunichar2 *url_utf16 = g_utf8_to_utf16 (escaped_url, -1, NULL, NULL, NULL);
- tmp_env = spawn_env;
- spawn_env = g_environ_unsetenv (tmp_env, "LD_PRELOAD");
- if (spawn_env != tmp_env)
- g_strfreev (tmp_env);
- }
+ if (url_utf16 != NULL)
+ {
+ opened = ((INT_PTR) ShellExecuteW (0, L"open", url_utf16, NULL, NULL, SW_SHOWNORMAL)) > 32;
+ g_free (url_utf16);
+ }
+#else
+ gchar *xdg_open_argv[] = {(gchar *) "xdg-open", escaped_url, NULL};
+ gchar **spawn_env = NULL;
- /* Prefer xdg-open when available because gtk_show_uri can inherit
- * AppImage runtime state and fail before we can control the environment. */
- {
- gchar *xdg_open_path = g_find_program_in_path ("xdg-open");
- if (xdg_open_path &&
- g_spawn_async (NULL, xdg_open_argv, spawn_env,
+ spawn_env = g_get_environ ();
+ {
+ gchar **tmp_env = spawn_env;
+ spawn_env = g_environ_unsetenv (tmp_env, "LD_LIBRARY_PATH");
+ if (spawn_env != tmp_env)
+ g_strfreev (tmp_env);
+
+ tmp_env = spawn_env;
+ spawn_env = g_environ_unsetenv (tmp_env, "LD_PRELOAD");
+ if (spawn_env != tmp_env)
+ g_strfreev (tmp_env);
+ }
+
+ if (g_spawn_async (NULL, xdg_open_argv, spawn_env,
G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL,
NULL, NULL, NULL, &error))
{
@@ -1369,27 +1307,26 @@ fe_open_url_inner (const char *url)
{
g_clear_error (&error);
}
- g_free (xdg_open_path);
- }
- if (!opened && gtk_show_uri (NULL, escaped_url, GDK_CURRENT_TIME, &error))
- {
- opened = TRUE;
- }
- else if (!opened)
- {
- g_warning ("gtk_show_uri failed for '%s': %s", escaped_url, error ? error->message : "unknown error");
- g_clear_error (&error);
+ if (!opened && gtk_show_uri (NULL, escaped_url, GDK_CURRENT_TIME, &error))
+ {
+ opened = TRUE;
+ }
+ else if (!opened)
+ {
+ g_clear_error (&error);
+ }
+
+ g_strfreev (spawn_env);
+#endif
}
if (!opened)
{
- g_warning ("Unable to open URL '%s' via xdg-open or gtk_show_uri", escaped_url);
+ g_warning ("Unable to open URL '%s' using system default application", escaped_url);
}
- g_strfreev (spawn_env);
g_free (escaped_url);
-#endif
}
void
diff --git a/src/fe-gtk/fe-gtk.vcxproj b/src/fe-gtk/fe-gtk.vcxproj
index 35fc5072..63fc5cf1 100644
--- a/src/fe-gtk/fe-gtk.vcxproj
+++ b/src/fe-gtk/fe-gtk.vcxproj
@@ -30,7 +30,7 @@
4244;4267;%(DisableSpecificWarnings)
- $(DepsRoot)\lib;%(AdditionalLibraryDirectories)
+ $(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)
$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;%(AdditionalDependencies)
mainCRTStartup
@@ -85,6 +85,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
+
@@ -126,6 +127,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
+
diff --git a/src/fe-gtk/fe-gtk.vcxproj.filters b/src/fe-gtk/fe-gtk.vcxproj.filters
index ad3d5540..0267bc56 100644
--- a/src/fe-gtk/fe-gtk.vcxproj.filters
+++ b/src/fe-gtk/fe-gtk.vcxproj.filters
@@ -123,6 +123,9 @@
Header Files
+
+ Header Files
+
Header Files
@@ -242,6 +245,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/src/fe-gtk/fkeys.c b/src/fe-gtk/fkeys.c
index a7f35778..13aea5fd 100644
--- a/src/fe-gtk/fkeys.c
+++ b/src/fe-gtk/fkeys.c
@@ -51,6 +51,7 @@
#include "theme/theme-access.h"
#include "theme/theme-manager.h"
#include "theme/theme-css.h"
+#include "theme/theme-gtk3.h"
#include "maingui.h"
#include "textgui.h"
#include "fkeys.h"
@@ -795,6 +796,7 @@ key_dialog_treeview_new (GtkWidget *box)
"changed", G_CALLBACK (key_dialog_selection_changed), NULL);
gtk_widget_set_name (view, "fkeys-treeview");
+ if (!theme_gtk3_is_active ())
{
GtkCssProvider *provider = gtk_css_provider_new ();
diff --git a/src/fe-gtk/gtkutil.c b/src/fe-gtk/gtkutil.c
index 6af45925..f210dbe8 100644
--- a/src/fe-gtk/gtkutil.c
+++ b/src/fe-gtk/gtkutil.c
@@ -554,6 +554,8 @@ gtkutil_file_req (GtkWindow *parent, const char *title, void *callback, void *us
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
+ theme_manager_attach_window (dialog);
+
if (filter && filter[0] && (flags & FRF_FILTERISINITIAL))
{
if (flags & FRF_WRITE)
@@ -712,6 +714,7 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
_("_Cancel"), GTK_RESPONSE_REJECT,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
+ theme_manager_attach_window (dialog);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
@@ -807,6 +810,7 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
_("_Cancel"), GTK_RESPONSE_REJECT,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
+ theme_manager_attach_window (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
@@ -847,6 +851,7 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
_("_No"), GTK_RESPONSE_REJECT,
_("_Yes"), GTK_RESPONSE_ACCEPT,
NULL);
+ theme_manager_attach_window (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
@@ -963,6 +968,7 @@ gtkutil_window_new (char *title, char *role, int width, int height, int flags)
GtkWidget *win;
win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ theme_manager_attach_window (win);
gtkutil_set_icon (win);
#ifdef WIN32
gtk_window_set_wmclass (GTK_WINDOW (win), "ZoiteChat", "zoitechat");
diff --git a/src/fe-gtk/ignoregui.c b/src/fe-gtk/ignoregui.c
index e0f8df4a..8598221b 100644
--- a/src/fe-gtk/ignoregui.c
+++ b/src/fe-gtk/ignoregui.c
@@ -23,6 +23,7 @@
#include
#include
#include "fe-gtk.h"
+#include "theme/theme-manager.h"
#include "../common/zoitechat.h"
#include "../common/ignore.h"
@@ -295,6 +296,7 @@ ignore_clear_entry_clicked (GtkWidget * wid)
dialog = gtk_message_dialog_new (NULL, 0,
GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
_("Are you sure you want to remove all ignores?"));
+ theme_manager_attach_window (dialog);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (ignore_clear_cb), NULL);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
diff --git a/src/fe-gtk/joind.c b/src/fe-gtk/joind.c
index ae07d3d5..3b05d394 100644
--- a/src/fe-gtk/joind.c
+++ b/src/fe-gtk/joind.c
@@ -39,6 +39,7 @@
#include "fe-gtk.h"
#include "chanlist.h"
#include "gtkutil.h"
+#include "theme/theme-manager.h"
#define ICON_JOIND_NETWORK "network-workgroup"
@@ -129,6 +130,7 @@ joind_show_dialog (server *serv)
char buf2[256];
serv->gui->joind_win = dialog1 = gtk_dialog_new ();
+ theme_manager_attach_window (dialog1);
g_snprintf(buf, sizeof(buf), _("Connection Complete - %s"), _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (dialog1), buf);
gtk_window_set_type_hint (GTK_WINDOW (dialog1), GDK_WINDOW_TYPE_HINT_DIALOG);
diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c
index 4e19057b..4729c060 100644
--- a/src/fe-gtk/maingui.c
+++ b/src/fe-gtk/maingui.c
@@ -1364,6 +1364,7 @@ mg_tab_close (session *sess)
GTK_MESSAGE_WARNING, GTK_BUTTONS_OK_CANCEL,
_("This server still has %d channels or dialogs associated with it. "
"Close them all?"), i);
+ theme_manager_attach_window (dialog);
g_signal_connect (G_OBJECT (dialog), "response",
G_CALLBACK (mg_tab_close_cb), sess);
if (prefs.hex_gui_tab_layout)
@@ -1461,6 +1462,7 @@ mg_open_quit_dialog (gboolean minimize_button)
}
dialog = gtk_dialog_new ();
+ theme_manager_attach_window (dialog);
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
@@ -2617,7 +2619,7 @@ mg_update_xtext (GtkWidget *wid)
const gchar *font_name;
XTextColor xtext_palette[XTEXT_COLS];
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
+ theme_get_xtext_colors_for_widget (wid, xtext_palette, XTEXT_COLS);
gtk_xtext_set_palette (xtext, xtext_palette);
gtk_xtext_set_max_lines (xtext, prefs.hex_text_max_lines);
gtk_xtext_set_background (xtext, channelwin_pix);
@@ -2664,7 +2666,7 @@ mg_create_textarea (session *sess, GtkWidget *box)
gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
gtk_box_pack_start (GTK_BOX (inbox), frame, TRUE, TRUE, 0);
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
+ theme_get_xtext_colors_for_widget (frame, xtext_palette, XTEXT_COLS);
gui->xtext = gtk_xtext_new (xtext_palette, TRUE);
xtext = GTK_XTEXT (gui->xtext);
gtk_xtext_set_max_indent (xtext, prefs.hex_text_max_indent);
diff --git a/src/fe-gtk/menu.c b/src/fe-gtk/menu.c
index f32cce27..7faddc3e 100644
--- a/src/fe-gtk/menu.c
+++ b/src/fe-gtk/menu.c
@@ -55,6 +55,7 @@
#include "pixmaps.h"
#include "rawlog.h"
#include "theme/theme-gtk.h"
+#include "theme/theme-manager.h"
#include "plugingui.h"
#include "search.h"
#include "textgui.h"
@@ -1520,6 +1521,7 @@ menu_join (GtkWidget * wid, gpointer none)
_("_Cancel"), GTK_RESPONSE_REJECT,
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
+ theme_manager_attach_window (dialog);
{
GtkWidget *button;
@@ -1856,6 +1858,7 @@ static void
menu_about (GtkWidget *wid, gpointer sess)
{
GtkAboutDialog *dialog = GTK_ABOUT_DIALOG(gtk_about_dialog_new());
+ theme_manager_attach_window (GTK_WIDGET (dialog));
char comment[512];
char *license = "This program is free software; you can redistribute it and/or modify\n" \
"it under the terms of the GNU General Public License as published by\n" \
@@ -2483,6 +2486,7 @@ menu_create_main (void *accel_group, int bar, int away, int toplevel,
if (bar)
{
menu_bar = gtk_menu_bar_new ();
+ gtk_style_context_add_class (gtk_widget_get_style_context (menu_bar), GTK_STYLE_CLASS_MENUBAR);
#ifdef HAVE_GTK_MAC
gtkosx_application_set_menu_bar (osx_app, GTK_MENU_SHELL (menu_bar));
#endif
diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build
index af32f106..90fe134e 100644
--- a/src/fe-gtk/meson.build
+++ b/src/fe-gtk/meson.build
@@ -2,6 +2,7 @@ zoitechat_theme_sources = [
'theme/theme-access.c',
'theme/theme-application.c',
'theme/theme-css.c',
+ 'theme/theme-gtk3.c',
'theme/theme-manager.c',
'theme/theme-palette.c',
'theme/theme-preferences.c',
@@ -142,6 +143,7 @@ theme_manager_policy_tests = executable('theme_manager_policy_tests',
'theme/tests/test-theme-manager-policy.c',
'theme/theme-manager.c',
'theme/theme-palette.c',
+ 'theme/tests/test-theme-gtk3-stub.c',
],
include_directories: [config_h_include],
dependencies: [gtk_dep],
@@ -158,6 +160,7 @@ theme_manager_dispatch_tests = executable('theme_manager_dispatch_routing_tests'
'theme/tests/test-theme-manager-dispatch-routing.c',
'theme/theme-manager.c',
'theme/theme-palette.c',
+ 'theme/tests/test-theme-gtk3-stub.c',
],
include_directories: [config_h_include],
dependencies: [gtk_dep],
@@ -173,6 +176,7 @@ theme_manager_auto_refresh_tests = executable('theme_manager_auto_refresh_tests'
'theme/tests/test-theme-manager-auto-refresh.c',
'theme/theme-manager.c',
'theme/theme-palette.c',
+ 'theme/tests/test-theme-gtk3-stub.c',
],
include_directories: [config_h_include],
dependencies: [gtk_dep],
@@ -187,6 +191,7 @@ theme_application_input_style_tests = executable('theme_application_input_style_
[
'theme/tests/test-theme-application-input-style.c',
'theme/theme-application.c',
+ 'theme/tests/test-theme-gtk3-stub.c',
],
include_directories: [config_h_include],
dependencies: [gtk_dep],
@@ -220,3 +225,29 @@ test('Theme Access Routing Tests', theme_access_tests,
protocol: 'tap',
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/tests/test-theme-preferences-gtk3-populate.c',
+ include_directories: [config_h_include],
+ dependencies: [gtk_dep],
+)
+
+test('Theme Preferences GTK3 Populate Tests', theme_preferences_gtk3_populate_tests,
+ protocol: 'tap',
+ timeout: 120,
+)
diff --git a/src/fe-gtk/notifygui.c b/src/fe-gtk/notifygui.c
index b28dc00f..17444572 100644
--- a/src/fe-gtk/notifygui.c
+++ b/src/fe-gtk/notifygui.c
@@ -37,6 +37,7 @@
#include "theme/theme-gtk.h"
#include "notifygui.h"
#include "theme/theme-access.h"
+#include "theme/theme-manager.h"
#define ICON_NOTIFY_NEW "document-new"
#define ICON_NOTIFY_DELETE "edit-delete"
@@ -381,6 +382,7 @@ fe_notify_ask (char *nick, char *networks)
LABEL_NOTIFY_CANCEL, GTK_RESPONSE_REJECT,
LABEL_NOTIFY_OK, GTK_RESPONSE_ACCEPT,
NULL);
+ theme_manager_attach_window (dialog);
if (parent_window)
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
diff --git a/src/fe-gtk/rawlog.c b/src/fe-gtk/rawlog.c
index 3a7dd57c..15dcf4d8 100644
--- a/src/fe-gtk/rawlog.c
+++ b/src/fe-gtk/rawlog.c
@@ -61,7 +61,7 @@ rawlog_theme_apply (GtkWidget *window)
if (!xtext_widget)
return;
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
+ theme_get_xtext_colors_for_widget (xtext_widget, xtext_palette, XTEXT_COLS);
gtk_xtext_set_palette (GTK_XTEXT (xtext_widget), xtext_palette);
}
@@ -174,7 +174,7 @@ open_rawlog (struct server *serv)
gtk_widget_set_vexpand (scrolledwindow, TRUE);
gtk_box_pack_start (GTK_BOX (vbox), scrolledwindow, TRUE, TRUE, 0);
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
+ theme_get_xtext_colors_for_widget (scrolledwindow, xtext_palette, XTEXT_COLS);
serv->gui->rawlog_textlist = gtk_xtext_new (xtext_palette, 0);
gtk_container_add (GTK_CONTAINER (scrolledwindow), serv->gui->rawlog_textlist);
gtk_xtext_set_font (GTK_XTEXT (serv->gui->rawlog_textlist), prefs.hex_text_font);
@@ -198,6 +198,7 @@ open_rawlog (struct server *serv)
g_signal_connect (G_OBJECT (serv->gui->rawlog_window), "destroy", G_CALLBACK (rawlog_theme_destroy_cb), NULL);
gtk_widget_show_all (serv->gui->rawlog_window);
+ rawlog_theme_apply (serv->gui->rawlog_window);
}
void
diff --git a/src/fe-gtk/servlistgui.c b/src/fe-gtk/servlistgui.c
index 421b02b1..f6d7a84d 100644
--- a/src/fe-gtk/servlistgui.c
+++ b/src/fe-gtk/servlistgui.c
@@ -35,6 +35,7 @@
#include "menu.h"
#include "pixmaps.h"
#include "fkeys.h"
+#include "theme/theme-manager.h"
#define SERVLIST_X_PADDING 4 /* horizontal paddig in the network editor */
#define SERVLIST_Y_PADDING 0 /* vertical padding in the network editor */
@@ -787,6 +788,7 @@ servlist_deletenet_cb (GtkWidget *item, ircnet *net)
GTK_BUTTONS_OK_CANCEL,
_("Really remove network \"%s\" and all its servers?"),
net->name);
+ theme_manager_attach_window (dialog);
g_signal_connect (dialog, "response",
G_CALLBACK (servlist_deletenetdialog_cb), net);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
@@ -1791,6 +1793,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
char buf[128];
editwindow = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ theme_manager_attach_window (editwindow);
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
@@ -2072,6 +2075,7 @@ servlist_open_networks (void)
char buf[128];
servlist = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ theme_manager_attach_window (servlist);
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (servlist), buf);
diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c
index ed8de201..37554c23 100644
--- a/src/fe-gtk/setup.c
+++ b/src/fe-gtk/setup.c
@@ -193,7 +193,6 @@ static const char *const tabcompmenu[] =
static const setting inputbox_settings[] =
{
{ST_HEADER, N_("Input Box"),0,0,0},
- {ST_TOGGLE, N_("Use the text box font and colors"), P_OFFINTNL(hex_gui_input_style),0,0,0},
{ST_TOGGLE, N_("Render colors and attributes"), P_OFFINTNL (hex_gui_input_attr),0,0,0},
{ST_TOGGLE, N_("Show nick box"), P_OFFINTNL(hex_gui_input_nick),0,0,1},
{ST_TOGGLE, N_("Show user mode icon in nick box"), P_OFFINTNL(hex_gui_input_icon),0,0,0},
@@ -257,7 +256,6 @@ static const setting userlist_settings[] =
{
{ST_HEADER, N_("User List"),0,0,0},
{ST_TOGGLE, N_("Show hostnames in user list"), P_OFFINTNL(hex_gui_ulist_show_hosts), 0, 0, 0},
- {ST_TOGGLE, N_("Use the Text box font and colors"), P_OFFINTNL(hex_gui_ulist_style),0,0,0},
{ST_TOGGLE, N_("Show icons for user modes"), P_OFFINTNL(hex_gui_ulist_icons), N_("Use graphical icons instead of text symbols in the user list."), 0, 0},
{ST_TOGGLE, N_("Color nicknames in userlist"), P_OFFINTNL(hex_gui_ulist_color), N_("Will color nicknames the same as in chat."), 0, 0},
{ST_TOGGLE, N_("Show user count in channels"), P_OFFINTNL(hex_gui_ulist_count), 0, 0, 0},
@@ -1194,6 +1192,7 @@ setup_browsefont_cb (GtkWidget *button, GtkWidget *entry)
const char *font_name;
dialog = gtk_font_chooser_dialog_new (_("Select font"), GTK_WINDOW (setup_window));
+ theme_manager_attach_window (dialog);
font_dialog = dialog; /* global var */
gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
@@ -1435,18 +1434,12 @@ setup_create_page (const setting *set)
return tab;
}
-static GtkWidget *
-setup_create_color_page (void)
-{
- return theme_preferences_create_color_page (GTK_WINDOW (setup_window),
- &setup_prefs,
- &color_change);
-}
-
static GtkWidget *
setup_create_theme_page (void)
{
- return theme_preferences_create_page (GTK_WINDOW (setup_window), &color_change);
+ return theme_preferences_create_page (GTK_WINDOW (setup_window),
+ &setup_prefs,
+ &color_change);
}
/* === GLOBALS for sound GUI === */
@@ -1736,8 +1729,7 @@ static const char *const cata_interface[] =
N_("Input box"),
N_("User list"),
N_("Channel switcher"),
- N_("Themes"),
- N_("Colors"),
+ N_("GTK3 Theme"),
NULL
};
@@ -1772,7 +1764,6 @@ setup_create_pages (GtkWidget *box)
setup_add_page (cata_interface[2], book, setup_create_page (userlist_settings));
setup_add_page (cata_interface[3], book, setup_create_page (tabs_settings));
setup_add_page (cata_interface[4], book, setup_create_theme_page ());
- setup_add_page (cata_interface[5], book, setup_create_color_page ());
setup_add_page (cata_chatting[0], book, setup_create_page (general_settings));
@@ -2053,12 +2044,8 @@ setup_apply (struct zoitechatprefs *pr)
noapply = TRUE;
if (DIFF (hex_gui_ulist_show_hosts))
noapply = TRUE;
- if (DIFF (hex_gui_ulist_style))
- noapply = TRUE;
if (DIFF (hex_gui_ulist_sort))
noapply = TRUE;
- if (DIFF (hex_gui_input_style) && prefs.hex_gui_input_style == TRUE)
- noapply = TRUE; /* Requires restart to *disable* */
if ((pr->hex_gui_tab_pos == 5 || pr->hex_gui_tab_pos == 6) &&
pr->hex_gui_tab_layout == 2 && pr->hex_gui_tab_pos != prefs.hex_gui_tab_pos)
diff --git a/src/fe-gtk/textgui.c b/src/fe-gtk/textgui.c
index 3d8cb699..9ca95040 100644
--- a/src/fe-gtk/textgui.c
+++ b/src/fe-gtk/textgui.c
@@ -167,7 +167,7 @@ pevent_dialog_theme_apply (GtkWidget *window)
if (!xtext)
return;
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
+ theme_get_xtext_colors_for_widget (xtext, xtext_palette, XTEXT_COLS);
gtk_xtext_set_palette (GTK_XTEXT (xtext), xtext_palette);
}
@@ -515,7 +515,7 @@ pevent_dialog_show ()
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (wid), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
gtk_box_pack_start (GTK_BOX (vbox), wid, FALSE, TRUE, 0);
- theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
+ theme_get_xtext_colors_for_widget (wid, xtext_palette, XTEXT_COLS);
pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0);
gtk_widget_set_sensitive (pevent_dialog_twid, FALSE);
gtk_widget_set_size_request (pevent_dialog_twid, -1, 75);
@@ -539,4 +539,5 @@ pevent_dialog_show ()
NULL, _("OK"));
gtk_widget_show_all (pevent_dialog);
+ pevent_dialog_theme_apply (pevent_dialog);
}
diff --git a/src/fe-gtk/theme/tests/test-theme-access-routing.c b/src/fe-gtk/theme/tests/test-theme-access-routing.c
index 479ff299..9f87d150 100644
--- a/src/fe-gtk/theme/tests/test-theme-access-routing.c
+++ b/src/fe-gtk/theme/tests/test-theme-access-routing.c
@@ -13,11 +13,16 @@ struct session *lastact_sess;
struct zoitechatprefs prefs;
static gboolean stub_dark_active;
+static gboolean stub_gtk3_active;
static ThemeSemanticToken stub_last_color_token;
static int stub_runtime_get_color_calls;
static int stub_runtime_widget_calls;
static int stub_runtime_xtext_calls;
+static int stub_runtime_xtext_mapped_calls;
static size_t stub_runtime_xtext_last_len;
+static ThemeGtkPaletteMap stub_last_gtk_map;
+static gboolean stub_last_gtk_map_valid;
+static gboolean gtk_available;
static GdkRGBA stub_light_colors[THEME_TOKEN_COUNT];
static GdkRGBA stub_dark_colors[THEME_TOKEN_COUNT];
@@ -70,6 +75,13 @@ theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col)
(void) col;
}
+gboolean
+theme_runtime_mode_has_user_colors (gboolean dark_mode)
+{
+ (void) dark_mode;
+ return FALSE;
+}
+
gboolean
theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
{
@@ -109,6 +121,36 @@ theme_runtime_is_dark_active (void)
return stub_dark_active;
}
+void
+theme_runtime_get_widget_style_values_mapped (const ThemeGtkPaletteMap *gtk_map, ThemeWidgetStyleValues *out_values)
+{
+ (void) gtk_map;
+ theme_runtime_get_widget_style_values (out_values);
+}
+
+void
+theme_runtime_get_xtext_colors_mapped (const ThemeGtkPaletteMap *gtk_map, XTextColor *palette, size_t palette_len)
+{
+ size_t i;
+
+ stub_runtime_xtext_mapped_calls++;
+ stub_last_gtk_map = *gtk_map;
+ stub_last_gtk_map_valid = TRUE;
+ stub_runtime_xtext_last_len = palette_len;
+ for (i = 0; i < palette_len; i++)
+ {
+ palette[i].red = (unsigned short) (100 + i);
+ palette[i].green = (unsigned short) (200 + i);
+ palette[i].blue = (unsigned short) (300 + i);
+ }
+}
+
+gboolean
+theme_gtk3_is_active (void)
+{
+ return stub_gtk3_active;
+}
+
static gboolean
rgba_equal (const GdkRGBA *a, const GdkRGBA *b)
{
@@ -127,7 +169,10 @@ reset_stubs (void)
stub_runtime_get_color_calls = 0;
stub_runtime_widget_calls = 0;
stub_runtime_xtext_calls = 0;
+ stub_runtime_xtext_mapped_calls = 0;
stub_runtime_xtext_last_len = 0;
+ stub_last_gtk_map_valid = FALSE;
+ stub_gtk3_active = FALSE;
for (i = 0; i < THEME_TOKEN_COUNT; i++)
{
g_snprintf (light, sizeof (light), "#%02x%02x%02x", (unsigned int) (i + 1), 0x11, 0x22);
@@ -137,6 +182,15 @@ reset_stubs (void)
}
}
+static gboolean
+rgba_close (const GdkRGBA *a, const GdkRGBA *b)
+{
+ return fabs (a->red - b->red) < 0.0001 &&
+ fabs (a->green - b->green) < 0.0001 &&
+ fabs (a->blue - b->blue) < 0.0001 &&
+ fabs (a->alpha - b->alpha) < 0.0001;
+}
+
static void
test_access_semantic_token_routes_directly (void)
{
@@ -204,6 +258,62 @@ test_access_widget_style_forwarding (void)
g_assert_true (fabs (values.foreground.green - (0xfc / 255.0)) < 0.0001);
}
+static void
+test_access_xtext_palette_widget_mapping_when_gtk3_active (void)
+{
+ GtkWidget *window;
+ GtkWidget *label;
+ GtkStyleContext *context;
+ GtkCssProvider *provider;
+ XTextColor palette[2] = { 0 };
+ GdkRGBA expected;
+
+ if (!gtk_available)
+ {
+ g_test_skip ("GTK display not available");
+ return;
+ }
+
+ reset_stubs ();
+ stub_gtk3_active = TRUE;
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ label = gtk_label_new ("mapped");
+ gtk_container_add (GTK_CONTAINER (window), label);
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_data (provider,
+ "label { color: #112233; background-color: #445566; }"
+ "label:selected { color: #778899; background-color: #aabbcc; }"
+ "label:link { color: #123456; }",
+ -1,
+ NULL);
+ context = gtk_widget_get_style_context (label);
+ gtk_style_context_add_provider (context,
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ gtk_widget_realize (window);
+
+ theme_get_xtext_colors_for_widget (label, palette, G_N_ELEMENTS (palette));
+
+ g_assert_cmpint (stub_runtime_xtext_mapped_calls, ==, 1);
+ g_assert_cmpint (stub_runtime_xtext_calls, ==, 0);
+ g_assert_true (stub_last_gtk_map_valid);
+ g_assert_true (gdk_rgba_parse (&expected, "#112233"));
+ g_assert_true (rgba_close (&stub_last_gtk_map.text_foreground, &expected));
+ g_assert_true (gdk_rgba_parse (&expected, "#445566"));
+ g_assert_true (rgba_close (&stub_last_gtk_map.text_background, &expected));
+ g_assert_true (gdk_rgba_parse (&expected, "#778899"));
+ g_assert_true (rgba_close (&stub_last_gtk_map.selection_foreground, &expected));
+ g_assert_true (gdk_rgba_parse (&expected, "#aabbcc"));
+ g_assert_true (rgba_close (&stub_last_gtk_map.selection_background, &expected));
+ g_assert_true (gdk_rgba_parse (&expected, "#123456"));
+ g_assert_true (rgba_close (&stub_last_gtk_map.accent, &expected));
+ g_assert_cmpuint (palette[0].red, ==, 100);
+ g_assert_cmpuint (palette[1].green, ==, 201);
+
+ gtk_widget_destroy (window);
+ g_object_unref (provider);
+}
+
static void
test_access_dark_light_switch_affects_token_consumers (void)
{
@@ -229,8 +339,11 @@ main (int argc, char **argv)
g_test_add_func ("/theme/access/semantic_token_routes_directly", test_access_semantic_token_routes_directly);
g_test_add_func ("/theme/access/token_routes_without_legacy_accessor", test_access_token_routes_without_legacy_accessor);
g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding);
+ g_test_add_func ("/theme/access/xtext_palette_widget_mapping_when_gtk3_active",
+ test_access_xtext_palette_widget_mapping_when_gtk3_active);
g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding);
g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers",
test_access_dark_light_switch_affects_token_consumers);
+ gtk_available = gtk_init_check (&argc, &argv);
return g_test_run ();
}
diff --git a/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c b/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c
new file mode 100644
index 00000000..69694950
--- /dev/null
+++ b/src/fe-gtk/theme/tests/test-theme-gtk3-settings.c
@@ -0,0 +1,325 @@
+#include
+#include
+#include
+
+#include "../theme-gtk3.h"
+#include "../../../common/gtk3-theme-service.h"
+#include "../../../common/zoitechat.h"
+#include "../../../common/zoitechatc.h"
+
+struct session *current_sess;
+struct session *current_tab;
+struct session *lastact_sess;
+struct zoitechatprefs prefs;
+
+static gboolean gtk_available;
+static char *temp_root;
+static char *theme_parent_root;
+static char *theme_child_root;
+static char *theme_switch_root;
+
+gboolean
+theme_policy_system_prefers_dark (void)
+{
+ return FALSE;
+}
+
+static void
+remove_tree (const char *path)
+{
+ GDir *dir;
+ const char *name;
+
+ if (!path || !g_file_test (path, G_FILE_TEST_EXISTS))
+ return;
+ if (!g_file_test (path, G_FILE_TEST_IS_DIR))
+ {
+ g_remove (path);
+ return;
+ }
+
+ dir = g_dir_open (path, 0, NULL);
+ if (dir)
+ {
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ char *child = g_build_filename (path, name, NULL);
+ remove_tree (child);
+ g_free (child);
+ }
+ g_dir_close (dir);
+ }
+ g_rmdir (path);
+}
+
+static void
+write_file (const char *path, const char *contents)
+{
+ gboolean ok = g_file_set_contents (path, contents, -1, NULL);
+ g_assert_true (ok);
+}
+
+static void
+ensure_css_dir (const char *theme_root, const char *css_dir)
+{
+ char *dir = g_build_filename (theme_root, css_dir, NULL);
+ char *css = g_build_filename (dir, "gtk.css", NULL);
+ int rc = g_mkdir_with_parents (dir, 0700);
+ g_assert_cmpint (rc, ==, 0);
+ write_file (css, "* { }\n");
+ g_free (css);
+ g_free (dir);
+}
+
+static void
+write_settings (const char *theme_root, const char *css_dir, const char *settings)
+{
+ char *path = g_build_filename (theme_root, css_dir, "settings.ini", NULL);
+ write_file (path, settings);
+ g_free (path);
+}
+
+static ZoitechatGtk3Theme *
+make_theme (const char *id, const char *path)
+{
+ ZoitechatGtk3Theme *theme = g_new0 (ZoitechatGtk3Theme, 1);
+ theme->id = g_strdup (id);
+ theme->display_name = g_strdup (id);
+ theme->path = g_strdup (path);
+ theme->source = ZOITECHAT_GTK3_THEME_SOURCE_USER;
+ return theme;
+}
+
+void
+zoitechat_gtk3_theme_free (ZoitechatGtk3Theme *theme)
+{
+ if (!theme)
+ return;
+ g_free (theme->id);
+ g_free (theme->display_name);
+ g_free (theme->path);
+ g_free (theme->thumbnail_path);
+ g_free (theme);
+}
+
+ZoitechatGtk3Theme *
+zoitechat_gtk3_theme_find_by_id (const char *theme_id)
+{
+ if (g_strcmp0 (theme_id, "layered") == 0)
+ return make_theme (theme_id, theme_child_root);
+ if (g_strcmp0 (theme_id, "switch") == 0)
+ return make_theme (theme_id, theme_switch_root);
+ return NULL;
+}
+
+char *
+zoitechat_gtk3_theme_pick_css_dir_for_minor (const char *theme_root, int preferred_minor)
+{
+ char *path;
+ (void) preferred_minor;
+ path = g_build_filename (theme_root, "gtk-3.24", "gtk.css", NULL);
+ if (g_file_test (path, G_FILE_TEST_IS_REGULAR))
+ {
+ g_free (path);
+ return g_strdup ("gtk-3.24");
+ }
+ g_free (path);
+ path = g_build_filename (theme_root, "gtk-3.0", "gtk.css", NULL);
+ if (g_file_test (path, G_FILE_TEST_IS_REGULAR))
+ {
+ g_free (path);
+ return g_strdup ("gtk-3.0");
+ }
+ g_free (path);
+ return NULL;
+}
+
+char *
+zoitechat_gtk3_theme_pick_css_dir (const char *theme_root)
+{
+ return zoitechat_gtk3_theme_pick_css_dir_for_minor (theme_root, -1);
+}
+
+GPtrArray *
+zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root)
+{
+ GPtrArray *chain = g_ptr_array_new_with_free_func (g_free);
+ if (g_strcmp0 (theme_root, theme_child_root) == 0)
+ {
+ g_ptr_array_add (chain, g_strdup (theme_parent_root));
+ g_ptr_array_add (chain, g_strdup (theme_child_root));
+ return chain;
+ }
+ if (g_strcmp0 (theme_root, theme_switch_root) == 0)
+ {
+ g_ptr_array_add (chain, g_strdup (theme_switch_root));
+ return chain;
+ }
+ g_ptr_array_unref (chain);
+ return NULL;
+}
+
+static gboolean
+get_bool_setting (const char *name)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+ gboolean value = FALSE;
+ g_object_get (settings, name, &value, NULL);
+ return value;
+}
+
+static gint
+get_int_setting (const char *name)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+ gint value = 0;
+ g_object_get (settings, name, &value, NULL);
+ return value;
+}
+
+static void
+setup_themes (void)
+{
+ char *path;
+
+ temp_root = g_dir_make_tmp ("zoitechat-theme-gtk3-settings-XXXXXX", NULL);
+ g_assert_nonnull (temp_root);
+ theme_parent_root = g_build_filename (temp_root, "parent", NULL);
+ theme_child_root = g_build_filename (temp_root, "child", NULL);
+ theme_switch_root = g_build_filename (temp_root, "switch", NULL);
+ g_assert_cmpint (g_mkdir_with_parents (theme_parent_root, 0700), ==, 0);
+ g_assert_cmpint (g_mkdir_with_parents (theme_child_root, 0700), ==, 0);
+ g_assert_cmpint (g_mkdir_with_parents (theme_switch_root, 0700), ==, 0);
+
+ ensure_css_dir (theme_parent_root, "gtk-3.24");
+ write_settings (theme_parent_root, "gtk-3.24",
+ "[Settings]\n"
+ "gtk-enable-animations=true\n"
+ "gtk-cursor-blink-time=111\n");
+
+ ensure_css_dir (theme_child_root, "gtk-3.0");
+ ensure_css_dir (theme_child_root, "gtk-3.24");
+ write_settings (theme_child_root, "gtk-3.0",
+ "[Settings]\n"
+ "gtk-enable-animations=false\n"
+ "gtk-cursor-blink-time=222\n");
+ write_settings (theme_child_root, "gtk-3.24",
+ "[Settings]\n"
+ "gtk-cursor-blink-time=333\n");
+
+ ensure_css_dir (theme_switch_root, "gtk-3.24");
+ write_settings (theme_switch_root, "gtk-3.24",
+ "[Settings]\n"
+ "gtk-enable-animations=false\n"
+ "gtk-cursor-blink-time=444\n");
+
+ path = g_build_filename (theme_parent_root, "index.theme", NULL);
+ write_file (path, "[Desktop Entry]\nName=parent\n");
+ g_free (path);
+ path = g_build_filename (theme_child_root, "index.theme", NULL);
+ write_file (path, "[Desktop Entry]\nName=child\nInherits=parent\n");
+ g_free (path);
+ path = g_build_filename (theme_switch_root, "index.theme", NULL);
+ write_file (path, "[Desktop Entry]\nName=switch\n");
+ g_free (path);
+}
+
+static void
+teardown_themes (void)
+{
+ g_assert_nonnull (temp_root);
+ remove_tree (temp_root);
+ g_free (theme_parent_root);
+ g_free (theme_child_root);
+ g_free (theme_switch_root);
+ g_free (temp_root);
+ theme_parent_root = NULL;
+ theme_child_root = NULL;
+ theme_switch_root = NULL;
+ temp_root = NULL;
+}
+
+static void
+test_settings_layer_precedence (void)
+{
+ GError *error = NULL;
+
+ if (!gtk_available)
+ {
+ g_test_skip ("GTK display not available");
+ return;
+ }
+
+ g_assert_true (theme_gtk3_apply ("layered", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
+ g_assert_no_error (error);
+ g_assert_false (get_bool_setting ("gtk-enable-animations"));
+ g_assert_cmpint (get_int_setting ("gtk-cursor-blink-time"), ==, 333);
+ g_assert_true (theme_gtk3_is_active ());
+ theme_gtk3_disable ();
+}
+
+static void
+test_settings_restored_on_disable_and_switch (void)
+{
+ GError *error = NULL;
+ gboolean default_animations;
+ gint default_blink;
+ char *default_theme_name = NULL;
+ char *active_theme_name = NULL;
+
+ if (!gtk_available)
+ {
+ g_test_skip ("GTK display not available");
+ return;
+ }
+
+
+ default_animations = get_bool_setting ("gtk-enable-animations");
+ default_blink = get_int_setting ("gtk-cursor-blink-time");
+ g_object_get (gtk_settings_get_default (), "gtk-theme-name", &default_theme_name, NULL);
+
+ g_assert_true (theme_gtk3_apply ("layered", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (get_int_setting ("gtk-cursor-blink-time"), ==, 333);
+ g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
+ g_assert_cmpstr (active_theme_name, ==, "child");
+ g_free (active_theme_name);
+ active_theme_name = NULL;
+
+ g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
+ g_assert_no_error (error);
+ g_assert_false (get_bool_setting ("gtk-enable-animations"));
+ g_assert_cmpint (get_int_setting ("gtk-cursor-blink-time"), ==, 444);
+
+ theme_gtk3_disable ();
+ g_assert_cmpint (get_int_setting ("gtk-cursor-blink-time"), ==, default_blink);
+ g_assert_cmpint (get_bool_setting ("gtk-enable-animations"), ==, default_animations);
+ g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
+ g_assert_cmpstr (active_theme_name, ==, default_theme_name);
+ g_free (active_theme_name);
+ g_free (default_theme_name);
+ g_assert_false (theme_gtk3_is_active ());
+}
+
+int
+main (int argc, char **argv)
+{
+ int rc;
+
+ g_test_init (&argc, &argv, NULL);
+ gtk_available = gtk_init_check (&argc, &argv);
+ setup_themes ();
+
+ 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);
+
+ prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
+
+ if (!gtk_available)
+ g_test_message ("Skipping GTK3 settings tests because GTK initialization failed");
+
+ rc = g_test_run ();
+ theme_gtk3_disable ();
+ teardown_themes ();
+ return rc;
+}
diff --git a/src/fe-gtk/theme/tests/test-theme-gtk3-stub.c b/src/fe-gtk/theme/tests/test-theme-gtk3-stub.c
new file mode 100644
index 00000000..fdec47a3
--- /dev/null
+++ b/src/fe-gtk/theme/tests/test-theme-gtk3-stub.c
@@ -0,0 +1,71 @@
+#include
+
+#include "../theme-gtk3.h"
+
+static int apply_current_calls;
+
+void
+test_theme_gtk3_stub_reset (void)
+{
+ apply_current_calls = 0;
+}
+
+int
+test_theme_gtk3_stub_apply_current_calls (void)
+{
+ return apply_current_calls;
+}
+
+void
+theme_gtk3_init (void)
+{
+}
+
+gboolean
+theme_gtk3_apply_current (GError **error)
+{
+ (void) error;
+ apply_current_calls++;
+ return TRUE;
+}
+
+gboolean
+theme_gtk3_apply (const char *theme_id, ThemeGtk3Variant variant, GError **error)
+{
+ (void) theme_id;
+ (void) variant;
+ (void) error;
+ return TRUE;
+}
+
+gboolean
+theme_gtk3_refresh (const char *theme_id, ThemeGtk3Variant variant, GError **error)
+{
+ (void) theme_id;
+ (void) variant;
+ (void) error;
+ return TRUE;
+}
+
+ThemeGtk3Variant
+theme_gtk3_variant_for_theme (const char *theme_id)
+{
+ (void) theme_id;
+ return THEME_GTK3_VARIANT_PREFER_LIGHT;
+}
+
+void
+theme_gtk3_invalidate_provider_cache (void)
+{
+}
+
+void
+theme_gtk3_disable (void)
+{
+}
+
+gboolean
+theme_gtk3_is_active (void)
+{
+ return FALSE;
+}
diff --git a/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c b/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c
index 686e0d0c..b49acd76 100644
--- a/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c
+++ b/src/fe-gtk/theme/tests/test-theme-manager-auto-refresh.c
@@ -1,6 +1,7 @@
#include
#include "../theme-manager.h"
+#include "../theme-gtk3.h"
#include "../../fe-gtk.h"
#include "../../../common/zoitechat.h"
#include "../../../common/zoitechatc.h"
@@ -19,6 +20,9 @@ static ThemeChangedEvent last_event;
static int idle_add_calls;
static guint next_idle_source_id = 33;
+void test_theme_gtk3_stub_reset (void);
+int test_theme_gtk3_stub_apply_current_calls (void);
+
void setup_apply_real (const ThemeChangedEvent *event)
{
(void) event;
@@ -85,6 +89,11 @@ void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *colo
(void) color;
}
+void theme_runtime_reset_mode_colors (gboolean dark_mode)
+{
+ (void) dark_mode;
+}
+
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active)
{
(void) mode;
@@ -135,6 +144,12 @@ void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values)
gdk_rgba_parse (&out_values->foreground, "#f0f0f0");
}
+void theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *out_values)
+{
+ (void) widget;
+ theme_get_widget_style_values (out_values);
+}
+
void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark)
{
(void) window;
@@ -167,6 +182,8 @@ reset_state (void)
listener_calls = 0;
idle_add_calls = 0;
next_idle_source_id = 33;
+ prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
+ test_theme_gtk3_stub_reset ();
}
static void
@@ -187,6 +204,7 @@ test_auto_refresh_dispatches_mode_palette_and_style_reasons (void)
g_assert_cmpint (auto_state_calls, ==, 2);
g_assert_true (last_auto_state);
g_assert_cmpint (listener_calls, ==, 1);
+ g_assert_cmpint (test_theme_gtk3_stub_apply_current_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));
@@ -212,6 +230,29 @@ test_auto_refresh_ignores_non_auto_mode (void)
g_assert_cmpint (idle_add_calls, ==, 1);
g_assert_cmpint (auto_state_calls, ==, 0);
g_assert_cmpint (listener_calls, ==, 0);
+ g_assert_cmpint (test_theme_gtk3_stub_apply_current_calls (), ==, 0);
+
+ theme_manager_set_idle_add_func (NULL);
+ theme_listener_unregister (listener_id);
+}
+
+static void
+test_auto_refresh_reapplies_gtk3_for_follow_system_variant (void)
+{
+ guint listener_id;
+
+ reset_state ();
+ prefs.hex_gui_dark_mode = ZOITECHAT_DARK_MODE_DARK;
+ prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
+ listener_id = theme_listener_register ("auto.gtk3", auto_listener, NULL);
+ theme_manager_set_idle_add_func (immediate_idle_add);
+
+ theme_manager_refresh_auto_mode ();
+
+ g_assert_cmpint (idle_add_calls, ==, 1);
+ g_assert_cmpint (auto_state_calls, ==, 0);
+ g_assert_cmpint (listener_calls, ==, 0);
+ g_assert_cmpint (test_theme_gtk3_stub_apply_current_calls (), ==, 1);
theme_manager_set_idle_add_func (NULL);
theme_listener_unregister (listener_id);
@@ -225,5 +266,7 @@ main (int argc, char **argv)
test_auto_refresh_dispatches_mode_palette_and_style_reasons);
g_test_add_func ("/theme/manager/auto_refresh_ignores_non_auto_mode",
test_auto_refresh_ignores_non_auto_mode);
+ g_test_add_func ("/theme/manager/auto_refresh_reapplies_gtk3_for_follow_system_variant",
+ test_auto_refresh_reapplies_gtk3_for_follow_system_variant);
return g_test_run ();
}
diff --git a/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c b/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c
index 21ad04e5..6e6b492e 100644
--- a/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c
+++ b/src/fe-gtk/theme/tests/test-theme-manager-dispatch-routing.c
@@ -81,6 +81,11 @@ void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *colo
(void) color;
}
+void theme_runtime_reset_mode_colors (gboolean dark_mode)
+{
+ (void) dark_mode;
+}
+
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active)
{
(void) mode;
@@ -131,6 +136,12 @@ void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values)
gdk_rgba_parse (&out_values->foreground, "#f0f0f0");
}
+void theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *out_values)
+{
+ (void) widget;
+ theme_get_widget_style_values (out_values);
+}
+
void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark)
{
(void) window;
diff --git a/src/fe-gtk/theme/tests/test-theme-manager-policy.c b/src/fe-gtk/theme/tests/test-theme-manager-policy.c
index f64078bc..717736c6 100644
--- a/src/fe-gtk/theme/tests/test-theme-manager-policy.c
+++ b/src/fe-gtk/theme/tests/test-theme-manager-policy.c
@@ -85,6 +85,11 @@ void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *colo
stub_last_user_token = token;
}
+void theme_runtime_reset_mode_colors (gboolean dark_mode)
+{
+ (void) dark_mode;
+}
+
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *dark_active)
{
(void) mode;
@@ -147,6 +152,12 @@ void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values)
gdk_rgba_parse (&out_values->foreground, "#f0f0f0");
}
+void theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *out_values)
+{
+ (void) widget;
+ theme_get_widget_style_values (out_values);
+}
+
void fe_win32_apply_native_titlebar (GtkWidget *window, gboolean dark)
{
(void) window;
diff --git a/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c b/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c
new file mode 100644
index 00000000..71540bc2
--- /dev/null
+++ b/src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c
@@ -0,0 +1,280 @@
+#include
+
+#include "../../../common/zoitechat.h"
+#include "../../../common/zoitechatc.h"
+#include "../../../common/gtk3-theme-service.h"
+#include "../../fe-gtk.h"
+#include "../theme-gtk3.h"
+#include "../theme-manager.h"
+
+struct session *current_sess;
+struct session *current_tab;
+struct zoitechatprefs prefs;
+InputStyle *input_style;
+
+static gboolean gtk_available;
+static int apply_current_calls;
+static char applied_theme_id[256];
+static ThemeGtk3Variant applied_variant;
+static gboolean removed_selected;
+
+GtkWidget *
+gtkutil_box_new (GtkOrientation orientation, gboolean homogeneous, gint spacing)
+{
+ (void)homogeneous;
+ return gtk_box_new (orientation, spacing);
+}
+
+void
+gtkutil_apply_palette (GtkWidget *wid, const GdkRGBA *fg, const GdkRGBA *bg, const PangoFontDescription *font)
+{
+ (void)wid;
+ (void)fg;
+ (void)bg;
+ (void)font;
+}
+
+void
+fe_open_url (const char *url)
+{
+ (void)url;
+}
+
+gboolean
+theme_get_color (ThemeSemanticToken token, GdkRGBA *color)
+{
+ (void)token;
+ if (color)
+ gdk_rgba_parse (color, "#000000");
+ return TRUE;
+}
+
+void
+theme_manager_set_token_color (unsigned int dark_mode, ThemeSemanticToken token, const GdkRGBA *color, gboolean *changed)
+{
+ (void)dark_mode;
+ (void)token;
+ (void)color;
+ if (changed)
+ *changed = FALSE;
+}
+
+void
+theme_manager_reset_mode_colors (unsigned int mode, gboolean *palette_changed)
+{
+ (void)mode;
+ if (palette_changed)
+ *palette_changed = FALSE;
+}
+
+void
+theme_manager_save_preferences (void)
+{
+}
+
+ThemePaletteBehavior
+theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_desc)
+{
+ ThemePaletteBehavior behavior;
+
+ behavior.font_desc = font_desc;
+ behavior.apply_background = FALSE;
+ behavior.apply_foreground = FALSE;
+ return behavior;
+}
+
+void
+theme_manager_apply_userlist_style (GtkWidget *widget, ThemePaletteBehavior behavior)
+{
+ (void)widget;
+ (void)behavior;
+}
+
+void
+theme_manager_attach_window (GtkWidget *window)
+{
+ (void)window;
+}
+
+char *
+zoitechat_gtk3_theme_service_get_user_themes_dir (void)
+{
+ return g_strdup ("/tmp");
+}
+
+static ZoitechatGtk3Theme *
+new_theme (const char *id, const char *name, ZoitechatGtk3ThemeSource source)
+{
+ ZoitechatGtk3Theme *theme = g_new0 (ZoitechatGtk3Theme, 1);
+ theme->id = g_strdup (id);
+ theme->display_name = g_strdup (name);
+ theme->source = source;
+ return theme;
+}
+
+void
+zoitechat_gtk3_theme_free (ZoitechatGtk3Theme *theme)
+{
+ if (!theme)
+ return;
+ g_free (theme->id);
+ g_free (theme->display_name);
+ g_free (theme->path);
+ g_free (theme->thumbnail_path);
+ g_free (theme);
+}
+
+GPtrArray *
+zoitechat_gtk3_theme_service_discover (void)
+{
+ GPtrArray *themes = g_ptr_array_new_with_free_func ((GDestroyNotify)zoitechat_gtk3_theme_free);
+
+ if (!removed_selected)
+ g_ptr_array_add (themes, new_theme ("removed-theme", "Removed Theme", ZOITECHAT_GTK3_THEME_SOURCE_USER));
+ g_ptr_array_add (themes, new_theme ("fallback-theme", "Fallback Theme", ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM));
+ return themes;
+}
+
+ZoitechatGtk3Theme *
+zoitechat_gtk3_theme_find_by_id (const char *theme_id)
+{
+ (void)theme_id;
+ return NULL;
+}
+
+gboolean
+zoitechat_gtk3_theme_service_import (const char *source_path, char **imported_id, GError **error)
+{
+ (void)source_path;
+ (void)imported_id;
+ (void)error;
+ return FALSE;
+}
+
+gboolean
+zoitechat_gtk3_theme_service_remove_user_theme (const char *theme_id, GError **error)
+{
+ (void)error;
+ if (g_strcmp0 (theme_id, "removed-theme") == 0)
+ {
+ removed_selected = TRUE;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+char *
+zoitechat_gtk3_theme_pick_css_dir_for_minor (const char *theme_root, int preferred_minor)
+{
+ (void)theme_root;
+ (void)preferred_minor;
+ return NULL;
+}
+
+char *
+zoitechat_gtk3_theme_pick_css_dir (const char *theme_root)
+{
+ (void)theme_root;
+ return NULL;
+}
+
+GPtrArray *
+zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root)
+{
+ (void)theme_root;
+ return NULL;
+}
+
+gboolean
+theme_gtk3_apply_current (GError **error)
+{
+ (void)error;
+ apply_current_calls++;
+ g_strlcpy (applied_theme_id, prefs.hex_gui_gtk3_theme, sizeof (applied_theme_id));
+ applied_variant = (ThemeGtk3Variant)prefs.hex_gui_gtk3_variant;
+ return TRUE;
+}
+
+void
+theme_gtk3_init (void)
+{
+}
+
+gboolean
+theme_gtk3_apply (const char *theme_id, ThemeGtk3Variant variant, GError **error)
+{
+ (void)theme_id;
+ (void)variant;
+ (void)error;
+ return TRUE;
+}
+
+ThemeGtk3Variant
+theme_gtk3_variant_for_theme (const char *theme_id)
+{
+ if (g_str_has_suffix (theme_id, "dark"))
+ return THEME_GTK3_VARIANT_PREFER_DARK;
+ return THEME_GTK3_VARIANT_PREFER_LIGHT;
+}
+
+void
+theme_gtk3_disable (void)
+{
+}
+
+gboolean
+theme_gtk3_is_active (void)
+{
+ return FALSE;
+}
+
+#include "../theme-preferences.c"
+
+static void
+test_removed_selected_theme_commits_fallback_and_applies (void)
+{
+ GtkWidget *page;
+ theme_preferences_ui *ui;
+ struct zoitechatprefs setup_prefs;
+
+ if (!gtk_available)
+ {
+ g_test_skip ("GTK display not available");
+ return;
+ }
+
+ memset (&setup_prefs, 0, sizeof (setup_prefs));
+ memset (&prefs, 0, sizeof (prefs));
+ 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;
+ removed_selected = FALSE;
+ apply_current_calls = 0;
+ applied_theme_id[0] = '\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);
+
+ g_assert_nonnull (ui->gtk3_remove);
+ gtk_button_clicked (GTK_BUTTON (ui->gtk3_remove));
+
+ g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
+ g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
+ g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
+ g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
+ g_assert_cmpint (apply_current_calls, ==, 1);
+ g_assert_cmpstr (applied_theme_id, ==, "fallback-theme");
+ g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
+
+ gtk_widget_destroy (page);
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+ gtk_available = gtk_init_check (&argc, &argv);
+ g_test_add_func ("/theme/preferences/gtk3_removed_selection_applies_fallback",
+ test_removed_selected_theme_commits_fallback_and_applies);
+ return g_test_run ();
+}
diff --git a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c
index 25ed1083..bfb8dff7 100644
--- a/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c
+++ b/src/fe-gtk/theme/tests/test-theme-runtime-persistence.c
@@ -1,4 +1,5 @@
#include
+#include
#include
#include
#include
@@ -267,6 +268,91 @@ test_ui_edits_persist_without_legacy_array_mutation (void)
g_assert_true (colors_equal (&dark_loaded, &dark_expected));
}
+static void
+test_gtk_map_colors_blend_with_palette_without_transparency (void)
+{
+ ThemeGtkPaletteMap map = { 0 };
+ ThemeWidgetStyleValues base_values;
+ ThemeWidgetStyleValues values;
+ GdkRGBA mapped_bg;
+ double alpha;
+ double expected_red;
+ double expected_green;
+ double expected_blue;
+
+ setup_temp_home ();
+ theme_runtime_load ();
+ theme_runtime_get_widget_style_values (&base_values);
+
+ map.enabled = TRUE;
+ g_assert_true (gdk_rgba_parse (&map.text_foreground, "rgba(10, 20, 30, 0.25)"));
+ g_assert_true (gdk_rgba_parse (&map.text_background, "rgba(40, 50, 60, 0.30)"));
+ g_assert_true (gdk_rgba_parse (&map.selection_foreground, "rgba(70, 80, 90, 0.35)"));
+ g_assert_true (gdk_rgba_parse (&map.selection_background, "rgba(100, 110, 120, 0.40)"));
+ g_assert_true (gdk_rgba_parse (&map.accent, "rgba(130, 140, 150, 0.45)"));
+
+ theme_runtime_get_widget_style_values_mapped (&map, &values);
+ g_assert_cmpfloat (values.foreground.alpha, ==, 1.0);
+ g_assert_cmpfloat (values.background.alpha, ==, 1.0);
+ g_assert_cmpfloat (values.selection_foreground.alpha, ==, 1.0);
+ g_assert_cmpfloat (values.selection_background.alpha, ==, 1.0);
+
+ mapped_bg = map.text_background;
+ alpha = mapped_bg.alpha;
+ expected_red = (mapped_bg.red * alpha) + (base_values.background.red * (1.0 - alpha));
+ expected_green = (mapped_bg.green * alpha) + (base_values.background.green * (1.0 - alpha));
+ expected_blue = (mapped_bg.blue * alpha) + (base_values.background.blue * (1.0 - alpha));
+ g_assert_true (fabs (values.background.red - expected_red) < 0.0001);
+ g_assert_true (fabs (values.background.green - expected_green) < 0.0001);
+ g_assert_true (fabs (values.background.blue - expected_blue) < 0.0001);
+}
+
+
+static void
+test_gtk_map_uses_theme_defaults_until_custom_token_is_set (void)
+{
+ ThemeGtkPaletteMap map = { 0 };
+ ThemeWidgetStyleValues values;
+ GdkRGBA custom;
+
+ setup_temp_home ();
+ theme_runtime_load ();
+
+ map.enabled = TRUE;
+ g_assert_true (gdk_rgba_parse (&map.text_foreground, "#010203"));
+ g_assert_true (gdk_rgba_parse (&map.text_background, "#111213"));
+ g_assert_true (gdk_rgba_parse (&map.selection_foreground, "#212223"));
+ g_assert_true (gdk_rgba_parse (&map.selection_background, "#313233"));
+ g_assert_true (gdk_rgba_parse (&map.accent, "#414243"));
+
+ theme_runtime_get_widget_style_values_mapped (&map, &values);
+ g_assert_true (colors_equal (&values.foreground, &map.text_foreground));
+
+ g_assert_true (gdk_rgba_parse (&custom, "#a1b2c3"));
+ theme_runtime_user_set_color (THEME_TOKEN_TEXT_FOREGROUND, &custom);
+ theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, NULL);
+ theme_runtime_get_widget_style_values_mapped (&map, &values);
+ g_assert_true (colors_equal (&values.foreground, &custom));
+}
+
+static void
+test_save_writes_only_custom_token_keys (void)
+{
+ GdkRGBA custom;
+ char *cfg;
+
+ setup_temp_home ();
+ theme_runtime_load ();
+ g_assert_true (gdk_rgba_parse (&custom, "#445566"));
+ theme_runtime_user_set_color (THEME_TOKEN_TEXT_FOREGROUND, &custom);
+ theme_runtime_save ();
+
+ cfg = read_colors_conf ();
+ g_assert_nonnull (g_strstr_len (cfg, -1, "theme.mode.light.token.text_foreground"));
+ g_assert_null (g_strstr_len (cfg, -1, "theme.mode.light.token.text_background"));
+ g_free (cfg);
+}
+
int
main (int argc, char **argv)
{
@@ -277,5 +363,11 @@ main (int argc, char **argv)
test_loads_legacy_color_keys_via_migration_loader);
g_test_add_func ("/theme/runtime/ui_edits_persist_without_legacy_array_mutation",
test_ui_edits_persist_without_legacy_array_mutation);
+ g_test_add_func ("/theme/runtime/gtk_map_colors_blend_with_palette_without_transparency",
+ test_gtk_map_colors_blend_with_palette_without_transparency);
+ g_test_add_func ("/theme/runtime/gtk_map_uses_theme_defaults_until_custom_token_is_set",
+ test_gtk_map_uses_theme_defaults_until_custom_token_is_set);
+ g_test_add_func ("/theme/runtime/save_writes_only_custom_token_keys",
+ test_save_writes_only_custom_token_keys);
return g_test_run ();
}
diff --git a/src/fe-gtk/theme/theme-access.c b/src/fe-gtk/theme/theme-access.c
index 621649c1..a2d10613 100644
--- a/src/fe-gtk/theme/theme-access.c
+++ b/src/fe-gtk/theme/theme-access.c
@@ -1,6 +1,7 @@
#include "theme-access.h"
#include "theme-runtime.h"
+#include "theme-gtk3.h"
static gboolean
@@ -17,6 +18,32 @@ theme_token_to_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, gu
return TRUE;
}
+static gboolean
+theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map)
+{
+ GtkStyleContext *context;
+ GdkRGBA accent;
+
+ g_return_val_if_fail (out_map != NULL, FALSE);
+ if (!theme_gtk3_is_active () || widget == NULL)
+ return FALSE;
+
+ context = gtk_widget_get_style_context (widget);
+ if (context == NULL)
+ return FALSE;
+
+ gtk_style_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground);
+ gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_background);
+ gtk_style_context_get_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_foreground);
+ gtk_style_context_get_background_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_background);
+ gtk_style_context_get_color (context, GTK_STATE_FLAG_LINK, &accent);
+ if (accent.alpha <= 0.0)
+ accent = out_map->selection_background;
+ out_map->accent = accent;
+ out_map->enabled = TRUE;
+ return TRUE;
+}
+
gboolean
theme_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
{
@@ -63,11 +90,37 @@ theme_get_legacy_color (int legacy_idx, GdkRGBA *out_rgba)
void
theme_get_widget_style_values (ThemeWidgetStyleValues *out_values)
{
+ theme_get_widget_style_values_for_widget (NULL, out_values);
+}
+
+void
+theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *out_values)
+{
+ ThemeGtkPaletteMap gtk_map = { 0 };
+
+ if (theme_access_get_gtk_palette_map (widget, >k_map))
+ {
+ theme_runtime_get_widget_style_values_mapped (>k_map, out_values);
+ return;
+ }
theme_runtime_get_widget_style_values (out_values);
}
void
theme_get_xtext_colors (XTextColor *palette, size_t palette_len)
{
+ theme_get_xtext_colors_for_widget (NULL, palette, palette_len);
+}
+
+void
+theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len)
+{
+ ThemeGtkPaletteMap gtk_map = { 0 };
+
+ if (theme_access_get_gtk_palette_map (widget, >k_map))
+ {
+ theme_runtime_get_xtext_colors_mapped (>k_map, palette, palette_len);
+ return;
+ }
theme_runtime_get_xtext_colors (palette, palette_len);
}
diff --git a/src/fe-gtk/theme/theme-access.h b/src/fe-gtk/theme/theme-access.h
index 9db6ddb8..9c170d83 100644
--- a/src/fe-gtk/theme/theme-access.h
+++ b/src/fe-gtk/theme/theme-access.h
@@ -15,6 +15,8 @@ gboolean theme_get_mirc_color_rgb16 (unsigned int mirc_index, guint16 *red, guin
G_DEPRECATED_FOR (theme_get_color)
gboolean theme_get_legacy_color (int legacy_idx, GdkRGBA *out_rgba);
void theme_get_widget_style_values (ThemeWidgetStyleValues *out_values);
+void theme_get_widget_style_values_for_widget (GtkWidget *widget, ThemeWidgetStyleValues *out_values);
void theme_get_xtext_colors (XTextColor *palette, size_t palette_len);
+void theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len);
#endif
diff --git a/src/fe-gtk/theme/theme-application.c b/src/fe-gtk/theme/theme-application.c
index fd8e2c5c..6f66335e 100644
--- a/src/fe-gtk/theme/theme-application.c
+++ b/src/fe-gtk/theme/theme-application.c
@@ -4,6 +4,7 @@
#include "../../common/zoitechatc.h"
#include "theme-css.h"
#include "theme-runtime.h"
+#include "theme-gtk3.h"
#include "../maingui.h"
#ifdef G_OS_WIN32
@@ -13,23 +14,52 @@ static void
theme_application_apply_windows_theme (gboolean dark)
{
GtkSettings *settings = gtk_settings_get_default ();
+ static GtkCssProvider *win_theme_provider = NULL;
+ static gboolean win_theme_provider_installed = FALSE;
+ GdkScreen *screen;
+ gboolean prefer_dark = dark;
+ char *css;
+
+ if (theme_gtk3_is_active ())
+ {
+ if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK)
+ prefer_dark = TRUE;
+ else if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_LIGHT)
+ prefer_dark = FALSE;
+ }
if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings),
"gtk-application-prefer-dark-theme"))
+ g_object_set (settings, "gtk-application-prefer-dark-theme", prefer_dark, NULL);
+
+ screen = gdk_screen_get_default ();
+ if (!screen)
+ return;
+
+ if (theme_gtk3_is_active ())
{
- g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL);
+ if (win_theme_provider_installed && win_theme_provider)
+ {
+ gtk_style_context_remove_provider_for_screen (screen,
+ GTK_STYLE_PROVIDER (win_theme_provider));
+ win_theme_provider_installed = FALSE;
+ }
+ return;
}
+ if (!win_theme_provider)
+ win_theme_provider = gtk_css_provider_new ();
+
+ css = theme_css_build_toplevel_classes ();
+ gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL);
+ g_free (css);
+
+ if (!win_theme_provider_installed)
{
- static GtkCssProvider *win_theme_provider = NULL;
- char *css = theme_css_build_toplevel_classes ();
-
- if (!win_theme_provider)
- win_theme_provider = gtk_css_provider_new ();
-
- gtk_css_provider_load_from_data (win_theme_provider, css, -1, NULL);
- theme_css_apply_app_provider (GTK_STYLE_PROVIDER (win_theme_provider));
- g_free (css);
+ gtk_style_context_add_provider_for_screen (screen,
+ GTK_STYLE_PROVIDER (win_theme_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1);
+ win_theme_provider_installed = TRUE;
}
}
#endif
@@ -77,7 +107,7 @@ theme_application_update_input_style (InputStyle *style)
style->font_desc = pango_font_description_from_string ("sans 11");
}
- theme_css_reload_input_style (prefs.hex_gui_input_style, style->font_desc);
+ theme_css_reload_input_style (FALSE, style->font_desc);
return style;
}
diff --git a/src/fe-gtk/theme/theme-css.c b/src/fe-gtk/theme/theme-css.c
index b7a2ee59..ca59056e 100644
--- a/src/fe-gtk/theme/theme-css.c
+++ b/src/fe-gtk/theme/theme-css.c
@@ -1,6 +1,7 @@
#include "theme-css.h"
#include "theme-runtime.h"
+#include "theme-gtk3.h"
#include "../gtkutil.h"
#include
@@ -10,7 +11,7 @@ static const char *theme_css_selector_palette_class = "zoitechat-palette";
static const char *theme_css_selector_dark_class = "zoitechat-dark";
static const char *theme_css_selector_light_class = "zoitechat-light";
static const char *theme_css_palette_provider_key = "zoitechat-palette-provider";
-static const guint theme_css_provider_priority = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION;
+static const guint theme_css_provider_priority = GTK_STYLE_PROVIDER_PRIORITY_USER;
typedef struct
{
@@ -222,6 +223,8 @@ theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkR
GString *css;
gchar *bg_color = NULL;
gchar *fg_color = NULL;
+ gchar *sel_bg_color = NULL;
+ gchar *sel_fg_color = NULL;
if (!widget)
return;
@@ -260,12 +263,30 @@ theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkR
fg_color = gdk_rgba_to_string (fg);
g_string_append_printf (css, " color: %s;", fg_color);
}
+ {
+ GdkRGBA selection_bg;
+ GdkRGBA selection_fg;
+ if (theme_runtime_get_color (THEME_TOKEN_SELECTION_BACKGROUND, &selection_bg))
+ sel_bg_color = gdk_rgba_to_string (&selection_bg);
+ if (theme_runtime_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &selection_fg))
+ sel_fg_color = gdk_rgba_to_string (&selection_fg);
+ }
gtkutil_append_font_css (css, font_desc);
g_string_append (css, " }");
- g_string_append_printf (css, ".%s *:selected {", theme_css_selector_palette_class);
+ g_string_append_printf (css, ".%s, .%s *, .%s treeview, .%s treeview.view, .%s treeview.view text, .%s treeview.view cell, .%s treeview.view row, .%s list, .%s list row, .%s text {", theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class);
if (bg)
- g_string_append (css, " background-color: @theme_selected_bg_color;");
+ g_string_append_printf (css, " background-color: %s;", bg_color);
if (fg)
+ g_string_append_printf (css, " color: %s;", fg_color);
+ g_string_append (css, " }");
+ g_string_append_printf (css, ".%s *:selected, .%s *:selected:focus, .%s *:selected:hover, .%s treeview.view:selected, .%s treeview.view:selected:focus, .%s treeview.view:selected:hover, .%s row:selected, .%s row:selected:focus, .%s row:selected:hover {", theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class, theme_css_selector_palette_class);
+ if (sel_bg_color)
+ g_string_append_printf (css, " background-color: %s;", sel_bg_color);
+ else if (bg)
+ g_string_append (css, " background-color: @theme_selected_bg_color;");
+ if (sel_fg_color)
+ g_string_append_printf (css, " color: %s;", sel_fg_color);
+ else if (fg)
g_string_append (css, " color: @theme_selected_fg_color;");
g_string_append (css, " }");
@@ -277,22 +298,46 @@ theme_css_apply_palette_widget (GtkWidget *widget, const GdkRGBA *bg, const GdkR
g_string_free (css, TRUE);
g_free (bg_color);
g_free (fg_color);
+ g_free (sel_bg_color);
+ g_free (sel_fg_color);
}
char *
theme_css_build_toplevel_classes (void)
{
return g_strdup_printf (
- "window.%s, .%s {"
+ "window.%s, window.%s:backdrop, .%s {"
"background-color: #202020;"
"color: #f0f0f0;"
+ "border-color: #202020;"
"}"
- "window.%s, .%s {"
+ "window.%s menubar, window.%s menubar:backdrop, window.%s menuitem, window.%s menuitem:backdrop {"
+ "background-color: #202020;"
+ "color: #f0f0f0;"
+ "border-color: #202020;"
+ "}"
+ "window.%s, window.%s:backdrop, .%s {"
"background-color: #f6f6f6;"
"color: #101010;"
+ "border-color: #f6f6f6;"
+ "}"
+ "window.%s menubar, window.%s menubar:backdrop, window.%s menuitem, window.%s menuitem:backdrop {"
+ "background-color: #f6f6f6;"
+ "color: #101010;"
+ "border-color: #f6f6f6;"
"}",
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);
}
diff --git a/src/fe-gtk/theme/theme-gtk3.c b/src/fe-gtk/theme/theme-gtk3.c
new file mode 100644
index 00000000..5d31a435
--- /dev/null
+++ b/src/fe-gtk/theme/theme-gtk3.c
@@ -0,0 +1,941 @@
+#include "theme-gtk3.h"
+
+#include
+#include
+#include
+#include
+
+#include "theme-policy.h"
+#include "../../common/gtk3-theme-service.h"
+#include "../../common/zoitechat.h"
+#include "../../common/zoitechatc.h"
+
+static GPtrArray *theme_gtk3_providers_base;
+static GPtrArray *theme_gtk3_providers_variant;
+static GHashTable *theme_gtk3_provider_cache;
+static gboolean theme_gtk3_active;
+static char *theme_gtk3_current_id;
+static ThemeGtk3Variant theme_gtk3_current_variant;
+
+typedef struct
+{
+ GHashTable *defaults;
+ char **icon_search_path;
+ gint icon_search_path_count;
+ gboolean icon_search_path_captured;
+} ThemeGtk3SettingsState;
+
+static ThemeGtk3SettingsState theme_gtk3_settings_state;
+
+static gboolean settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value);
+
+static gboolean
+theme_gtk3_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;
+ g_free (lower);
+ return dark;
+}
+
+static ThemeGtk3Variant
+theme_gtk3_infer_variant (const ZoitechatGtk3Theme *theme)
+{
+ char *css_dir;
+ char *light_css;
+ gboolean has_light_css;
+ ThemeGtk3Variant variant;
+
+ if (!theme)
+ return THEME_GTK3_VARIANT_PREFER_LIGHT;
+
+ css_dir = zoitechat_gtk3_theme_pick_css_dir (theme->path);
+ light_css = css_dir ? g_build_filename (theme->path, css_dir, "gtk.css", NULL) : NULL;
+ has_light_css = light_css && g_file_test (light_css, G_FILE_TEST_IS_REGULAR);
+ g_free (light_css);
+ g_free (css_dir);
+
+ variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
+ if ((theme->has_dark_variant && !has_light_css) ||
+ theme_gtk3_theme_name_is_dark (theme->id) ||
+ theme_gtk3_theme_name_is_dark (theme->display_name))
+ variant = THEME_GTK3_VARIANT_PREFER_DARK;
+
+ return variant;
+}
+
+static void
+settings_value_free (gpointer data)
+{
+ GValue *value = data;
+
+ if (!value)
+ return;
+
+ g_value_unset (value);
+ g_free (value);
+}
+
+static GValue *
+settings_value_dup (const GValue *source)
+{
+ GValue *copy;
+
+ copy = g_new0 (GValue, 1);
+ g_value_init (copy, G_VALUE_TYPE (source));
+ g_value_copy (source, copy);
+ return copy;
+}
+
+static GHashTable *
+settings_defaults_table (void)
+{
+ if (!theme_gtk3_settings_state.defaults)
+ {
+ theme_gtk3_settings_state.defaults = g_hash_table_new_full (
+ g_str_hash,
+ g_str_equal,
+ g_free,
+ settings_value_free);
+ }
+
+ return theme_gtk3_settings_state.defaults;
+}
+
+
+
+
+static void
+settings_rescan_icon_theme (void)
+{
+ GtkIconTheme *icon_theme;
+
+ icon_theme = gtk_icon_theme_get_default ();
+ if (!icon_theme)
+ return;
+
+ gtk_icon_theme_rescan_if_needed (icon_theme);
+}
+
+static void
+theme_gtk3_reset_widgets (void)
+{
+ GdkScreen *screen = gdk_screen_get_default ();
+
+ if (screen)
+ gtk_style_context_reset_widgets (screen);
+}
+
+static void
+settings_capture_icon_search_path (void)
+{
+ GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
+
+ if (!icon_theme || theme_gtk3_settings_state.icon_search_path_captured)
+ return;
+
+ gtk_icon_theme_get_search_path (icon_theme, &theme_gtk3_settings_state.icon_search_path, &theme_gtk3_settings_state.icon_search_path_count);
+ theme_gtk3_settings_state.icon_search_path_captured = TRUE;
+}
+
+static void
+settings_append_icon_search_path (const char *path)
+{
+ GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
+
+ if (!icon_theme || !path || !g_file_test (path, G_FILE_TEST_IS_DIR))
+ return;
+
+ settings_capture_icon_search_path ();
+ gtk_icon_theme_append_search_path (icon_theme, path);
+ gtk_icon_theme_rescan_if_needed (icon_theme);
+}
+
+static void
+settings_apply_icon_paths (const char *theme_root)
+{
+ char *icons_dir;
+ char *theme_parent;
+
+ if (!theme_root)
+ return;
+
+ icons_dir = g_build_filename (theme_root, "icons", NULL);
+ theme_parent = g_path_get_dirname (theme_root);
+ settings_append_icon_search_path (icons_dir);
+ settings_append_icon_search_path (theme_root);
+ settings_append_icon_search_path (theme_parent);
+ g_free (theme_parent);
+ g_free (icons_dir);
+}
+
+static void
+settings_restore_icon_search_path (void)
+{
+ GtkIconTheme *icon_theme = gtk_icon_theme_get_default ();
+
+ if (!icon_theme || !theme_gtk3_settings_state.icon_search_path_captured)
+ return;
+
+ gtk_icon_theme_set_search_path (icon_theme, (const char * const *) theme_gtk3_settings_state.icon_search_path, theme_gtk3_settings_state.icon_search_path_count);
+ gtk_icon_theme_rescan_if_needed (icon_theme);
+ g_strfreev (theme_gtk3_settings_state.icon_search_path);
+ theme_gtk3_settings_state.icon_search_path = NULL;
+ theme_gtk3_settings_state.icon_search_path_count = 0;
+ theme_gtk3_settings_state.icon_search_path_captured = FALSE;
+}
+
+static void
+theme_gtk3_parsing_error_cb (GtkCssProvider *provider, GtkCssSection *section, const GError *error, gpointer user_data)
+{
+ (void) provider;
+ (void) section;
+ (void) error;
+ (void) user_data;
+ g_signal_stop_emission_by_name (provider, "parsing-error");
+}
+
+static GHashTable *
+theme_gtk3_provider_cache_table (void)
+{
+ if (!theme_gtk3_provider_cache)
+ {
+ theme_gtk3_provider_cache = g_hash_table_new_full (
+ g_str_hash,
+ g_str_equal,
+ g_free,
+ g_object_unref);
+ }
+
+ return theme_gtk3_provider_cache;
+}
+
+static char *
+theme_gtk3_provider_cache_key (const char *theme_root, const char *css_dir, gboolean prefer_dark)
+{
+ return g_strdup_printf ("%s\n%s\n%d", theme_root, css_dir, prefer_dark ? 1 : 0);
+}
+
+static GtkCssProvider *
+theme_gtk3_provider_cache_load (const char *path, GError **error)
+{
+ GtkCssProvider *provider;
+
+ provider = gtk_css_provider_new ();
+ g_signal_connect (provider, "parsing-error", G_CALLBACK (theme_gtk3_parsing_error_cb), NULL);
+ if (!gtk_css_provider_load_from_path (provider, path, error))
+ {
+ g_object_unref (provider);
+ return NULL;
+ }
+
+ return provider;
+}
+
+static GtkCssProvider *
+theme_gtk3_provider_cache_get_or_load (const char *theme_root, const char *css_dir, gboolean prefer_dark, GError **error)
+{
+ GHashTable *cache;
+ char *key;
+ char *css_path;
+ GtkCssProvider *provider;
+
+ cache = theme_gtk3_provider_cache_table ();
+ key = theme_gtk3_provider_cache_key (theme_root, css_dir, prefer_dark);
+ provider = g_hash_table_lookup (cache, key);
+ if (provider)
+ {
+ g_object_ref (provider);
+ g_free (key);
+ return provider;
+ }
+
+ css_path = g_build_filename (theme_root, css_dir, prefer_dark ? "gtk-dark.css" : "gtk.css", NULL);
+ provider = theme_gtk3_provider_cache_load (css_path, error);
+ g_free (css_path);
+ if (!provider)
+ {
+ g_free (key);
+ return NULL;
+ }
+
+ g_hash_table_insert (cache, key, g_object_ref (provider));
+ return provider;
+}
+
+void
+theme_gtk3_invalidate_provider_cache (void)
+{
+ if (theme_gtk3_provider_cache)
+ g_hash_table_remove_all (theme_gtk3_provider_cache);
+}
+
+static void
+settings_apply_for_variant (ThemeGtk3Variant variant)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+ gboolean dark = FALSE;
+ GValue current = G_VALUE_INIT;
+ GParamSpec *property;
+
+ if (!settings)
+ return;
+
+ if (variant == THEME_GTK3_VARIANT_PREFER_DARK)
+ dark = TRUE;
+ else if (variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
+ dark = theme_policy_system_prefers_dark ();
+
+ property = g_object_class_find_property (G_OBJECT_GET_CLASS (settings), "gtk-application-prefer-dark-theme");
+ if (!property)
+ return;
+
+ g_value_init (¤t, G_PARAM_SPEC_VALUE_TYPE (property));
+ g_object_get_property (G_OBJECT (settings), "gtk-application-prefer-dark-theme", ¤t);
+ if (g_value_get_boolean (¤t) != dark)
+ g_object_set (settings, "gtk-application-prefer-dark-theme", dark, NULL);
+ g_value_unset (¤t);
+}
+
+static gboolean
+settings_theme_root_is_searchable (const char *theme_root)
+{
+ char *parent;
+ char *user_data_themes;
+ char *home_themes;
+ const gchar *const *system_data_dirs;
+ guint i;
+ gboolean searchable = FALSE;
+
+ if (!theme_root || !theme_root[0])
+ return FALSE;
+
+ parent = g_path_get_dirname (theme_root);
+ user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
+ home_themes = g_build_filename (g_get_home_dir (), ".themes", NULL);
+
+ if (g_strcmp0 (parent, user_data_themes) == 0 ||
+ g_strcmp0 (parent, home_themes) == 0 ||
+ g_strcmp0 (parent, "/usr/share/themes") == 0)
+ searchable = TRUE;
+
+ system_data_dirs = g_get_system_data_dirs ();
+ for (i = 0; !searchable && system_data_dirs && system_data_dirs[i]; i++)
+ {
+ char *system_themes = g_build_filename (system_data_dirs[i], "themes", NULL);
+ if (g_strcmp0 (parent, system_themes) == 0)
+ searchable = TRUE;
+ g_free (system_themes);
+ }
+
+ g_free (home_themes);
+ g_free (user_data_themes);
+ g_free (parent);
+ return searchable;
+}
+
+
+static gboolean
+settings_theme_link_search_path (const char *theme_root, const char *theme_name)
+{
+ char *themes_root;
+ char *link_path;
+ gboolean ok = TRUE;
+
+ if (!theme_root || !theme_name || !theme_name[0])
+ return FALSE;
+
+ themes_root = g_build_filename (g_get_user_data_dir (), "themes", NULL);
+ if (g_mkdir_with_parents (themes_root, 0700) != 0)
+ {
+ g_free (themes_root);
+ return FALSE;
+ }
+
+ link_path = g_build_filename (themes_root, theme_name, NULL);
+ if (!g_file_test (link_path, G_FILE_TEST_EXISTS))
+ {
+ GFile *link_file = g_file_new_for_path (link_path);
+ GError *link_error = NULL;
+ ok = g_file_make_symbolic_link (link_file, theme_root, NULL, &link_error);
+ g_clear_error (&link_error);
+ g_object_unref (link_file);
+ }
+
+ g_free (link_path);
+ g_free (themes_root);
+ return ok;
+}
+
+static void
+settings_apply_theme_name (const char *theme_root)
+{
+ GtkSettings *settings;
+ char *theme_name;
+
+ if (!theme_root)
+ return;
+
+ settings = gtk_settings_get_default ();
+ if (!settings)
+ return;
+
+ theme_name = g_path_get_basename (theme_root);
+ if (theme_name && theme_name[0])
+ {
+ gboolean searchable = settings_theme_root_is_searchable (theme_root);
+ if (!searchable)
+ searchable = settings_theme_link_search_path (theme_root, theme_name);
+ if (searchable)
+ settings_apply_property (settings, "gtk-theme-name", theme_name);
+ }
+ g_free (theme_name);
+}
+
+static gboolean
+settings_value_equal_typed (const GValue *a, const GValue *b, GType property_type)
+{
+ if (property_type == G_TYPE_BOOLEAN)
+ return g_value_get_boolean (a) == g_value_get_boolean (b);
+ if (property_type == G_TYPE_STRING)
+ return g_strcmp0 (g_value_get_string (a), g_value_get_string (b)) == 0;
+ if (property_type == G_TYPE_INT)
+ return g_value_get_int (a) == g_value_get_int (b);
+ if (property_type == G_TYPE_UINT)
+ return g_value_get_uint (a) == g_value_get_uint (b);
+ if (property_type == G_TYPE_FLOAT)
+ return g_value_get_float (a) == g_value_get_float (b);
+ if (property_type == G_TYPE_DOUBLE)
+ return g_value_get_double (a) == g_value_get_double (b);
+ if (G_TYPE_IS_ENUM (property_type))
+ return g_value_get_enum (a) == g_value_get_enum (b);
+ if (G_TYPE_IS_FLAGS (property_type))
+ return g_value_get_flags (a) == g_value_get_flags (b);
+ return FALSE;
+}
+
+static gboolean
+settings_parse_long (const char *text, glong min_value, glong max_value, glong *value)
+{
+ char *end = NULL;
+ gint64 parsed;
+
+ if (!text)
+ return FALSE;
+
+ parsed = g_ascii_strtoll (text, &end, 10);
+ if (end == text || *end != '\0')
+ return FALSE;
+ if (parsed < min_value || parsed > max_value)
+ return FALSE;
+
+ *value = (glong) parsed;
+ return TRUE;
+}
+
+static void
+settings_remember_default (GtkSettings *settings, const char *property_name, GParamSpec *property)
+{
+ GHashTable *defaults;
+ GValue current = G_VALUE_INIT;
+
+ if (!settings || !property_name || !property)
+ return;
+
+ defaults = settings_defaults_table ();
+ if (g_hash_table_contains (defaults, property_name))
+ return;
+
+ g_value_init (¤t, G_PARAM_SPEC_VALUE_TYPE (property));
+ g_object_get_property (G_OBJECT (settings), property_name, ¤t);
+ g_hash_table_insert (defaults, g_strdup (property_name), settings_value_dup (¤t));
+ g_value_unset (¤t);
+}
+
+static gboolean
+settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value)
+{
+ GParamSpec *property;
+ GValue value = G_VALUE_INIT;
+ GValue current = G_VALUE_INIT;
+ GType property_type;
+ gboolean ok = FALSE;
+ gboolean changed = TRUE;
+
+ property = g_object_class_find_property (G_OBJECT_GET_CLASS (settings), property_name);
+ if (!property)
+ return FALSE;
+
+ settings_remember_default (settings, property_name, property);
+ property_type = G_PARAM_SPEC_VALUE_TYPE (property);
+ g_value_init (&value, property_type);
+
+ if (property_type == G_TYPE_BOOLEAN)
+ {
+ if (g_ascii_strcasecmp (raw_value, "true") == 0 ||
+ g_ascii_strcasecmp (raw_value, "yes") == 0 ||
+ g_strcmp0 (raw_value, "1") == 0)
+ {
+ g_value_set_boolean (&value, TRUE);
+ ok = TRUE;
+ }
+ else if (g_ascii_strcasecmp (raw_value, "false") == 0 ||
+ g_ascii_strcasecmp (raw_value, "no") == 0 ||
+ g_strcmp0 (raw_value, "0") == 0)
+ {
+ g_value_set_boolean (&value, FALSE);
+ ok = TRUE;
+ }
+ }
+ else if (property_type == G_TYPE_STRING)
+ {
+ g_value_set_string (&value, raw_value);
+ ok = TRUE;
+ }
+ else if (property_type == G_TYPE_INT)
+ {
+ glong parsed;
+ if (settings_parse_long (raw_value, G_MININT, G_MAXINT, &parsed))
+ {
+ g_value_set_int (&value, (gint) parsed);
+ ok = TRUE;
+ }
+ }
+ else if (property_type == G_TYPE_UINT)
+ {
+ glong parsed;
+ if (settings_parse_long (raw_value, 0, G_MAXUINT, &parsed))
+ {
+ g_value_set_uint (&value, (guint) parsed);
+ ok = TRUE;
+ }
+ }
+ else if (property_type == G_TYPE_DOUBLE)
+ {
+ char *end = NULL;
+ double parsed = g_ascii_strtod (raw_value, &end);
+ if (end != raw_value && *end == '\0')
+ {
+ g_value_set_double (&value, parsed);
+ ok = TRUE;
+ }
+ }
+ else if (property_type == G_TYPE_FLOAT)
+ {
+ char *end = NULL;
+ double parsed = g_ascii_strtod (raw_value, &end);
+ if (end != raw_value && *end == '\0')
+ {
+ g_value_set_float (&value, (gfloat) parsed);
+ ok = TRUE;
+ }
+ }
+ else if (G_TYPE_IS_ENUM (property_type))
+ {
+ GEnumClass *enum_class = g_type_class_ref (property_type);
+ GEnumValue *enum_value = g_enum_get_value_by_nick (enum_class, raw_value);
+ if (!enum_value)
+ enum_value = g_enum_get_value_by_name (enum_class, raw_value);
+ if (!enum_value)
+ {
+ glong parsed;
+ if (settings_parse_long (raw_value, G_MININT, G_MAXINT, &parsed))
+ enum_value = g_enum_get_value (enum_class, (gint) parsed);
+ }
+ if (enum_value)
+ {
+ g_value_set_enum (&value, enum_value->value);
+ ok = TRUE;
+ }
+ g_type_class_unref (enum_class);
+ }
+ else if (G_TYPE_IS_FLAGS (property_type))
+ {
+ GFlagsClass *flags_class = g_type_class_ref (property_type);
+ char **tokens = g_strsplit_set (raw_value, ",|", -1);
+ guint flags_value = 0;
+ guint i = 0;
+ for (; tokens && tokens[i]; i++)
+ {
+ char *token = g_strstrip (tokens[i]);
+ GFlagsValue *flag_value;
+ if (!token[0])
+ continue;
+ flag_value = g_flags_get_value_by_nick (flags_class, token);
+ if (!flag_value)
+ flag_value = g_flags_get_value_by_name (flags_class, token);
+ if (!flag_value)
+ {
+ glong parsed;
+ if (!settings_parse_long (token, 0, G_MAXUINT, &parsed))
+ {
+ ok = FALSE;
+ break;
+ }
+ flags_value |= (guint) parsed;
+ ok = TRUE;
+ continue;
+ }
+ flags_value |= flag_value->value;
+ ok = TRUE;
+ }
+ if (ok)
+ g_value_set_flags (&value, flags_value);
+ g_strfreev (tokens);
+ g_type_class_unref (flags_class);
+ }
+
+
+ if (ok)
+ {
+ g_value_init (¤t, property_type);
+ g_object_get_property (G_OBJECT (settings), property_name, ¤t);
+ changed = !settings_value_equal_typed (¤t, &value, property_type);
+ g_value_unset (¤t);
+ }
+
+ if (ok && changed)
+ g_object_set_property (G_OBJECT (settings), property_name, &value);
+
+ g_value_unset (&value);
+ return ok;
+}
+
+static void
+settings_restore_defaults (void)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+ GHashTableIter iter;
+ gpointer key;
+ gpointer value;
+
+ if (settings && theme_gtk3_settings_state.defaults)
+ {
+ g_hash_table_iter_init (&iter, theme_gtk3_settings_state.defaults);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_object_set_property (G_OBJECT (settings), (const char *) key, (const GValue *) value);
+
+ g_hash_table_remove_all (theme_gtk3_settings_state.defaults);
+ }
+
+ settings_rescan_icon_theme ();
+ settings_restore_icon_search_path ();
+}
+
+static void
+settings_cleanup (void)
+{
+ if (theme_gtk3_settings_state.defaults)
+ {
+ g_hash_table_destroy (theme_gtk3_settings_state.defaults);
+ theme_gtk3_settings_state.defaults = NULL;
+ }
+
+ if (theme_gtk3_settings_state.icon_search_path_captured)
+ settings_restore_icon_search_path ();
+}
+
+static void
+settings_apply_from_file (const char *theme_root, const char *css_dir)
+{
+ GtkSettings *settings;
+ GPtrArray *settings_paths;
+ char *selected_path;
+ char *fallback_path;
+ guint layer;
+
+ settings = gtk_settings_get_default ();
+ if (!settings)
+ return;
+
+ if (!css_dir)
+ return;
+
+ settings_apply_icon_paths (theme_root);
+ settings_paths = g_ptr_array_new_with_free_func (g_free);
+ selected_path = g_build_filename (theme_root, css_dir, "settings.ini", NULL);
+ fallback_path = g_build_filename (theme_root, "gtk-3.0", "settings.ini", NULL);
+ if (g_strcmp0 (css_dir, "gtk-3.0") != 0)
+ g_ptr_array_add (settings_paths, fallback_path);
+ else
+ g_free (fallback_path);
+ g_ptr_array_add (settings_paths, selected_path);
+
+ for (layer = 0; layer < settings_paths->len; layer++)
+ {
+ GKeyFile *keyfile;
+ char **keys;
+ gsize n_keys = 0;
+ gsize i;
+ const char *settings_path = g_ptr_array_index (settings_paths, layer);
+
+ if (!g_file_test (settings_path, G_FILE_TEST_IS_REGULAR))
+ continue;
+
+ keyfile = g_key_file_new ();
+ if (!g_key_file_load_from_file (keyfile, settings_path, G_KEY_FILE_NONE, NULL))
+ {
+ g_key_file_unref (keyfile);
+ continue;
+ }
+
+ keys = g_key_file_get_keys (keyfile, "Settings", &n_keys, NULL);
+ for (i = 0; keys && i < n_keys; i++)
+ {
+ char *raw_value;
+
+ raw_value = g_key_file_get_value (keyfile, "Settings", keys[i], NULL);
+ if (!raw_value)
+ continue;
+
+ settings_apply_property (settings, keys[i], raw_value);
+ g_free (raw_value);
+ }
+
+ g_strfreev (keys);
+ g_key_file_unref (keyfile);
+ }
+
+ settings_rescan_icon_theme ();
+ g_ptr_array_unref (settings_paths);
+}
+
+static void
+theme_gtk3_remove_provider (void)
+{
+ GdkScreen *screen = gdk_screen_get_default ();
+ guint i;
+
+ if (screen && theme_gtk3_providers_variant)
+ {
+ for (i = 0; i < theme_gtk3_providers_variant->len; i++)
+ {
+ GtkCssProvider *provider = g_ptr_array_index (theme_gtk3_providers_variant, i);
+ gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (provider));
+ }
+ }
+ if (screen && theme_gtk3_providers_base)
+ {
+ for (i = 0; i < theme_gtk3_providers_base->len; i++)
+ {
+ GtkCssProvider *provider = g_ptr_array_index (theme_gtk3_providers_base, i);
+ gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (provider));
+ }
+ }
+
+ if (theme_gtk3_providers_variant)
+ g_ptr_array_unref (theme_gtk3_providers_variant);
+ if (theme_gtk3_providers_base)
+ g_ptr_array_unref (theme_gtk3_providers_base);
+ theme_gtk3_providers_variant = NULL;
+ theme_gtk3_providers_base = NULL;
+ settings_restore_defaults ();
+ theme_gtk3_reset_widgets ();
+ theme_gtk3_active = FALSE;
+}
+
+static gboolean
+load_css_with_variant (ZoitechatGtk3Theme *theme, ThemeGtk3Variant variant, GError **error)
+{
+ gboolean prefer_dark = FALSE;
+ GdkScreen *screen;
+ GPtrArray *chain;
+ guint i;
+
+ if (variant == THEME_GTK3_VARIANT_PREFER_DARK)
+ prefer_dark = TRUE;
+ else if (variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
+ prefer_dark = theme_policy_system_prefers_dark ();
+
+ settings_apply_theme_name (theme->path);
+
+ chain = zoitechat_gtk3_theme_build_inheritance_chain (theme->path);
+ if (!chain || chain->len == 0)
+ {
+ if (chain)
+ g_ptr_array_unref (chain);
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme CSS not found."), FALSE;
+ }
+
+ theme_gtk3_providers_base = g_ptr_array_new_with_free_func (g_object_unref);
+ theme_gtk3_providers_variant = g_ptr_array_new_with_free_func (g_object_unref);
+
+ screen = gdk_screen_get_default ();
+ for (i = 0; i < chain->len; i++)
+ {
+ const char *theme_root = g_ptr_array_index (chain, i);
+ char *css_dir = zoitechat_gtk3_theme_pick_css_dir_for_minor (theme_root, gtk_get_minor_version ());
+ char *variant_css;
+ GtkCssProvider *provider;
+ GtkCssProvider *variant_provider;
+
+ if (!css_dir)
+ continue;
+
+ provider = theme_gtk3_provider_cache_get_or_load (theme_root, css_dir, FALSE, error);
+ if (!provider)
+ {
+ g_free (css_dir);
+ g_ptr_array_unref (chain);
+ return FALSE;
+ }
+ if (screen)
+ gtk_style_context_add_provider_for_screen (screen,
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER + (gint) (i * 2));
+ g_ptr_array_add (theme_gtk3_providers_base, provider);
+
+ variant_css = g_build_filename (theme_root, css_dir, "gtk-dark.css", NULL);
+ if (prefer_dark && g_file_test (variant_css, G_FILE_TEST_IS_REGULAR))
+ {
+ variant_provider = theme_gtk3_provider_cache_get_or_load (theme_root, css_dir, TRUE, error);
+ if (!variant_provider)
+ {
+ g_free (variant_css);
+ g_free (css_dir);
+ g_ptr_array_unref (chain);
+ return FALSE;
+ }
+ if (screen)
+ gtk_style_context_add_provider_for_screen (screen,
+ GTK_STYLE_PROVIDER (variant_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER + (gint) (i * 2) + 1);
+ g_ptr_array_add (theme_gtk3_providers_variant, variant_provider);
+ }
+ g_free (variant_css);
+
+ settings_apply_from_file (theme_root, css_dir);
+ g_free (css_dir);
+ }
+
+ g_ptr_array_unref (chain);
+ settings_apply_for_variant (variant);
+ theme_gtk3_reset_widgets ();
+ theme_gtk3_active = TRUE;
+ return TRUE;
+}
+
+static gboolean
+theme_gtk3_apply_internal (const char *theme_id, ThemeGtk3Variant variant, gboolean force_reload, GError **error)
+{
+ ZoitechatGtk3Theme *theme;
+ char *previous_id = g_strdup (theme_gtk3_current_id);
+ ThemeGtk3Variant previous_variant = theme_gtk3_current_variant;
+ gboolean had_previous = theme_gtk3_active && previous_id && previous_id[0];
+ gboolean ok;
+
+ if (!force_reload &&
+ theme_gtk3_active &&
+ g_strcmp0 (theme_gtk3_current_id, theme_id) == 0 &&
+ theme_gtk3_current_variant == variant)
+ return TRUE;
+
+ theme = zoitechat_gtk3_theme_find_by_id (theme_id);
+ if (!theme)
+ {
+ g_free (previous_id);
+ return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE;
+ }
+
+ theme_gtk3_remove_provider ();
+ if (force_reload)
+ theme_gtk3_invalidate_provider_cache ();
+ ok = load_css_with_variant (theme, variant, error);
+ zoitechat_gtk3_theme_free (theme);
+
+ if (ok)
+ {
+ g_free (theme_gtk3_current_id);
+ theme_gtk3_current_id = g_strdup (theme_id);
+ theme_gtk3_current_variant = variant;
+ g_free (previous_id);
+ return TRUE;
+ }
+
+ if (had_previous)
+ {
+ GError *restore_error = NULL;
+ theme = zoitechat_gtk3_theme_find_by_id (previous_id);
+ if (theme)
+ {
+ if (load_css_with_variant (theme, previous_variant, &restore_error))
+ {
+ g_free (theme_gtk3_current_id);
+ theme_gtk3_current_id = g_strdup (previous_id);
+ theme_gtk3_current_variant = previous_variant;
+ }
+ zoitechat_gtk3_theme_free (theme);
+ }
+ g_clear_error (&restore_error);
+ }
+
+ g_free (previous_id);
+ return ok;
+}
+
+gboolean
+theme_gtk3_apply (const char *theme_id, ThemeGtk3Variant variant, GError **error)
+{
+ return theme_gtk3_apply_internal (theme_id, variant, FALSE, error);
+}
+
+gboolean
+theme_gtk3_refresh (const char *theme_id, ThemeGtk3Variant variant, GError **error)
+{
+ return theme_gtk3_apply_internal (theme_id, variant, TRUE, error);
+}
+
+ThemeGtk3Variant
+theme_gtk3_variant_for_theme (const char *theme_id)
+{
+ ZoitechatGtk3Theme *theme;
+ ThemeGtk3Variant variant;
+
+ theme = zoitechat_gtk3_theme_find_by_id (theme_id);
+ if (!theme)
+ return THEME_GTK3_VARIANT_PREFER_LIGHT;
+
+ variant = theme_gtk3_infer_variant (theme);
+ zoitechat_gtk3_theme_free (theme);
+ return variant;
+}
+
+void
+theme_gtk3_disable (void)
+{
+ theme_gtk3_remove_provider ();
+ g_clear_pointer (&theme_gtk3_current_id, g_free);
+ theme_gtk3_invalidate_provider_cache ();
+ g_clear_pointer (&theme_gtk3_provider_cache, g_hash_table_destroy);
+ settings_cleanup ();
+}
+
+void
+theme_gtk3_init (void)
+{
+ theme_gtk3_apply_current (NULL);
+}
+
+gboolean
+theme_gtk3_apply_current (GError **error)
+{
+ if (!prefs.hex_gui_gtk3_theme[0])
+ {
+ theme_gtk3_disable ();
+ return TRUE;
+ }
+
+ return theme_gtk3_apply (prefs.hex_gui_gtk3_theme, (ThemeGtk3Variant) prefs.hex_gui_gtk3_variant, error);
+}
+
+gboolean
+theme_gtk3_is_active (void)
+{
+ return theme_gtk3_active;
+}
diff --git a/src/fe-gtk/theme/theme-gtk3.h b/src/fe-gtk/theme/theme-gtk3.h
new file mode 100644
index 00000000..f150ba90
--- /dev/null
+++ b/src/fe-gtk/theme/theme-gtk3.h
@@ -0,0 +1,22 @@
+#ifndef ZOITECHAT_THEME_GTK3_H
+#define ZOITECHAT_THEME_GTK3_H
+
+#include
+
+typedef enum
+{
+ THEME_GTK3_VARIANT_FOLLOW_SYSTEM = 0,
+ THEME_GTK3_VARIANT_PREFER_LIGHT = 1,
+ THEME_GTK3_VARIANT_PREFER_DARK = 2
+} ThemeGtk3Variant;
+
+void theme_gtk3_init (void);
+gboolean theme_gtk3_apply_current (GError **error);
+gboolean theme_gtk3_apply (const char *theme_id, ThemeGtk3Variant variant, GError **error);
+gboolean theme_gtk3_refresh (const char *theme_id, ThemeGtk3Variant variant, GError **error);
+ThemeGtk3Variant theme_gtk3_variant_for_theme (const char *theme_id);
+void theme_gtk3_invalidate_provider_cache (void);
+void theme_gtk3_disable (void);
+gboolean theme_gtk3_is_active (void);
+
+#endif
diff --git a/src/fe-gtk/theme/theme-manager.c b/src/fe-gtk/theme/theme-manager.c
index f7d318bd..e5886a49 100644
--- a/src/fe-gtk/theme/theme-manager.c
+++ b/src/fe-gtk/theme/theme-manager.c
@@ -2,18 +2,22 @@
#include "theme-manager.h"
#include
+#include
#include "theme-application.h"
#include "theme-policy.h"
#include "theme-runtime.h"
#include "theme-access.h"
#include "theme-css.h"
+#include "theme-gtk3.h"
#include "../gtkutil.h"
#include "../maingui.h"
#include "../setup.h"
#include "../../common/zoitechat.h"
#include "../../common/zoitechatc.h"
+void theme_runtime_reset_mode_colors (gboolean dark_mode);
+
typedef struct
{
guint id;
@@ -26,6 +30,38 @@ static GHashTable *theme_manager_listeners;
static guint theme_manager_next_listener_id = 1;
static guint theme_manager_setup_listener_id;
static const char theme_manager_window_destroy_handler_key[] = "theme-manager-window-destroy-handler";
+static const char theme_manager_window_csd_headerbar_key[] = "theme-manager-window-csd-headerbar";
+
+typedef struct
+{
+ gboolean initialized;
+ gboolean resolved_dark_preference;
+ char gtk3_theme_id[sizeof prefs.hex_gui_gtk3_theme];
+ int gtk3_variant;
+} ThemeManagerAutoRefreshCache;
+
+static ThemeManagerAutoRefreshCache theme_manager_auto_refresh_cache;
+
+static void theme_manager_apply_platform_window_theme (GtkWidget *window);
+
+static void
+theme_manager_apply_to_toplevel_windows (void)
+{
+ GList *toplevels;
+ GList *iter;
+
+ toplevels = gtk_window_list_toplevels ();
+ for (iter = toplevels; iter != NULL; iter = iter->next)
+ {
+ GtkWidget *window = GTK_WIDGET (iter->data);
+
+ if (!GTK_IS_WINDOW (window) || gtk_widget_get_mapped (window) == FALSE)
+ continue;
+
+ theme_manager_apply_platform_window_theme (window);
+ }
+ g_list_free (toplevels);
+}
static void
theme_listener_free (gpointer data)
@@ -76,27 +112,59 @@ theme_manager_synthesize_preference_reasons (const struct zoitechatprefs *old_pr
return reasons;
}
+static gboolean
+theme_manager_should_refresh_gtk3 (void)
+{
+ return prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
+}
+
static void
theme_manager_auto_dark_mode_changed (GtkSettings *settings, GParamSpec *pspec, gpointer data)
{
gboolean color_change = FALSE;
+ gboolean should_refresh_gtk3;
+ gboolean gtk3_refresh;
+ gboolean resolved_dark_preference;
static gboolean in_handler = FALSE;
(void) settings;
(void) pspec;
(void) data;
- if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO)
+ resolved_dark_preference = theme_policy_system_prefers_dark ();
+ gtk3_refresh = theme_manager_should_refresh_gtk3 ();
+ should_refresh_gtk3 = gtk3_refresh || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO;
+
+ if (theme_manager_auto_refresh_cache.initialized &&
+ theme_manager_auto_refresh_cache.resolved_dark_preference == resolved_dark_preference &&
+ 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)
+ return;
+
+ 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));
+
+ if (prefs.hex_gui_dark_mode != ZOITECHAT_DARK_MODE_AUTO && !gtk3_refresh)
return;
if (in_handler)
return;
in_handler = TRUE;
- fe_set_auto_dark_mode_state (theme_policy_system_prefers_dark ());
- theme_manager_commit_preferences (prefs.hex_gui_dark_mode, &color_change);
- if (color_change)
- theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE);
+ if (prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_AUTO)
+ {
+ fe_set_auto_dark_mode_state (resolved_dark_preference);
+ theme_manager_commit_preferences (prefs.hex_gui_dark_mode, &color_change);
+ if (color_change)
+ theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE);
+ }
+
+ if (should_refresh_gtk3)
+ theme_gtk3_apply_current (NULL);
in_handler = FALSE;
}
@@ -141,6 +209,7 @@ theme_manager_init (void)
fe_set_auto_dark_mode_state (theme_policy_system_prefers_dark ());
theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL);
+ theme_gtk3_init ();
zoitechat_set_theme_post_apply_callback (theme_manager_handle_theme_applied);
if (settings)
@@ -167,22 +236,37 @@ theme_manager_set_mode (unsigned int mode, gboolean *palette_changed)
void
theme_manager_set_token_color (unsigned int mode, ThemeSemanticToken token, const GdkRGBA *color, gboolean *palette_changed)
{
- gboolean dark;
gboolean changed = FALSE;
if (!color)
return;
- dark = theme_policy_is_dark_mode_active (mode);
- if (dark)
- theme_runtime_dark_set_color (token, color);
- else
- theme_runtime_user_set_color (token, color);
+ (void) mode;
+ theme_runtime_user_set_color (token, color);
- changed = theme_runtime_apply_mode (mode, NULL);
+ theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, &changed);
if (palette_changed)
*palette_changed = changed;
+ if (changed)
+ theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST);
+
+ theme_application_reload_input_style ();
+}
+
+void
+theme_manager_reset_mode_colors (unsigned int mode, gboolean *palette_changed)
+{
+ gboolean changed;
+
+ (void) mode;
+ theme_runtime_reset_mode_colors (FALSE);
+ theme_runtime_apply_mode (ZOITECHAT_DARK_MODE_LIGHT, &changed);
+ changed = TRUE;
+ if (palette_changed)
+ *palette_changed = changed;
+ theme_manager_dispatch_changed (THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST);
+
theme_application_reload_input_style ();
}
@@ -231,6 +315,13 @@ theme_manager_dispatch_changed (ThemeChangedReason reasons)
event.reasons = reasons;
+ if ((reasons & (THEME_CHANGED_REASON_MODE |
+ THEME_CHANGED_REASON_THEME_PACK |
+ THEME_CHANGED_REASON_WIDGET_STYLE)) != 0)
+ {
+ theme_manager_apply_to_toplevel_windows ();
+ }
+
if (!theme_manager_listeners)
return;
@@ -284,11 +375,99 @@ theme_listener_unregister (guint listener_id)
void
theme_manager_handle_theme_applied (void)
{
- theme_runtime_load ();
- theme_runtime_apply_mode (prefs.hex_gui_dark_mode, NULL);
+ theme_gtk3_invalidate_provider_cache ();
+ if (prefs.hex_gui_gtk3_theme[0])
+ theme_gtk3_refresh (prefs.hex_gui_gtk3_theme, (ThemeGtk3Variant) prefs.hex_gui_gtk3_variant, NULL);
+ theme_application_apply_mode (prefs.hex_gui_dark_mode, NULL);
theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK | THEME_CHANGED_REASON_PALETTE | THEME_CHANGED_REASON_WIDGET_STYLE | THEME_CHANGED_REASON_USERLIST | THEME_CHANGED_REASON_MODE);
}
+
+static gboolean
+theme_manager_is_kde_wayland (void)
+{
+ const char *wayland_display;
+ const char *desktop;
+ char *desktop_lower;
+ gboolean is_kde;
+
+ wayland_display = g_getenv ("WAYLAND_DISPLAY");
+ if (!wayland_display || !wayland_display[0])
+ return FALSE;
+
+ desktop = g_getenv ("XDG_CURRENT_DESKTOP");
+ if (!desktop || !desktop[0])
+ desktop = g_getenv ("XDG_SESSION_DESKTOP");
+ if (!desktop || !desktop[0])
+ return FALSE;
+
+ desktop_lower = g_ascii_strdown (desktop, -1);
+ is_kde = strstr (desktop_lower, "kde") != NULL || strstr (desktop_lower, "plasma") != NULL;
+ g_free (desktop_lower);
+ return is_kde;
+}
+
+static void
+theme_manager_apply_wayland_kde_csd (GtkWidget *window)
+{
+ GtkWindow *gtk_window;
+ GtkWidget *headerbar;
+ gboolean enable_csd;
+
+ if (!window || !GTK_IS_WINDOW (window))
+ return;
+
+ gtk_window = GTK_WINDOW (window);
+ enable_csd = theme_gtk3_is_active () && theme_manager_is_kde_wayland ();
+ headerbar = g_object_get_data (G_OBJECT (window), theme_manager_window_csd_headerbar_key);
+
+ if (enable_csd)
+ {
+ if (!headerbar)
+ {
+ GtkWidget *icon_image;
+ GdkPixbuf *icon_pixbuf;
+
+ if (gtk_widget_get_realized (window))
+ return;
+
+ headerbar = gtk_header_bar_new ();
+ gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (headerbar), TRUE);
+ gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (headerbar), "menu:minimize,maximize,close");
+ icon_pixbuf = gdk_pixbuf_new_from_resource_at_scale ("/icons/zoitechat.png", 32, 32, TRUE, NULL);
+ icon_image = icon_pixbuf ? gtk_image_new_from_pixbuf (icon_pixbuf) : gtk_image_new_from_resource ("/icons/zoitechat.png");
+ if (icon_pixbuf)
+ g_object_unref (icon_pixbuf);
+ gtk_header_bar_pack_start (GTK_HEADER_BAR (headerbar), icon_image);
+ gtk_widget_show (icon_image);
+ gtk_window_set_titlebar (gtk_window, headerbar);
+ g_object_set_data (G_OBJECT (window), theme_manager_window_csd_headerbar_key, headerbar);
+ }
+ gtk_header_bar_set_title (GTK_HEADER_BAR (headerbar), gtk_window_get_title (gtk_window));
+ gtk_widget_show (headerbar);
+ {
+ GdkScreen *screen = gdk_screen_get_default ();
+ if (screen)
+ gtk_style_context_reset_widgets (screen);
+ }
+ return;
+ }
+
+ if (headerbar)
+ {
+ if (gtk_widget_get_realized (window))
+ return;
+ gtk_window_set_titlebar (gtk_window, NULL);
+ g_object_set_data (G_OBJECT (window), theme_manager_window_csd_headerbar_key, NULL);
+ }
+
+ {
+ GdkScreen *screen = gdk_screen_get_default ();
+ if (screen)
+ gtk_style_context_reset_widgets (screen);
+ }
+}
+
static void
theme_manager_apply_platform_window_theme (GtkWidget *window)
{
@@ -300,7 +479,14 @@ theme_manager_apply_platform_window_theme (GtkWidget *window)
return;
context = gtk_widget_get_style_context (window);
- dark = theme_runtime_is_dark_active ();
+ if (theme_gtk3_is_active ())
+ {
+ 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
+ dark = theme_runtime_is_dark_active ();
if (context)
{
gtk_style_context_remove_class (context, "zoitechat-dark");
@@ -309,7 +495,7 @@ theme_manager_apply_platform_window_theme (GtkWidget *window)
}
fe_win32_apply_native_titlebar (window, dark);
#else
- (void) window;
+ theme_manager_apply_wayland_kde_csd (window);
#endif
}
@@ -379,7 +565,7 @@ theme_manager_apply_entry_palette (GtkWidget *widget, const PangoFontDescription
if (!widget || !font_desc)
return;
- theme_get_widget_style_values (&style_values);
+ theme_get_widget_style_values_for_widget (widget, &style_values);
gtkutil_apply_palette (widget, &style_values.background, &style_values.foreground, font_desc);
}
@@ -387,11 +573,10 @@ ThemePaletteBehavior
theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_desc)
{
ThemePaletteBehavior behavior;
- gboolean dark_mode_active = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
behavior.font_desc = font_desc;
- behavior.apply_background = prefs.hex_gui_ulist_style || dark_mode_active;
- behavior.apply_foreground = dark_mode_active;
+ behavior.apply_background = TRUE;
+ behavior.apply_foreground = TRUE;
return behavior;
}
@@ -400,11 +585,10 @@ ThemePaletteBehavior
theme_manager_get_channel_tree_palette_behavior (const PangoFontDescription *font_desc)
{
ThemePaletteBehavior behavior;
- gboolean dark_mode_active = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
behavior.font_desc = font_desc;
- behavior.apply_background = dark_mode_active || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_LIGHT;
- behavior.apply_foreground = dark_mode_active || prefs.hex_gui_dark_mode == ZOITECHAT_DARK_MODE_LIGHT;
+ behavior.apply_background = TRUE;
+ behavior.apply_foreground = TRUE;
return behavior;
}
@@ -431,7 +615,7 @@ theme_manager_apply_userlist_style (GtkWidget *widget, ThemePaletteBehavior beha
if (!widget)
return;
- theme_get_widget_style_values (&style_values);
+ theme_get_widget_style_values_for_widget (widget, &style_values);
if (behavior.apply_background)
background = &style_values.background;
if (behavior.apply_foreground)
diff --git a/src/fe-gtk/theme/theme-manager.h b/src/fe-gtk/theme/theme-manager.h
index 0d94b614..7f700054 100644
--- a/src/fe-gtk/theme/theme-manager.h
+++ b/src/fe-gtk/theme/theme-manager.h
@@ -41,6 +41,7 @@ void theme_manager_init (void);
gboolean theme_manager_apply_mode (unsigned int mode, gboolean *palette_changed);
void theme_manager_set_mode (unsigned int mode, gboolean *palette_changed);
void theme_manager_set_token_color (unsigned int mode, ThemeSemanticToken token, const GdkRGBA *color, gboolean *palette_changed);
+void theme_manager_reset_mode_colors (unsigned int mode, gboolean *palette_changed);
void theme_manager_commit_preferences (unsigned int old_mode, gboolean *color_change);
void theme_manager_save_preferences (void);
gboolean theme_changed_event_has_reason (const ThemeChangedEvent *event, ThemeChangedReason reason);
diff --git a/src/fe-gtk/theme/theme-preferences.c b/src/fe-gtk/theme/theme-preferences.c
index 3d508505..8270404a 100644
--- a/src/fe-gtk/theme/theme-preferences.c
+++ b/src/fe-gtk/theme/theme-preferences.c
@@ -9,19 +9,19 @@
#include "../../common/util.h"
#include "../../common/cfgfiles.h"
#include "../../common/zoitechat.h"
-#include "../../common/theme-service.h"
+#include "../../common/gtk3-theme-service.h"
#include "../../common/zoitechatc.h"
-#include "theme-css.h"
+#include "theme-gtk3.h"
#include "theme-manager.h"
#include "theme-preferences.h"
typedef struct
{
- GtkWidget *combo;
- GtkWidget *apply_button;
- GtkWidget *status_label;
GtkWindow *parent;
- gboolean *color_change_flag;
+ GtkWidget *gtk3_combo;
+ GtkWidget *gtk3_remove;
+ gboolean gtk3_populating;
+ struct zoitechatprefs *setup_prefs;
} theme_preferences_ui;
typedef struct
@@ -29,12 +29,238 @@ typedef struct
GtkWidget *button;
ThemeSemanticToken token;
gboolean *color_change_flag;
+ gpointer manager_ui;
} theme_color_dialog_data;
typedef struct
{
- struct zoitechatprefs *setup_prefs;
-} theme_preferences_dark_mode_data;
+ GtkWidget *row;
+ GtkWidget *button;
+ GtkWidget *entry;
+ GtkWidget *preview;
+ ThemeSemanticToken token;
+ char *search_text;
+ gboolean *color_change_flag;
+ GtkWindow *parent;
+ gpointer manager_ui;
+} theme_color_manager_row;
+
+typedef struct
+{
+ theme_color_manager_row *row;
+ GdkRGBA original;
+ gboolean has_original;
+} theme_manager_live_picker_data;
+
+typedef struct
+{
+ GPtrArray *rows;
+ GtkWidget *search_entry;
+ gboolean *color_change_flag;
+ GtkWidget *preview_window;
+ GtkWidget *preview_chat;
+ GtkWidget *preview_selected;
+ GtkWidget *preview_marker;
+ GtkWidget *preview_tab_new_data;
+ GtkWidget *preview_tab_new_message;
+ GtkWidget *preview_tab_highlight;
+ GtkWidget *preview_tab_away;
+ GtkWidget *preview_spell;
+} theme_color_manager_ui;
+
+#define COLOR_MANAGER_RESPONSE_RESET 1
+
+static void
+theme_preferences_manager_row_free (gpointer data)
+{
+ theme_color_manager_row *row = data;
+
+ if (!row)
+ return;
+
+ g_free (row->search_text);
+ g_free (row);
+}
+
+static void
+theme_preferences_manager_ui_free (gpointer data)
+{
+ theme_color_manager_ui *ui = data;
+
+ if (!ui)
+ return;
+
+ if (ui->rows)
+ g_ptr_array_unref (ui->rows);
+ g_free (ui);
+}
+
+static void
+theme_preferences_manager_update_preview (theme_color_manager_ui *ui)
+{
+ GdkRGBA text_fg;
+ GdkRGBA text_bg;
+ GdkRGBA sel_fg;
+ GdkRGBA sel_bg;
+ GdkRGBA marker;
+ GdkRGBA tab_new_data;
+ GdkRGBA tab_new_message;
+ GdkRGBA tab_highlight;
+ GdkRGBA tab_away;
+ GdkRGBA spell;
+ GtkWidget *label;
+
+ if (!ui)
+ return;
+
+ if (!theme_get_color (THEME_TOKEN_TEXT_FOREGROUND, &text_fg)
+ || !theme_get_color (THEME_TOKEN_TEXT_BACKGROUND, &text_bg)
+ || !theme_get_color (THEME_TOKEN_SELECTION_FOREGROUND, &sel_fg)
+ || !theme_get_color (THEME_TOKEN_SELECTION_BACKGROUND, &sel_bg)
+ || !theme_get_color (THEME_TOKEN_MARKER, &marker)
+ || !theme_get_color (THEME_TOKEN_TAB_NEW_DATA, &tab_new_data)
+ || !theme_get_color (THEME_TOKEN_TAB_NEW_MESSAGE, &tab_new_message)
+ || !theme_get_color (THEME_TOKEN_TAB_HIGHLIGHT, &tab_highlight)
+ || !theme_get_color (THEME_TOKEN_TAB_AWAY, &tab_away)
+ || !theme_get_color (THEME_TOKEN_SPELL, &spell))
+ return;
+
+ gtkutil_apply_palette (ui->preview_window, &text_bg, &text_fg, NULL);
+ gtkutil_apply_palette (ui->preview_chat, &text_bg, &text_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_chat), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &text_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_selected, &sel_bg, &sel_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_selected), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &sel_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_marker, &marker, &text_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_marker), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &text_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_tab_new_data, &tab_new_data, &text_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_tab_new_data), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &text_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_tab_new_message, &tab_new_message, &text_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_tab_new_message), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &text_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_tab_highlight, &tab_highlight, &text_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_tab_highlight), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &text_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_tab_away, &tab_away, &text_fg, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_tab_away), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &text_fg, NULL);
+
+ gtkutil_apply_palette (ui->preview_spell, &text_bg, &spell, NULL);
+ label = g_object_get_data (G_OBJECT (ui->preview_spell), "zoitechat-preview-label");
+ if (GTK_IS_WIDGET (label))
+ gtkutil_apply_palette (label, NULL, &spell, NULL);
+}
+
+static GtkWidget *
+theme_preferences_manager_preview_item_new (const char *text)
+{
+ GtkWidget *box;
+ GtkWidget *label;
+
+ box = gtk_event_box_new ();
+ gtk_event_box_set_visible_window (GTK_EVENT_BOX (box), TRUE);
+ gtk_container_set_border_width (GTK_CONTAINER (box), 3);
+
+ label = gtk_label_new (text);
+ gtk_widget_set_halign (label, GTK_ALIGN_START);
+ gtk_container_add (GTK_CONTAINER (box), label);
+ g_object_set_data (G_OBJECT (box), "zoitechat-preview-label", label);
+
+ return box;
+}
+
+static GtkWidget *
+theme_preferences_manager_create_preview (theme_color_manager_ui *ui)
+{
+ GtkWidget *frame;
+ GtkWidget *vbox;
+ GtkWidget *header;
+ GtkWidget *chat_box;
+ GtkWidget *tabs_box;
+ GtkWidget *label;
+
+ frame = gtk_frame_new (_("Live preview"));
+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_set_border_width (GTK_CONTAINER (vbox), 8);
+ gtk_container_add (GTK_CONTAINER (frame), vbox);
+
+ ui->preview_window = gtk_event_box_new ();
+ gtk_event_box_set_visible_window (GTK_EVENT_BOX (ui->preview_window), TRUE);
+ gtk_container_set_border_width (GTK_CONTAINER (ui->preview_window), 8);
+ gtk_box_pack_start (GTK_BOX (vbox), ui->preview_window, TRUE, TRUE, 0);
+
+ chat_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_add (GTK_CONTAINER (ui->preview_window), chat_box);
+
+ header = gtk_label_new (_("#zoitechat-preview"));
+ gtk_widget_set_halign (header, GTK_ALIGN_START);
+ gtk_box_pack_start (GTK_BOX (chat_box), header, FALSE, FALSE, 0);
+
+ ui->preview_chat = theme_preferences_manager_preview_item_new (_(" Example chat message"));
+ gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_chat, FALSE, FALSE, 0);
+
+ ui->preview_selected = theme_preferences_manager_preview_item_new (_("Selected text example"));
+ gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_selected, FALSE, FALSE, 0);
+
+ ui->preview_marker = theme_preferences_manager_preview_item_new (_("Marker line"));
+ gtk_widget_set_hexpand (ui->preview_marker, TRUE);
+ gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_marker, FALSE, FALSE, 0);
+
+ ui->preview_spell = theme_preferences_manager_preview_item_new (_("mispelled wrd"));
+ gtk_box_pack_start (GTK_BOX (chat_box), ui->preview_spell, FALSE, FALSE, 0);
+
+ label = gtk_label_new (_("Tab states"));
+ gtk_widget_set_halign (label, GTK_ALIGN_START);
+ gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
+
+ tabs_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_box_pack_start (GTK_BOX (vbox), tabs_box, FALSE, FALSE, 0);
+
+ ui->preview_tab_new_data = theme_preferences_manager_preview_item_new (_("New data"));
+ gtk_widget_set_hexpand (ui->preview_tab_new_data, TRUE);
+ gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_new_data, TRUE, TRUE, 0);
+
+ ui->preview_tab_new_message = theme_preferences_manager_preview_item_new (_("New message"));
+ gtk_widget_set_hexpand (ui->preview_tab_new_message, TRUE);
+ gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_new_message, TRUE, TRUE, 0);
+
+ ui->preview_tab_highlight = theme_preferences_manager_preview_item_new (_("Highlight"));
+ gtk_widget_set_hexpand (ui->preview_tab_highlight, TRUE);
+ gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_highlight, TRUE, TRUE, 0);
+
+ ui->preview_tab_away = theme_preferences_manager_preview_item_new (_("Away"));
+ gtk_widget_set_hexpand (ui->preview_tab_away, TRUE);
+ gtk_box_pack_start (GTK_BOX (tabs_box), ui->preview_tab_away, TRUE, TRUE, 0);
+
+ theme_preferences_manager_update_preview (ui);
+
+ return frame;
+}
+
+enum
+{
+ GTK3_THEME_COL_ID = 0,
+ GTK3_THEME_COL_LABEL,
+ GTK3_THEME_COL_SOURCE,
+ GTK3_THEME_COL_THUMBNAIL,
+ GTK3_THEME_COL_COUNT
+};
#define LABEL_INDENT 12
@@ -49,6 +275,7 @@ theme_preferences_show_message (theme_preferences_ui *ui, GtkMessageType message
GTK_BUTTONS_OK,
"%s",
primary);
+ theme_manager_attach_window (dialog);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
}
@@ -78,13 +305,14 @@ theme_preferences_color_response_cb (GtkDialog *dialog, gint response_id, gpoint
gboolean changed = FALSE;
gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (dialog), &rgba);
- theme_manager_set_token_color (prefs.hex_gui_dark_mode,
+ theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT,
data->token,
&rgba,
&changed);
if (data->color_change_flag)
*data->color_change_flag = *data->color_change_flag || changed;
theme_preferences_color_button_apply (data->button, &rgba);
+ theme_preferences_manager_update_preview ((theme_color_manager_ui *) data->manager_ui);
}
gtk_widget_destroy (GTK_WIDGET (dialog));
@@ -104,6 +332,7 @@ theme_preferences_color_cb (GtkWidget *button, gpointer userdata)
if (!theme_get_color (token, &rgba))
return;
dialog = gtk_color_chooser_dialog_new (_("Select color"), GTK_WINDOW (userdata));
+ theme_manager_attach_window (dialog);
gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (dialog), &rgba);
gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
@@ -111,10 +340,405 @@ theme_preferences_color_cb (GtkWidget *button, gpointer userdata)
data->button = button;
data->token = token;
data->color_change_flag = g_object_get_data (G_OBJECT (button), "zoitechat-theme-color-change");
+ data->manager_ui = g_object_get_data (G_OBJECT (button), "zoitechat-theme-color-manager-ui");
g_signal_connect (dialog, "response", G_CALLBACK (theme_preferences_color_response_cb), data);
gtk_widget_show (dialog);
}
+static char *
+theme_preferences_format_hex (const GdkRGBA *color)
+{
+ return g_strdup_printf ("#%02X%02X%02X",
+ (guint) CLAMP (color->red * 255.0 + 0.5, 0.0, 255.0),
+ (guint) CLAMP (color->green * 255.0 + 0.5, 0.0, 255.0),
+ (guint) CLAMP (color->blue * 255.0 + 0.5, 0.0, 255.0));
+}
+
+static char *
+theme_preferences_token_display_name (ThemeSemanticToken token)
+{
+ if (token >= THEME_TOKEN_MIRC_0 && token <= THEME_TOKEN_MIRC_15)
+ return g_strdup_printf (_("mIRC color %d"), token - THEME_TOKEN_MIRC_0);
+
+ if (token >= THEME_TOKEN_MIRC_16 && token <= THEME_TOKEN_MIRC_31)
+ return g_strdup_printf (_("Local color %d"), token - THEME_TOKEN_MIRC_0);
+
+ switch (token)
+ {
+ case THEME_TOKEN_SELECTION_FOREGROUND:
+ return g_strdup (_("Selected text foreground"));
+ case THEME_TOKEN_SELECTION_BACKGROUND:
+ return g_strdup (_("Selected text background"));
+ case THEME_TOKEN_TEXT_FOREGROUND:
+ return g_strdup (_("Text foreground"));
+ case THEME_TOKEN_TEXT_BACKGROUND:
+ return g_strdup (_("Text background"));
+ case THEME_TOKEN_MARKER:
+ return g_strdup (_("Marker line"));
+ case THEME_TOKEN_TAB_NEW_DATA:
+ return g_strdup (_("Tab: new data"));
+ case THEME_TOKEN_TAB_HIGHLIGHT:
+ return g_strdup (_("Tab: highlight"));
+ case THEME_TOKEN_TAB_NEW_MESSAGE:
+ return g_strdup (_("Tab: new message"));
+ case THEME_TOKEN_TAB_AWAY:
+ return g_strdup (_("Tab: away"));
+ case THEME_TOKEN_SPELL:
+ return g_strdup (_("Spell checker"));
+ default:
+ return g_strdup (_("Unknown color"));
+ }
+}
+
+static void
+theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA *rgba)
+{
+ char *hex;
+
+ theme_preferences_color_button_apply (row->button, rgba);
+ gtkutil_apply_palette (row->preview, rgba, NULL, NULL);
+ hex = theme_preferences_format_hex (rgba);
+ gtk_entry_set_text (GTK_ENTRY (row->entry), hex);
+ g_free (hex);
+}
+
+static void
+theme_preferences_manager_row_commit (theme_color_manager_row *row, const GdkRGBA *rgba)
+{
+ gboolean changed = FALSE;
+
+ theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT,
+ row->token,
+ rgba,
+ &changed);
+ if (row->color_change_flag)
+ *row->color_change_flag = *row->color_change_flag || changed;
+ theme_preferences_manager_row_apply (row, rgba);
+ theme_preferences_manager_update_preview ((theme_color_manager_ui *) row->manager_ui);
+}
+
+static void
+theme_preferences_manager_entry_commit (theme_color_manager_row *row)
+{
+ GdkRGBA rgba;
+ const char *text = gtk_entry_get_text (GTK_ENTRY (row->entry));
+
+ if (!gdk_rgba_parse (&rgba, text))
+ {
+ if (theme_get_color (row->token, &rgba))
+ theme_preferences_manager_row_apply (row, &rgba);
+ return;
+ }
+
+ theme_preferences_manager_row_commit (row, &rgba);
+}
+
+static void
+theme_preferences_manager_entry_activate_cb (GtkEntry *entry, gpointer user_data)
+{
+ (void) entry;
+ theme_preferences_manager_entry_commit ((theme_color_manager_row *) user_data);
+}
+
+static gboolean
+theme_preferences_manager_entry_focus_out_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+ (void) widget;
+ (void) event;
+ theme_preferences_manager_entry_commit ((theme_color_manager_row *) user_data);
+ return FALSE;
+}
+
+static void
+theme_preferences_manager_picker_notify_rgba_cb (GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+ theme_manager_live_picker_data *data = user_data;
+ GdkRGBA rgba;
+
+ (void) pspec;
+ if (!data || !data->row)
+ return;
+
+ gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (object), &rgba);
+ theme_preferences_manager_row_commit (data->row, &rgba);
+}
+
+static void
+theme_preferences_manager_picker_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
+{
+ theme_manager_live_picker_data *data = user_data;
+
+ if (data && data->row && data->has_original && response_id != GTK_RESPONSE_OK)
+ theme_preferences_manager_row_commit (data->row, &data->original);
+
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+ g_free (data);
+}
+
+static void
+theme_preferences_manager_pick_cb (GtkWidget *button, gpointer user_data)
+{
+ theme_color_manager_row *row = user_data;
+ GtkWidget *dialog;
+ GdkRGBA rgba;
+ theme_manager_live_picker_data *data;
+
+ if (!theme_get_color (row->token, &rgba))
+ return;
+
+ dialog = gtk_color_chooser_dialog_new (_("Select color"), row->parent);
+ theme_manager_attach_window (dialog);
+ gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (dialog), &rgba);
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+
+ data = g_new0 (theme_manager_live_picker_data, 1);
+ data->row = row;
+ data->original = rgba;
+ data->has_original = TRUE;
+
+ g_signal_connect (G_OBJECT (dialog), "notify::rgba",
+ G_CALLBACK (theme_preferences_manager_picker_notify_rgba_cb), data);
+ g_signal_connect (G_OBJECT (dialog), "response",
+ G_CALLBACK (theme_preferences_manager_picker_response_cb), data);
+ gtk_widget_show (dialog);
+ (void) button;
+}
+
+static gboolean
+theme_preferences_manager_row_matches (theme_color_manager_row *row, const char *needle)
+{
+ if (!needle || !needle[0])
+ return TRUE;
+
+ return strstr (row->search_text, needle) != NULL;
+}
+
+static void
+theme_preferences_manager_search_changed_cb (GtkEditable *editable, gpointer user_data)
+{
+ theme_color_manager_ui *ui = user_data;
+ char *needle_lower;
+ size_t i;
+
+ needle_lower = g_utf8_strdown (gtk_entry_get_text (GTK_ENTRY (editable)), -1);
+ for (i = 0; i < ui->rows->len; i++)
+ {
+ theme_color_manager_row *row = g_ptr_array_index (ui->rows, i);
+ gtk_widget_set_visible (row->row, theme_preferences_manager_row_matches (row, needle_lower));
+ }
+ g_free (needle_lower);
+}
+
+static void
+theme_preferences_manager_refresh_rows (theme_color_manager_ui *ui)
+{
+ size_t i;
+
+ if (!ui || !ui->rows)
+ return;
+
+ for (i = 0; i < ui->rows->len; i++)
+ {
+ theme_color_manager_row *row = g_ptr_array_index (ui->rows, i);
+ GdkRGBA rgba;
+
+ if (theme_get_color (row->token, &rgba))
+ theme_preferences_manager_row_apply (row, &rgba);
+ }
+
+ theme_preferences_manager_update_preview (ui);
+}
+
+static void
+theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
+{
+ theme_color_manager_ui *ui = user_data;
+
+ if (response_id != COLOR_MANAGER_RESPONSE_RESET)
+ return;
+
+ {
+ gboolean changed = FALSE;
+
+ theme_manager_reset_mode_colors (ZOITECHAT_DARK_MODE_LIGHT, &changed);
+ if (ui->color_change_flag)
+ *ui->color_change_flag = *ui->color_change_flag || changed;
+ }
+
+ theme_preferences_manager_refresh_rows (ui);
+ g_signal_stop_emission_by_name (dialog, "response");
+}
+
+static GtkWidget *
+theme_preferences_create_color_manager_dialog (GtkWindow *parent, gboolean *color_change_flag)
+{
+ GtkWidget *dialog;
+ GtkWidget *content;
+ GtkWidget *vbox;
+ GtkWidget *content_hbox;
+ GtkWidget *left_box;
+ GtkWidget *search;
+ GtkWidget *scroller;
+ GtkWidget *list;
+ GtkWidget *preview_frame;
+ theme_color_manager_ui *ui;
+ ThemeSemanticToken token;
+
+ dialog = gtk_dialog_new_with_buttons (_("Manage client colors"),
+ parent,
+ GTK_DIALOG_MODAL,
+ _("_Reset to GTK3 defaults"),
+ COLOR_MANAGER_RESPONSE_RESET,
+ _("_Close"),
+ GTK_RESPONSE_CLOSE,
+ NULL);
+ theme_manager_attach_window (dialog);
+ gtk_window_set_default_size (GTK_WINDOW (dialog), 760, 560);
+
+ ui = g_new0 (theme_color_manager_ui, 1);
+ ui->rows = g_ptr_array_new_with_free_func (theme_preferences_manager_row_free);
+ ui->color_change_flag = color_change_flag;
+ g_object_set_data_full (G_OBJECT (dialog), "zoitechat-theme-color-manager", ui, theme_preferences_manager_ui_free);
+ g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (theme_preferences_manager_dialog_response_cb), ui);
+
+ content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ gtk_container_set_border_width (GTK_CONTAINER (vbox), 8);
+ gtk_widget_set_hexpand (vbox, TRUE);
+ gtk_widget_set_vexpand (vbox, TRUE);
+ gtk_box_pack_start (GTK_BOX (content), vbox, TRUE, TRUE, 0);
+
+ content_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_widget_set_hexpand (content_hbox, TRUE);
+ gtk_widget_set_vexpand (content_hbox, TRUE);
+ gtk_box_pack_start (GTK_BOX (vbox), content_hbox, TRUE, TRUE, 0);
+
+ left_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+ gtk_widget_set_hexpand (left_box, TRUE);
+ gtk_widget_set_vexpand (left_box, TRUE);
+ gtk_box_pack_start (GTK_BOX (content_hbox), left_box, TRUE, TRUE, 0);
+
+ search = gtk_search_entry_new ();
+ gtk_entry_set_placeholder_text (GTK_ENTRY (search), _("Search colors by name"));
+ gtk_box_pack_start (GTK_BOX (left_box), search, FALSE, FALSE, 0);
+ ui->search_entry = search;
+
+ scroller = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_set_hexpand (scroller, TRUE);
+ gtk_widget_set_vexpand (scroller, TRUE);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ gtk_box_pack_start (GTK_BOX (left_box), scroller, TRUE, TRUE, 0);
+
+ list = gtk_list_box_new ();
+ gtk_widget_set_hexpand (list, TRUE);
+ gtk_widget_set_vexpand (list, TRUE);
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (list), GTK_SELECTION_NONE);
+ gtk_container_add (GTK_CONTAINER (scroller), list);
+
+ preview_frame = theme_preferences_manager_create_preview (ui);
+ gtk_widget_set_size_request (preview_frame, 300, -1);
+ gtk_widget_set_hexpand (preview_frame, FALSE);
+ gtk_widget_set_vexpand (preview_frame, TRUE);
+ gtk_box_pack_start (GTK_BOX (content_hbox), preview_frame, FALSE, TRUE, 0);
+
+ for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
+ {
+ theme_color_manager_row *row;
+ GtkWidget *list_row;
+ GtkWidget *hbox;
+ GtkWidget *name;
+ GtkWidget *preview;
+ GtkWidget *button;
+ GtkWidget *entry;
+ GdkRGBA rgba;
+ char *display;
+ char *search_text;
+ char *token_code;
+
+ list_row = gtk_list_box_row_new ();
+ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_container_set_border_width (GTK_CONTAINER (hbox), 4);
+ gtk_container_add (GTK_CONTAINER (list_row), hbox);
+
+ display = theme_preferences_token_display_name (token);
+ name = gtk_label_new (display);
+ gtk_widget_set_halign (name, GTK_ALIGN_START);
+ gtk_widget_set_hexpand (name, TRUE);
+ gtk_box_pack_start (GTK_BOX (hbox), name, TRUE, TRUE, 0);
+
+ preview = gtk_label_new (_("Preview"));
+ gtk_widget_set_size_request (preview, 90, -1);
+ gtk_widget_set_halign (preview, GTK_ALIGN_CENTER);
+ gtk_box_pack_start (GTK_BOX (hbox), preview, FALSE, FALSE, 0);
+
+ button = gtk_button_new_with_label (_("Choose…"));
+ gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 0);
+
+ entry = gtk_entry_new ();
+ gtk_entry_set_width_chars (GTK_ENTRY (entry), 9);
+ gtk_entry_set_max_length (GTK_ENTRY (entry), 9);
+ gtk_box_pack_start (GTK_BOX (hbox), entry, FALSE, FALSE, 0);
+
+ row = g_new0 (theme_color_manager_row, 1);
+ row->row = list_row;
+ row->button = button;
+ row->entry = entry;
+ row->preview = preview;
+ row->token = token;
+ row->color_change_flag = color_change_flag;
+ row->parent = GTK_WINDOW (dialog);
+ row->manager_ui = ui;
+
+ token_code = g_strdup_printf ("token_%d", token);
+ search_text = g_strconcat (display, " ", token_code, NULL);
+ row->search_text = g_utf8_strdown (search_text, -1);
+ g_free (token_code);
+ g_free (search_text);
+
+ if (theme_get_color (token, &rgba))
+ theme_preferences_manager_row_apply (row, &rgba);
+
+ g_signal_connect (G_OBJECT (button), "clicked",
+ G_CALLBACK (theme_preferences_manager_pick_cb), row);
+ g_object_set_data (G_OBJECT (button), "zoitechat-theme-color-manager-ui", ui);
+ g_signal_connect (G_OBJECT (entry), "activate",
+ G_CALLBACK (theme_preferences_manager_entry_activate_cb), row);
+ g_signal_connect (G_OBJECT (entry), "focus-out-event",
+ G_CALLBACK (theme_preferences_manager_entry_focus_out_cb), row);
+
+ gtk_container_add (GTK_CONTAINER (list), list_row);
+ g_ptr_array_add (ui->rows, row);
+ g_free (display);
+ }
+
+ g_signal_connect (G_OBJECT (search), "changed",
+ G_CALLBACK (theme_preferences_manager_search_changed_cb), ui);
+
+ theme_preferences_manager_update_preview (ui);
+
+ gtk_widget_show_all (dialog);
+ return dialog;
+}
+
+static void
+theme_preferences_manage_colors_cb (GtkWidget *button, gpointer user_data)
+{
+ gboolean *color_change_flag = user_data;
+ gboolean old_changed = FALSE;
+ GtkWidget *dialog;
+
+ if (color_change_flag)
+ old_changed = *color_change_flag;
+
+ dialog = theme_preferences_create_color_manager_dialog (GTK_WINDOW (gtk_widget_get_toplevel (button)),
+ color_change_flag);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+
+ if (color_change_flag && *color_change_flag != old_changed)
+ theme_manager_save_preferences ();
+}
+
static void
theme_preferences_create_color_button (GtkWidget *table,
ThemeSemanticToken token,
@@ -235,60 +859,6 @@ theme_preferences_create_strip_toggle (GtkWidget *tab,
gtk_grid_attach (GTK_GRID (tab), toggle, 2, row, 1, 1);
}
-static void
-theme_preferences_dark_mode_changed_cb (GtkComboBox *combo, gpointer user_data)
-{
- theme_preferences_dark_mode_data *data = user_data;
-
- data->setup_prefs->hex_gui_dark_mode = gtk_combo_box_get_active (combo);
-}
-
-static void
-theme_preferences_create_dark_mode_menu (GtkWidget *tab,
- int row,
- struct zoitechatprefs *setup_prefs)
-{
- static const char *const dark_mode_modes[] =
- {
- N_("Auto (system)"),
- N_("Dark"),
- N_("Light"),
- NULL
- };
- GtkWidget *label;
- GtkWidget *combo;
- GtkWidget *box;
- theme_preferences_dark_mode_data *data;
- int i;
-
- label = gtk_label_new (_("Dark mode:"));
- gtk_widget_set_halign (label, GTK_ALIGN_START);
- gtk_widget_set_valign (label, GTK_ALIGN_CENTER);
- gtk_widget_set_margin_start (label, LABEL_INDENT);
- gtk_widget_set_tooltip_text (label,
- _("Choose how ZoiteChat selects its color palette for the chat buffer, channel list, and user list.\n"
- "This includes message colors, selection colors, and interface highlights.\n"));
- gtk_grid_attach (GTK_GRID (tab), label, 2, row, 1, 1);
-
- combo = gtk_combo_box_text_new ();
- for (i = 0; dark_mode_modes[i] != NULL; i++)
- gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), _(dark_mode_modes[i]));
- gtk_combo_box_set_active (GTK_COMBO_BOX (combo), setup_prefs->hex_gui_dark_mode);
- gtk_widget_set_tooltip_text (combo,
- _("Choose how ZoiteChat selects its color palette for the chat buffer, channel list, and user list.\n"
- "This includes message colors, selection colors, and interface highlights.\n"));
-
- box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_box_pack_start (GTK_BOX (box), combo, FALSE, FALSE, 0);
- gtk_grid_attach (GTK_GRID (tab), box, 3, row, 1, 1);
-
- data = g_new0 (theme_preferences_dark_mode_data, 1);
- data->setup_prefs = setup_prefs;
- g_signal_connect (G_OBJECT (combo), "changed",
- G_CALLBACK (theme_preferences_dark_mode_changed_cb), data);
- g_object_set_data_full (G_OBJECT (combo), "zoitechat-dark-mode-data", data, g_free);
-}
-
GtkWidget *
theme_preferences_create_color_page (GtkWindow *parent,
struct zoitechatprefs *setup_prefs,
@@ -297,6 +867,7 @@ theme_preferences_create_color_page (GtkWindow *parent,
GtkWidget *tab;
GtkWidget *box;
GtkWidget *label;
+ GtkWidget *manage_colors_button;
int i;
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
@@ -362,56 +933,31 @@ theme_preferences_create_color_page (GtkWindow *parent,
parent, color_change_flag);
theme_preferences_create_other_color_r (tab, _("Spell checker:"), THEME_TOKEN_SPELL, 11,
parent, color_change_flag);
- theme_preferences_create_dark_mode_menu (tab, 13, setup_prefs);
-
theme_preferences_create_header (tab, 15, N_("Color Stripping"));
theme_preferences_create_strip_toggle (tab, 16, _("Messages"), &setup_prefs->hex_text_stripcolor_msg);
theme_preferences_create_strip_toggle (tab, 17, _("Scrollback"), &setup_prefs->hex_text_stripcolor_replay);
theme_preferences_create_strip_toggle (tab, 18, _("Topic"), &setup_prefs->hex_text_stripcolor_topic);
+ manage_colors_button = gtk_button_new_with_label (_("Manage all client colors…"));
+ gtk_widget_set_halign (manage_colors_button, GTK_ALIGN_START);
+ gtk_widget_set_margin_start (manage_colors_button, LABEL_INDENT);
+ gtk_widget_set_margin_top (manage_colors_button, 10);
+ gtk_box_pack_start (GTK_BOX (box), manage_colors_button, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (manage_colors_button), "clicked",
+ G_CALLBACK (theme_preferences_manage_colors_cb), color_change_flag);
+
return box;
}
static void
-theme_preferences_populate (theme_preferences_ui *ui)
-{
- GStrv themes;
- int count = 0;
- guint i;
-
- gtk_combo_box_text_remove_all (GTK_COMBO_BOX_TEXT (ui->combo));
-
- themes = zoitechat_theme_service_discover_themes ();
- for (i = 0; themes[i] != NULL; i++)
- {
- gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (ui->combo), themes[i]);
- count++;
- }
- g_strfreev (themes);
-
- gtk_widget_set_sensitive (ui->apply_button, count > 0);
- gtk_label_set_text (GTK_LABEL (ui->status_label),
- count > 0 ? _("Select a theme to apply.") : _("No themes found."));
-}
-
-static void
-theme_preferences_refresh_cb (GtkWidget *button, gpointer user_data)
-{
- theme_preferences_ui *ui = user_data;
-
- (void)button;
- theme_preferences_populate (ui);
-}
-
-static void
-theme_preferences_open_folder_cb (GtkWidget *button, gpointer user_data)
+theme_preferences_open_gtk3_folder_cb (GtkWidget *button, gpointer user_data)
{
theme_preferences_ui *ui = user_data;
GAppInfo *handler;
char *themes_dir;
(void)button;
- themes_dir = zoitechat_theme_service_get_themes_dir ();
+ themes_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
g_mkdir_with_parents (themes_dir, 0700);
handler = g_app_info_get_default_for_uri_scheme ("file");
@@ -429,149 +975,399 @@ theme_preferences_open_folder_cb (GtkWidget *button, gpointer user_data)
g_free (themes_dir);
}
-static void
-theme_preferences_selection_changed (GtkComboBox *combo, gpointer user_data)
+static char *
+theme_preferences_gtk3_active_id (theme_preferences_ui *ui)
{
- theme_preferences_ui *ui = user_data;
- gboolean has_selection = gtk_combo_box_get_active (combo) >= 0;
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+ char *id = NULL;
- gtk_widget_set_sensitive (ui->apply_button, has_selection);
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->gtk3_combo), &iter))
+ return NULL;
+
+ model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->gtk3_combo));
+ gtk_tree_model_get (model, &iter, GTK3_THEME_COL_ID, &id, -1);
+ return id;
}
static void
-theme_preferences_apply_cb (GtkWidget *button, gpointer user_data)
+theme_preferences_gtk3_sync_remove_state (theme_preferences_ui *ui)
+{
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+ int source = -1;
+
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->gtk3_combo), &iter))
+ {
+ gtk_widget_set_sensitive (ui->gtk3_remove, FALSE);
+ return;
+ }
+
+ model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->gtk3_combo));
+ gtk_tree_model_get (model, &iter, GTK3_THEME_COL_SOURCE, &source, -1);
+ gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER);
+}
+
+static void
+theme_preferences_gtk3_changed_cb (GtkComboBox *combo, gpointer user_data)
+{
+ theme_preferences_ui *ui = user_data;
+ char *id;
+ gboolean selection_changed;
+ ThemeGtk3Variant variant;
+ GError *error = NULL;
+
+ (void) combo;
+ theme_preferences_gtk3_sync_remove_state (ui);
+ if (ui->gtk3_populating)
+ return;
+
+ id = theme_preferences_gtk3_active_id (ui);
+ if (!id)
+ return;
+
+ variant = theme_gtk3_variant_for_theme (id);
+ selection_changed = g_strcmp0 (prefs.hex_gui_gtk3_theme, id) != 0
+ || prefs.hex_gui_gtk3_variant != variant;
+ g_strlcpy (prefs.hex_gui_gtk3_theme, id, sizeof (prefs.hex_gui_gtk3_theme));
+ prefs.hex_gui_gtk3_variant = variant;
+
+ if (ui->setup_prefs)
+ {
+ g_strlcpy (ui->setup_prefs->hex_gui_gtk3_theme, id, sizeof (ui->setup_prefs->hex_gui_gtk3_theme));
+ ui->setup_prefs->hex_gui_gtk3_variant = prefs.hex_gui_gtk3_variant;
+ }
+
+ if (selection_changed && !theme_gtk3_apply_current (&error))
+ {
+ theme_preferences_show_message (ui, GTK_MESSAGE_ERROR,
+ error ? error->message : _("Failed to apply GTK3 theme."));
+ g_clear_error (&error);
+ }
+
+ g_free (id);
+}
+
+static GdkPixbuf *
+theme_preferences_load_thumbnail (const char *path)
+{
+ char *data = NULL;
+ gsize length = 0;
+ GdkPixbufLoader *loader;
+ GError *error = NULL;
+ GdkPixbuf *pixbuf;
+ GdkPixbuf *scaled;
+ int width;
+ int height;
+
+ if (!path || !g_file_get_contents (path, &data, &length, &error))
+ {
+ g_clear_error (&error);
+ return NULL;
+ }
+
+ loader = gdk_pixbuf_loader_new ();
+ if (!gdk_pixbuf_loader_write (loader, (const guchar *) data, length, &error))
+ {
+ g_clear_error (&error);
+ g_object_unref (loader);
+ g_free (data);
+ return NULL;
+ }
+
+ g_free (data);
+
+ if (!gdk_pixbuf_loader_close (loader, &error))
+ {
+ g_clear_error (&error);
+ g_object_unref (loader);
+ return NULL;
+ }
+
+ pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
+ if (!pixbuf)
+ {
+ g_object_unref (loader);
+ return NULL;
+ }
+
+ width = gdk_pixbuf_get_width (pixbuf);
+ height = gdk_pixbuf_get_height (pixbuf);
+ if (width > 48 || height > 48)
+ scaled = gdk_pixbuf_scale_simple (pixbuf, 48, 48, GDK_INTERP_BILINEAR);
+ else
+ scaled = gdk_pixbuf_copy (pixbuf);
+
+ g_object_unref (loader);
+ return scaled;
+}
+
+static void
+theme_preferences_populate_gtk3 (theme_preferences_ui *ui)
+{
+ GPtrArray *themes;
+ guint i;
+ GtkTreeStore *store;
+ GtkTreeIter iter;
+ int active = -1;
+ gboolean removed_selected_theme = FALSE;
+ gboolean should_apply = FALSE;
+ char *final_id;
+ ThemeGtk3Variant final_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
+ GError *error = NULL;
+
+ store = GTK_TREE_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (ui->gtk3_combo)));
+ ui->gtk3_populating = TRUE;
+ gtk_tree_store_clear (store);
+ themes = zoitechat_gtk3_theme_service_discover ();
+ for (i = 0; i < themes->len; i++)
+ {
+ ZoitechatGtk3Theme *theme = g_ptr_array_index (themes, i);
+ char *label = g_strdup_printf ("%s (%s)", theme->display_name,
+ theme->source == ZOITECHAT_GTK3_THEME_SOURCE_USER ? _("user") : _("system"));
+ GdkPixbuf *thumbnail = NULL;
+
+ if (theme->thumbnail_path && g_file_test (theme->thumbnail_path, G_FILE_TEST_IS_REGULAR))
+ thumbnail = theme_preferences_load_thumbnail (theme->thumbnail_path);
+
+ gtk_tree_store_append (store, &iter, NULL);
+ gtk_tree_store_set (store, &iter,
+ GTK3_THEME_COL_ID, theme->id,
+ GTK3_THEME_COL_LABEL, label,
+ GTK3_THEME_COL_SOURCE, theme->source,
+ GTK3_THEME_COL_THUMBNAIL, thumbnail,
+ -1);
+ if (g_strcmp0 (prefs.hex_gui_gtk3_theme, theme->id) == 0)
+ active = i;
+ if (thumbnail)
+ g_object_unref (thumbnail);
+ g_free (label);
+ }
+ if (active >= 0)
+ gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), active);
+ else if (themes->len > 0)
+ {
+ gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), 0);
+ if (prefs.hex_gui_gtk3_theme[0] != '\0')
+ removed_selected_theme = TRUE;
+ }
+ else if (prefs.hex_gui_gtk3_theme[0] != '\0')
+ removed_selected_theme = TRUE;
+ gtk_widget_set_sensitive (ui->gtk3_combo, themes->len > 0);
+ theme_preferences_gtk3_sync_remove_state (ui);
+ ui->gtk3_populating = FALSE;
+
+ final_id = theme_preferences_gtk3_active_id (ui);
+ if (final_id)
+ {
+ final_variant = theme_gtk3_variant_for_theme (final_id);
+ should_apply = g_strcmp0 (prefs.hex_gui_gtk3_theme, final_id) != 0
+ || prefs.hex_gui_gtk3_variant != final_variant
+ || removed_selected_theme;
+ g_strlcpy (prefs.hex_gui_gtk3_theme, final_id, sizeof (prefs.hex_gui_gtk3_theme));
+ if (ui->setup_prefs)
+ g_strlcpy (ui->setup_prefs->hex_gui_gtk3_theme,
+ final_id,
+ sizeof (ui->setup_prefs->hex_gui_gtk3_theme));
+ g_free (final_id);
+ }
+ prefs.hex_gui_gtk3_variant = final_variant;
+ if (ui->setup_prefs)
+ ui->setup_prefs->hex_gui_gtk3_variant = final_variant;
+
+ if (should_apply && !theme_gtk3_apply_current (&error))
+ {
+ theme_preferences_show_message (ui, GTK_MESSAGE_ERROR,
+ error ? error->message : _("Failed to apply GTK3 theme."));
+ g_clear_error (&error);
+ }
+
+ g_ptr_array_unref (themes);
+}
+
+static void
+theme_preferences_gtk3_import_cb (GtkWidget *button, gpointer user_data)
{
theme_preferences_ui *ui = user_data;
GtkWidget *dialog;
+ GtkFileFilter *filter;
+ GtkWidget *folder_dialog;
+ char *path;
+ char *id = NULL;
+ GError *error = NULL;
gint response;
- char *theme;
+
+ (void)button;
+ dialog = gtk_file_chooser_dialog_new (_("Import GTK3 Theme"), ui->parent,
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ _("Import _Folder"), 1,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Import"), GTK_RESPONSE_ACCEPT,
+ NULL);
+ theme_manager_attach_window (dialog);
+ gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (dialog), TRUE);
+ gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (dialog), FALSE);
+ filter = gtk_file_filter_new ();
+ gtk_file_filter_set_name (filter, _("Theme archives (*.zip, *.tar, *.tar.xz, *.tgz, *.tar.gz, *.tar.bz2)"));
+ gtk_file_filter_add_pattern (filter, "*.zip");
+ gtk_file_filter_add_pattern (filter, "*.tar");
+ gtk_file_filter_add_pattern (filter, "*.tar.xz");
+ gtk_file_filter_add_pattern (filter, "*.txz");
+ gtk_file_filter_add_pattern (filter, "*.tar.gz");
+ gtk_file_filter_add_pattern (filter, "*.tgz");
+ gtk_file_filter_add_pattern (filter, "*.tar.bz2");
+ gtk_file_filter_add_pattern (filter, "*.tbz");
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
+
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+ if (response == 1)
+ {
+ gtk_widget_destroy (dialog);
+ folder_dialog = gtk_file_chooser_dialog_new (_("Import GTK3 Theme Folder"), ui->parent,
+ GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Import"), GTK_RESPONSE_ACCEPT,
+ NULL);
+ theme_manager_attach_window (folder_dialog);
+ gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (folder_dialog), TRUE);
+ if (gtk_dialog_run (GTK_DIALOG (folder_dialog)) != GTK_RESPONSE_ACCEPT)
+ {
+ gtk_widget_destroy (folder_dialog);
+ return;
+ }
+
+ path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (folder_dialog));
+ gtk_widget_destroy (folder_dialog);
+ }
+ else if (response == GTK_RESPONSE_ACCEPT)
+ {
+ path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
+ gtk_widget_destroy (dialog);
+ }
+ else
+ {
+ gtk_widget_destroy (dialog);
+ return;
+ }
+
+ if (!zoitechat_gtk3_theme_service_import (path, &id, &error))
+ theme_preferences_show_message (ui, GTK_MESSAGE_ERROR,
+ error ? error->message : _("Failed to import GTK3 theme."));
+ g_clear_error (&error);
+ g_free (path);
+ theme_preferences_populate_gtk3 (ui);
+}
+
+static void
+theme_preferences_gtk3_remove_cb (GtkWidget *button, gpointer user_data)
+{
+ theme_preferences_ui *ui = user_data;
+ char *id;
GError *error = NULL;
(void)button;
- theme = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (ui->combo));
- if (!theme)
+ id = theme_preferences_gtk3_active_id (ui);
+ if (!id)
return;
- dialog = gtk_message_dialog_new (ui->parent, GTK_DIALOG_MODAL,
- GTK_MESSAGE_WARNING, GTK_BUTTONS_OK_CANCEL,
- "%s", _("Applying a theme will overwrite your current colors and event settings.\nContinue?"));
- response = gtk_dialog_run (GTK_DIALOG (dialog));
- gtk_widget_destroy (dialog);
-
- if (response != GTK_RESPONSE_OK)
- {
- g_free (theme);
- return;
- }
-
- if (!zoitechat_apply_theme (theme, &error))
- {
+ if (!zoitechat_gtk3_theme_service_remove_user_theme (id, &error))
theme_preferences_show_message (ui, GTK_MESSAGE_ERROR,
- error ? error->message : _("Failed to apply theme."));
- g_clear_error (&error);
- goto cleanup;
- }
-
- if (ui->color_change_flag)
- *ui->color_change_flag = TRUE;
-
- theme_preferences_show_message (ui,
- GTK_MESSAGE_INFO,
- _("Theme applied. Some changes may require a restart to take full effect."));
-
-cleanup:
- g_free (theme);
+ error ? error->message : _("Failed to remove GTK3 theme."));
+ g_clear_error (&error);
+ g_free (id);
+ theme_preferences_populate_gtk3 (ui);
}
GtkWidget *
-theme_preferences_create_page (GtkWindow *parent, gboolean *color_change_flag)
+theme_preferences_create_page (GtkWindow *parent,
+ struct zoitechatprefs *setup_prefs,
+ gboolean *color_change_flag)
{
theme_preferences_ui *ui;
GtkWidget *box;
GtkWidget *label;
- GtkWidget *hbox;
- GtkWidget *button_box;
- char *themes_dir;
- char *markup;
+ GtkWidget *colors_frame;
+ GtkWidget *colors_box;
+ GtkWidget *manage_colors_button;
+ GtkWidget *gtk3_frame;
+ GtkWidget *gtk3_grid;
+ GtkWidget *gtk3_button;
+ GtkTreeStore *gtk3_store;
+ GtkCellRenderer *renderer;
ui = g_new0 (theme_preferences_ui, 1);
ui->parent = parent;
- ui->color_change_flag = color_change_flag;
+ ui->setup_prefs = setup_prefs;
box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 6);
gtk_container_set_border_width (GTK_CONTAINER (box), 6);
- themes_dir = zoitechat_theme_service_get_themes_dir ();
- markup = g_markup_printf_escaped (_("Theme files are loaded from %s."), themes_dir);
- label = gtk_label_new (NULL);
- gtk_label_set_markup (GTK_LABEL (label), markup);
- gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+ colors_frame = gtk_frame_new (_("Colors"));
+ gtk_box_pack_start (GTK_BOX (box), colors_frame, FALSE, FALSE, 0);
+ colors_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ gtk_container_set_border_width (GTK_CONTAINER (colors_box), 6);
+ gtk_container_add (GTK_CONTAINER (colors_frame), colors_box);
+
+ label = gtk_label_new (_("GTK3 theme colors are used by default. Open the color manager to set custom colors."));
gtk_widget_set_halign (label, GTK_ALIGN_START);
- gtk_widget_set_valign (label, GTK_ALIGN_CENTER);
- gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
- g_free (markup);
- g_free (themes_dir);
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
+ gtk_box_pack_start (GTK_BOX (colors_box), label, FALSE, FALSE, 0);
- hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
- gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0);
+ manage_colors_button = gtk_button_new_with_label (_("Manage all client colors…"));
+ gtk_widget_set_halign (manage_colors_button, GTK_ALIGN_START);
+ gtk_box_pack_start (GTK_BOX (colors_box), manage_colors_button, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (manage_colors_button), "clicked",
+ G_CALLBACK (theme_preferences_manage_colors_cb), color_change_flag);
- ui->combo = gtk_combo_box_text_new ();
- gtk_box_pack_start (GTK_BOX (hbox), ui->combo, TRUE, TRUE, 0);
- g_signal_connect (G_OBJECT (ui->combo), "changed",
- G_CALLBACK (theme_preferences_selection_changed), ui);
+ gtk3_frame = gtk_frame_new (_("GTK3 Theme"));
+ gtk_box_pack_start (GTK_BOX (box), gtk3_frame, FALSE, FALSE, 0);
+ gtk3_grid = gtk_grid_new ();
+ gtk_container_set_border_width (GTK_CONTAINER (gtk3_grid), 6);
+ gtk_grid_set_row_spacing (GTK_GRID (gtk3_grid), 6);
+ gtk_grid_set_column_spacing (GTK_GRID (gtk3_grid), 6);
+ gtk_container_add (GTK_CONTAINER (gtk3_frame), gtk3_grid);
- button_box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
- gtk_box_pack_start (GTK_BOX (hbox), button_box, FALSE, FALSE, 0);
+ label = gtk_label_new (_("GTK3 theme:"));
+ gtk_widget_set_halign (label, GTK_ALIGN_START);
+ gtk_grid_attach (GTK_GRID (gtk3_grid), label, 0, 0, 1, 1);
+ gtk3_store = gtk_tree_store_new (GTK3_THEME_COL_COUNT,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_INT,
+ GDK_TYPE_PIXBUF);
+ ui->gtk3_combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (gtk3_store));
+ g_object_unref (gtk3_store);
+ renderer = gtk_cell_renderer_pixbuf_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, FALSE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, "pixbuf", GTK3_THEME_COL_THUMBNAIL);
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, "text", GTK3_THEME_COL_LABEL);
+ gtk_grid_attach (GTK_GRID (gtk3_grid), ui->gtk3_combo, 1, 0, 3, 1);
+ g_signal_connect (G_OBJECT (ui->gtk3_combo), "changed", G_CALLBACK (theme_preferences_gtk3_changed_cb), ui);
- ui->apply_button = gtk_button_new_with_mnemonic (_("_Apply Theme"));
- gtk_box_pack_start (GTK_BOX (button_box), ui->apply_button, FALSE, FALSE, 0);
- g_signal_connect (G_OBJECT (ui->apply_button), "clicked",
- G_CALLBACK (theme_preferences_apply_cb), ui);
+ gtk3_button = gtk_button_new_with_mnemonic (_("Import"));
+ gtk_grid_attach (GTK_GRID (gtk3_grid), gtk3_button, 0, 1, 1, 1);
+ g_signal_connect (G_OBJECT (gtk3_button), "clicked", G_CALLBACK (theme_preferences_gtk3_import_cb), ui);
- label = gtk_button_new_with_mnemonic (_("_Refresh"));
- gtk_box_pack_start (GTK_BOX (button_box), label, FALSE, FALSE, 0);
- g_signal_connect (G_OBJECT (label), "clicked",
- G_CALLBACK (theme_preferences_refresh_cb), ui);
+ ui->gtk3_remove = gtk_button_new_with_mnemonic (_("Remove"));
+ gtk_grid_attach (GTK_GRID (gtk3_grid), ui->gtk3_remove, 1, 1, 1, 1);
+ g_signal_connect (G_OBJECT (ui->gtk3_remove), "clicked", G_CALLBACK (theme_preferences_gtk3_remove_cb), ui);
- label = gtk_button_new_with_mnemonic (_("_Open Folder"));
- gtk_box_pack_start (GTK_BOX (button_box), label, FALSE, FALSE, 0);
- g_signal_connect (G_OBJECT (label), "clicked",
- G_CALLBACK (theme_preferences_open_folder_cb), ui);
+ gtk3_button = gtk_button_new_with_mnemonic (_("Open theme folder"));
+ gtk_grid_attach (GTK_GRID (gtk3_grid), gtk3_button, 2, 1, 2, 1);
+ g_signal_connect (G_OBJECT (gtk3_button), "clicked", G_CALLBACK (theme_preferences_open_gtk3_folder_cb), ui);
- ui->status_label = gtk_label_new (NULL);
- gtk_widget_set_halign (ui->status_label, GTK_ALIGN_START);
- gtk_widget_set_valign (ui->status_label, GTK_ALIGN_CENTER);
- gtk_box_pack_start (GTK_BOX (box), ui->status_label, FALSE, FALSE, 0);
-
- theme_preferences_populate (ui);
+ theme_preferences_populate_gtk3 (ui);
g_object_set_data_full (G_OBJECT (box), "theme-preferences-ui", ui, g_free);
return box;
}
-static void
-theme_preferences_apply_entry_style (GtkWidget *entry, InputStyle *input_style)
-{
- ThemeWidgetStyleValues style_values;
-
- theme_get_widget_style_values (&style_values);
- gtkutil_apply_palette (entry, &style_values.background, &style_values.foreground,
- input_style ? input_style->font_desc : NULL);
-}
-
void
theme_preferences_apply_to_session (session_gui *gui, InputStyle *input_style)
{
- if (prefs.hex_gui_input_style)
- {
- theme_css_reload_input_style (TRUE, input_style ? input_style->font_desc : NULL);
- theme_preferences_apply_entry_style (gui->input_box, input_style);
- theme_preferences_apply_entry_style (gui->limit_entry, input_style);
- theme_preferences_apply_entry_style (gui->key_entry, input_style);
- theme_preferences_apply_entry_style (gui->topic_entry, input_style);
- }
-
if (gui->user_tree)
{
theme_manager_apply_userlist_style (gui->user_tree,
diff --git a/src/fe-gtk/theme/theme-preferences.h b/src/fe-gtk/theme/theme-preferences.h
index ee55fbdf..2ca3c3a8 100644
--- a/src/fe-gtk/theme/theme-preferences.h
+++ b/src/fe-gtk/theme/theme-preferences.h
@@ -7,7 +7,9 @@
#include "../fe-gtk.h"
#include "../../common/zoitechat.h"
-GtkWidget *theme_preferences_create_page (GtkWindow *parent, gboolean *color_change_flag);
+GtkWidget *theme_preferences_create_page (GtkWindow *parent,
+ struct zoitechatprefs *setup_prefs,
+ gboolean *color_change_flag);
GtkWidget *theme_preferences_create_color_page (GtkWindow *parent,
struct zoitechatprefs *setup_prefs,
gboolean *color_change_flag);
diff --git a/src/fe-gtk/theme/theme-runtime.c b/src/fe-gtk/theme/theme-runtime.c
index b23c0b10..6990c380 100644
--- a/src/fe-gtk/theme/theme-runtime.c
+++ b/src/fe-gtk/theme/theme-runtime.c
@@ -97,6 +97,8 @@ static ThemePalette active_palette;
static gboolean user_colors_valid = FALSE;
static gboolean dark_user_colors_valid = FALSE;
static gboolean dark_mode_active = FALSE;
+static gboolean light_custom_tokens[THEME_TOKEN_COUNT];
+static gboolean dark_custom_tokens[THEME_TOKEN_COUNT];
#define THEME_PALETTE_MIGRATION_MARKER_KEY "theme.palette.semantic_migrated"
#define THEME_PALETTE_MIGRATION_MARKER_VALUE 1
@@ -109,6 +111,75 @@ typedef struct
gboolean *mode_valid;
} ThemePalettePersistenceMode;
+static void
+theme_runtime_resolve_color (const GdkRGBA *mapped, const GdkRGBA *fallback, GdkRGBA *resolved)
+{
+ gdouble alpha;
+
+ g_return_if_fail (mapped != NULL);
+ g_return_if_fail (fallback != NULL);
+ g_return_if_fail (resolved != NULL);
+
+ alpha = CLAMP (mapped->alpha, 0.0, 1.0);
+ resolved->red = (mapped->red * alpha) + (fallback->red * (1.0 - alpha));
+ resolved->green = (mapped->green * alpha) + (fallback->green * (1.0 - alpha));
+ resolved->blue = (mapped->blue * alpha) + (fallback->blue * (1.0 - alpha));
+ resolved->alpha = 1.0;
+}
+
+static void
+theme_runtime_apply_gtk_map (ThemePalette *palette, const ThemeGtkPaletteMap *gtk_map, const gboolean *custom_tokens)
+{
+ GdkRGBA text_foreground;
+ GdkRGBA text_background;
+ GdkRGBA selection_foreground;
+ GdkRGBA selection_background;
+ GdkRGBA accent;
+ GdkRGBA fallback;
+
+ g_return_if_fail (palette != NULL);
+ if (gtk_map == NULL || !gtk_map->enabled || custom_tokens == NULL)
+ return;
+
+ g_assert (theme_palette_get_color (palette, THEME_TOKEN_TEXT_FOREGROUND, &fallback));
+ theme_runtime_resolve_color (>k_map->text_foreground, &fallback, &text_foreground);
+ g_assert (theme_palette_get_color (palette, THEME_TOKEN_TEXT_BACKGROUND, &fallback));
+ theme_runtime_resolve_color (>k_map->text_background, &fallback, &text_background);
+ g_assert (theme_palette_get_color (palette, THEME_TOKEN_SELECTION_FOREGROUND, &fallback));
+ theme_runtime_resolve_color (>k_map->selection_foreground, &fallback, &selection_foreground);
+ g_assert (theme_palette_get_color (palette, THEME_TOKEN_SELECTION_BACKGROUND, &fallback));
+ theme_runtime_resolve_color (>k_map->selection_background, &fallback, &selection_background);
+ g_assert (theme_palette_get_color (palette, THEME_TOKEN_MARKER, &fallback));
+ theme_runtime_resolve_color (>k_map->accent, &fallback, &accent);
+
+ if (!custom_tokens[THEME_TOKEN_TEXT_FOREGROUND])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_TEXT_FOREGROUND, &text_foreground));
+ if (!custom_tokens[THEME_TOKEN_TEXT_BACKGROUND])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_TEXT_BACKGROUND, &text_background));
+ if (!custom_tokens[THEME_TOKEN_SELECTION_FOREGROUND])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_SELECTION_FOREGROUND, &selection_foreground));
+ if (!custom_tokens[THEME_TOKEN_SELECTION_BACKGROUND])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_SELECTION_BACKGROUND, &selection_background));
+ if (!custom_tokens[THEME_TOKEN_MARKER])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_MARKER, &accent));
+ if (!custom_tokens[THEME_TOKEN_TAB_NEW_DATA])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_TAB_NEW_DATA, &accent));
+ if (!custom_tokens[THEME_TOKEN_TAB_HIGHLIGHT])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_TAB_HIGHLIGHT, &accent));
+ if (!custom_tokens[THEME_TOKEN_TAB_NEW_MESSAGE])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_TAB_NEW_MESSAGE, &accent));
+ if (!custom_tokens[THEME_TOKEN_TAB_AWAY])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_TAB_AWAY, &accent));
+ if (!custom_tokens[THEME_TOKEN_SPELL])
+ g_assert (theme_palette_set_color (palette, THEME_TOKEN_SPELL, &accent));
+}
+
+static const gboolean *
+theme_runtime_active_custom_tokens (void)
+{
+ return light_custom_tokens;
+}
+
static void
palette_color_set_rgb16 (GdkRGBA *color, guint16 red, guint16 green, guint16 blue)
{
@@ -231,6 +302,13 @@ theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba)
return theme_palette_get_color (&active_palette, token, out_rgba);
}
+gboolean
+theme_runtime_mode_has_user_colors (gboolean dark_mode)
+{
+ (void) dark_mode;
+ return user_colors_valid;
+}
+
void
theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values)
{
@@ -238,6 +316,17 @@ theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values)
theme_palette_to_widget_style_values (&active_palette, out_values);
}
+void
+theme_runtime_get_widget_style_values_mapped (const ThemeGtkPaletteMap *gtk_map, ThemeWidgetStyleValues *out_values)
+{
+ ThemePalette mapped_palette;
+
+ g_return_if_fail (out_values != NULL);
+ mapped_palette = active_palette;
+ theme_runtime_apply_gtk_map (&mapped_palette, gtk_map, theme_runtime_active_custom_tokens ());
+ theme_palette_to_widget_style_values (&mapped_palette, out_values);
+}
+
void
theme_runtime_get_xtext_colors (XTextColor *palette, size_t palette_len)
{
@@ -245,6 +334,17 @@ theme_runtime_get_xtext_colors (XTextColor *palette, size_t palette_len)
theme_palette_to_xtext_colors (&active_palette, palette, palette_len);
}
+void
+theme_runtime_get_xtext_colors_mapped (const ThemeGtkPaletteMap *gtk_map, XTextColor *palette, size_t palette_len)
+{
+ ThemePalette mapped_palette;
+
+ g_return_if_fail (palette != NULL);
+ mapped_palette = active_palette;
+ theme_runtime_apply_gtk_map (&mapped_palette, gtk_map, theme_runtime_active_custom_tokens ());
+ theme_palette_to_xtext_colors (&mapped_palette, palette, palette_len);
+}
+
void
theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col)
{
@@ -256,6 +356,7 @@ theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col)
light_palette = active_palette;
g_assert (theme_palette_set_color (&light_palette, token, col));
+ light_custom_tokens[token] = TRUE;
user_colors_valid = TRUE;
}
@@ -270,9 +371,23 @@ theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col)
dark_palette = active_palette;
g_assert (theme_palette_set_color (&dark_palette, token, col));
+ dark_custom_tokens[token] = TRUE;
dark_user_colors_valid = TRUE;
}
+void
+theme_runtime_reset_mode_colors (gboolean dark_mode)
+{
+ (void) dark_mode;
+ theme_palette_from_legacy_colors (&light_palette, legacy_light_defaults, G_N_ELEMENTS (legacy_light_defaults));
+ active_palette = light_palette;
+ memset (light_custom_tokens, 0, sizeof light_custom_tokens);
+ memset (dark_custom_tokens, 0, sizeof dark_custom_tokens);
+ user_colors_valid = TRUE;
+ dark_user_colors_valid = FALSE;
+ dark_mode_active = FALSE;
+}
+
void
theme_runtime_load (void)
{
@@ -287,6 +402,8 @@ theme_runtime_load (void)
const size_t mode_count = G_N_ELEMENTS (modes);
palette_init_defaults ();
+ memset (light_custom_tokens, 0, sizeof light_custom_tokens);
+ memset (dark_custom_tokens, 0, sizeof dark_custom_tokens);
fh = zoitechat_open_file ("colors.conf", O_RDONLY, 0, 0);
if (fh != -1)
@@ -312,7 +429,10 @@ theme_runtime_load (void)
found = theme_runtime_load_migrated_legacy_color (cfg, &modes[i], def, &color);
if (found)
{
+ gboolean *custom_tokens;
g_assert (theme_palette_set_color (modes[i].palette, def->token, &color));
+ custom_tokens = (modes[i].palette == &dark_palette) ? dark_custom_tokens : light_custom_tokens;
+ custom_tokens[def->token] = TRUE;
mode_found = TRUE;
}
}
@@ -379,9 +499,13 @@ theme_runtime_save (void)
for (j = 0; j < theme_palette_token_def_count (); j++)
{
const ThemePaletteTokenDef *def = theme_palette_token_def_at (j);
+ const gboolean *custom_tokens;
GdkRGBA color;
g_assert (def != NULL);
+ custom_tokens = (modes[i].palette == &dark_palette) ? dark_custom_tokens : light_custom_tokens;
+ if (!custom_tokens[def->token])
+ continue;
g_assert (theme_palette_get_color (modes[i].palette, def->token, &color));
palette_write_token_color (fh, modes[i].mode_name, def, &color);
}
@@ -450,13 +574,15 @@ theme_runtime_apply_dark_mode (gboolean enable)
gboolean
theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed)
{
- gboolean dark = theme_policy_is_dark_mode_active (mode);
- gboolean changed = theme_runtime_apply_dark_mode (dark);
+ gboolean changed;
+
+ (void) mode;
+ changed = theme_runtime_apply_dark_mode (FALSE);
if (palette_changed)
*palette_changed = changed;
- return dark;
+ return FALSE;
}
gboolean
diff --git a/src/fe-gtk/theme/theme-runtime.h b/src/fe-gtk/theme/theme-runtime.h
index 332ab3a4..483c3af8 100644
--- a/src/fe-gtk/theme/theme-runtime.h
+++ b/src/fe-gtk/theme/theme-runtime.h
@@ -7,15 +7,29 @@
#include "theme-palette.h"
+typedef struct
+{
+ gboolean enabled;
+ GdkRGBA text_foreground;
+ GdkRGBA text_background;
+ GdkRGBA selection_foreground;
+ GdkRGBA selection_background;
+ GdkRGBA accent;
+} ThemeGtkPaletteMap;
+
void theme_runtime_load (void);
void theme_runtime_save (void);
gboolean theme_runtime_apply_mode (unsigned int mode, gboolean *palette_changed);
gboolean theme_runtime_apply_dark_mode (gboolean enable);
void theme_runtime_user_set_color (ThemeSemanticToken token, const GdkRGBA *col);
void theme_runtime_dark_set_color (ThemeSemanticToken token, const GdkRGBA *col);
+void theme_runtime_reset_mode_colors (gboolean dark_mode);
gboolean theme_runtime_get_color (ThemeSemanticToken token, GdkRGBA *out_rgba);
+gboolean theme_runtime_mode_has_user_colors (gboolean dark_mode);
void theme_runtime_get_widget_style_values (ThemeWidgetStyleValues *out_values);
+void theme_runtime_get_widget_style_values_mapped (const ThemeGtkPaletteMap *gtk_map, ThemeWidgetStyleValues *out_values);
void theme_runtime_get_xtext_colors (XTextColor *palette, size_t palette_len);
+void theme_runtime_get_xtext_colors_mapped (const ThemeGtkPaletteMap *gtk_map, XTextColor *palette, size_t palette_len);
gboolean theme_runtime_is_dark_active (void);
#endif
diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c
index ebf634ac..4229651b 100644
--- a/src/fe-gtk/xtext.c
+++ b/src/fe-gtk/xtext.c
@@ -47,6 +47,7 @@
#include "fe-gtk.h"
#include "xtext.h"
#include "fkeys.h"
+#include "theme/theme-access.h"
#define charlen(str) g_utf8_skip[*(guchar *)(str)]
@@ -363,8 +364,54 @@ xtext_draw_bg_offset (GtkXText *xtext, int x, int y, int width, int height, int
if (xtext->background_surface)
{
- cairo_set_source_surface (cr, xtext->background_surface, tile_x, tile_y);
- cairo_pattern_set_extend (cairo_get_source (cr), CAIRO_EXTEND_REPEAT);
+ int clip_x = xtext->clip_x;
+ int clip_y = xtext->clip_y;
+ int clip_w = xtext->clip_x2 - xtext->clip_x;
+ int clip_h = xtext->clip_y2 - xtext->clip_y;
+
+ if (clip_w < 1 || clip_h < 1)
+ {
+ GtkAllocation allocation;
+
+ gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation);
+ clip_x = 0;
+ clip_y = 0;
+ clip_w = allocation.width;
+ clip_h = allocation.height;
+ }
+
+ if (xtext->background_clip_surface == NULL ||
+ xtext->background_clip_cycle != xtext->render_cycle ||
+ xtext->background_clip_x != clip_x ||
+ xtext->background_clip_y != clip_y ||
+ xtext->background_clip_width != clip_w ||
+ xtext->background_clip_height != clip_h)
+ {
+ cairo_t *bg_cr;
+
+ if (xtext->background_clip_surface)
+ {
+ cairo_surface_destroy (xtext->background_clip_surface);
+ xtext->background_clip_surface = NULL;
+ }
+
+ xtext->background_clip_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, clip_w, clip_h);
+ bg_cr = cairo_create (xtext->background_clip_surface);
+ cairo_set_source_surface (bg_cr, xtext->background_surface, tile_x - clip_x, tile_y - clip_y);
+ cairo_pattern_set_extend (cairo_get_source (bg_cr), CAIRO_EXTEND_REPEAT);
+ cairo_rectangle (bg_cr, 0.0, 0.0, (double)clip_w, (double)clip_h);
+ cairo_fill (bg_cr);
+ cairo_destroy (bg_cr);
+
+ xtext->background_clip_x = clip_x;
+ xtext->background_clip_y = clip_y;
+ xtext->background_clip_width = clip_w;
+ xtext->background_clip_height = clip_h;
+ xtext->background_clip_cycle = xtext->render_cycle;
+ }
+
+ cairo_set_source_surface (cr, xtext->background_clip_surface,
+ (double)xtext->background_clip_x, (double)xtext->background_clip_y);
cairo_rectangle (cr, (double)x, (double)y, (double)width, (double)height);
cairo_fill (cr);
}
@@ -628,13 +675,36 @@ xtext_set_bg (GtkXText *xtext, int index)
xtext->bgc = xtext->palette[index];
}
+static void
+gtk_xtext_sync_palette_from_theme (GtkXText *xtext)
+{
+ XTextColor palette[XTEXT_COLS];
+
+ theme_get_xtext_colors_for_widget (GTK_WIDGET (xtext), palette, G_N_ELEMENTS (palette));
+ gtk_xtext_set_palette (xtext, palette);
+}
+
+static void
+gtk_xtext_style_updated (GtkWidget *widget, gpointer user_data)
+{
+ (void) user_data;
+ gtk_xtext_sync_palette_from_theme (GTK_XTEXT (widget));
+}
+
static void
gtk_xtext_init (GtkXText * xtext)
{
xtext->background_surface = NULL;
+ xtext->background_clip_surface = NULL;
xtext->draw_window = NULL;
xtext->draw_surface = NULL;
xtext->draw_cr = NULL;
+ xtext->background_clip_x = 0;
+ xtext->background_clip_y = 0;
+ xtext->background_clip_width = 0;
+ xtext->background_clip_height = 0;
+ xtext->background_clip_cycle = 0;
+ xtext->render_cycle = 0;
xtext->io_tag = 0;
xtext->add_io_tag = 0;
xtext->scroll_tag = 0;
@@ -668,6 +738,8 @@ gtk_xtext_init (GtkXText * xtext)
gtk_xtext_scroll_adjustments (xtext, NULL, NULL);
gtk_xtext_install_selection_targets (GTK_WIDGET (xtext));
+ gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (xtext)), "view");
+ g_signal_connect (G_OBJECT (xtext), "style-updated", G_CALLBACK (gtk_xtext_style_updated), NULL);
}
static void
@@ -768,6 +840,7 @@ gtk_xtext_new (const XTextColor *palette, int separator)
/* GTK3 already uses the GTK render pipeline; no manual double-buffering toggle. */
gtk_xtext_set_palette (xtext, palette);
+ gtk_xtext_sync_palette_from_theme (xtext);
return GTK_WIDGET (xtext);
}
@@ -799,6 +872,12 @@ gtk_xtext_cleanup (GtkXText *xtext)
xtext->background_surface = NULL;
}
+ if (xtext->background_clip_surface)
+ {
+ cairo_surface_destroy (xtext->background_clip_surface);
+ xtext->background_clip_surface = NULL;
+ }
+
if (xtext->font)
{
backend_font_close (xtext);
@@ -1356,6 +1435,12 @@ gtk_xtext_render (GtkWidget *widget, GdkRectangle *area, cairo_t *cr)
cairo_t *old_cr = xtext->draw_cr;
xtext->draw_cr = cr;
+ xtext->render_cycle++;
+ if (xtext->background_clip_surface)
+ {
+ cairo_surface_destroy (xtext->background_clip_surface);
+ xtext->background_clip_surface = NULL;
+ }
gtk_widget_get_allocation (widget, &allocation);
@@ -1418,6 +1503,11 @@ xit:
gtk_xtext_draw_sep (xtext, -1);
done:
+ if (xtext->background_clip_surface)
+ {
+ cairo_surface_destroy (xtext->background_clip_surface);
+ xtext->background_clip_surface = NULL;
+ }
xtext->draw_cr = old_cr;
}
@@ -2973,8 +3063,6 @@ gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str,
int len, int *emphasis, int str_width)
{
int dofill;
- int tile_x = xtext->ts_x;
- int tile_y = xtext->ts_y;
if (xtext->dont_render || len < 1 || xtext->hidden)
return 0;
@@ -2999,16 +3087,7 @@ gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str,
goto dounder;
}
- dofill = TRUE;
-
- /* backcolor is always handled by XDrawImageString */
- if (!xtext->backcolor && xtext->background_surface)
- {
- /* draw the background surface behind the text - CAUSES FLICKER HERE!! */
- xtext_draw_bg_offset (xtext, x, y - xtext->font->ascent, str_width,
- xtext->fontsize, tile_x, tile_y);
- dofill = FALSE; /* already drawn the background */
- }
+ dofill = !xtext->background_surface || xtext->backcolor;
backend_draw_text_emph (xtext, dofill, x, y, str, len, str_width, *emphasis);
@@ -3793,18 +3872,6 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
indent = ent->indent;
start_subline = subline;
- /* draw the timestamp */
- if (xtext->auto_indent && xtext->buffer->time_stamp &&
- (!xtext->skip_stamp || xtext->mark_stamp || xtext->force_stamp))
- {
- char *time_str;
- int len;
-
- len = xtext_get_stamp_str (ent->stamp, &time_str);
- gtk_xtext_render_stamp (xtext, ent, time_str, len, line, win_width);
- g_free (time_str);
- }
-
/* draw each line one by one */
do
{
@@ -3818,6 +3885,28 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
if (!subline)
{
+ int bg_x;
+ int bg_w;
+
+ if (!xtext->dont_render)
+ {
+ bg_x = MAX (0, xtext->clip_x);
+ bg_w = MIN (win_width + MARGIN, xtext->clip_x2) - bg_x;
+ if (bg_w > 0)
+ xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
+ }
+
+ if (entline == 1 && xtext->auto_indent && xtext->buffer->time_stamp &&
+ (!xtext->skip_stamp || xtext->mark_stamp || xtext->force_stamp))
+ {
+ char *time_str;
+ int stamp_len;
+
+ stamp_len = xtext_get_stamp_str (ent->stamp, &time_str);
+ gtk_xtext_render_stamp (xtext, ent, time_str, stamp_len, line, win_width);
+ g_free (time_str);
+ }
+
if (!gtk_xtext_render_str (xtext, y, ent, str, len, win_width,
indent, line, FALSE, NULL, &emphasis))
{
@@ -3960,6 +4049,12 @@ gtk_xtext_set_background (GtkXText * xtext, cairo_surface_t *surface)
xtext->background_surface = NULL;
}
+ if (xtext->background_clip_surface)
+ {
+ cairo_surface_destroy (xtext->background_clip_surface);
+ xtext->background_clip_surface = NULL;
+ }
+
dontscroll (xtext->buffer);
if (surface)
{
diff --git a/src/fe-gtk/xtext.h b/src/fe-gtk/xtext.h
index 58d70f7b..3f2df515 100644
--- a/src/fe-gtk/xtext.h
+++ b/src/fe-gtk/xtext.h
@@ -135,9 +135,16 @@ struct _GtkXText
GtkAdjustment *adj;
cairo_surface_t *background_surface; /* 0 = use palette[19] */
+ cairo_surface_t *background_clip_surface;
GdkWindow *draw_window; /* points to ->window */
cairo_surface_t *draw_surface; /* temporary surface for offscreen draws */
cairo_t *draw_cr; /* GTK3 draw context */
+ int background_clip_x;
+ int background_clip_y;
+ int background_clip_width;
+ int background_clip_height;
+ int background_clip_cycle;
+ int render_cycle;
GdkCursor *hand_cursor;
GdkCursor *resize_cursor;
diff --git a/win32/installer/zoitechat.iss.tt b/win32/installer/zoitechat.iss.tt
index 9d719f68..a3979f12 100644
--- a/win32/installer/zoitechat.iss.tt
+++ b/win32/installer/zoitechat.iss.tt
@@ -166,6 +166,8 @@ Source: "plugins\hclua.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Com
Source: "plugins\hcchecksum.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\checksum
Source: "plugins\hcexec.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\exec
Source: "plugins\hcfishlim.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\fishlim
+Source: "share\gtkpref.png"; DestDir: "{app}\share"; Flags: ignoreversion; Components: libs
+Source: "share\adwaita-icons-attribution.txt"; DestDir: "{app}\share"; Flags: ignoreversion; Components: libs
Source: "share\music.png"; DestDir: "{app}\share"; Flags: ignoreversion; Components: plugins\winamp
Source: "share\download.png"; DestDir: "{app}\share"; Flags: ignoreversion; Components: plugins\upd
Source: "plugins\hcupd.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: plugins\upd
diff --git a/win32/zoitechat.props b/win32/zoitechat.props
index 1fca9f00..8f3d6afd 100644
--- a/win32/zoitechat.props
+++ b/win32/zoitechat.props
@@ -113,7 +113,22 @@
libpng16_static.lib
libpng.lib
- $(Gtk3Lib);$(Gdk3Lib);wininet.lib;winmm.lib;ws2_32.lib;atk-1.0.lib;gio-2.0.lib;gdk_pixbuf-2.0.lib;pangowin32-1.0.lib;pangocairo-1.0.lib;pango-1.0.lib;cairo.lib;gobject-2.0.lib;gmodule-2.0.lib;glib-2.0.lib;$(IntlLib);$(IconvLib);$(ZlibLib);$(Xml2Lib);$(JpegLib);$(PngLib);$(OpenSslLibs)
+ $(DepsRoot)\lib
+
+ $(DepsRoot)\include
+ $(DepsRoot)\include\archive
+ $(DepsRoot)\include\libarchive
+
+ archive.lib
+ libarchive.lib
+ archive-13.lib
+ libarchive-13.lib
+ archive_static.lib
+ libarchive_static.lib
+
+ LIBARCHIVE_STATIC
+
+ $(Gtk3Lib);$(Gdk3Lib);wininet.lib;winmm.lib;ws2_32.lib;atk-1.0.lib;gio-2.0.lib;gdk_pixbuf-2.0.lib;pangowin32-1.0.lib;pangocairo-1.0.lib;pango-1.0.lib;cairo.lib;gobject-2.0.lib;gmodule-2.0.lib;glib-2.0.lib;$(IntlLib);$(IconvLib);$(ZlibLib);$(Xml2Lib);$(JpegLib);$(PngLib);$(ArchiveLib);$(OpenSslLibs)
$(SolutionDir)..\data\\
$(SolutionDir)..\..\zoitechat-build
@@ -160,10 +175,10 @@
true
true
true
- NTDDI_VERSION=NTDDI_WIN8;_WIN32_WINNT=_WIN32_WINNT_WIN8;%(PreProcessorDefinitions)
+ NTDDI_VERSION=NTDDI_WIN8;_WIN32_WINNT=_WIN32_WINNT_WIN8;$(ArchiveDefs);%(PreProcessorDefinitions)
- true
+ false
$(ZoiteChatLib)$(TargetName).lib
@@ -172,7 +187,7 @@
Debug
true
true
- UseLinkTimeCodeGeneration
+ Default