From 50346683a106ee5cd79cd2022c111345eee459df Mon Sep 17 00:00:00 2001 From: deepend Date: Wed, 4 Mar 2026 23:28:01 -0700 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20GTK3-only=20theming=20overhaul?= =?UTF-8?q?=E2=80=94new=20theme=20service=20(discover/import/inherits),=20?= =?UTF-8?q?layered=20CSS+settings.ini=20apply=20w/=20safe=20rollback=20+?= =?UTF-8?q?=20caching;=20widget/xtext=20palette=20mapping=20+=20all-colors?= =?UTF-8?q?=20editor;=20lots=20of=20win32/CI=20libarchive=20plumbing=20+?= =?UTF-8?q?=20installer=20assets;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/appimage-build.yml | 1 + .github/workflows/windows-build.yml | 64 +- data/misc/meson.build | 4 - data/misc/net.zoite.Zoitechat.desktop.in.in | 2 +- data/misc/net.zoite.Zoitechat.mime.xml | 8 - src/common/cfgfiles.c | 2 + src/common/common.vcxproj | 4 +- src/common/common.vcxproj.filters | 6 + src/common/gtk3-theme-service.c | 1152 ++++++++++++++++ src/common/gtk3-theme-service.h | 32 + src/common/meson.build | 22 +- src/common/outbound.c | 39 - src/common/tests/test-gtk3-theme-service.c | 684 ++++++++++ src/common/zoitechat.c | 296 +--- src/common/zoitechat.h | 4 +- src/fe-gtk/banlist.c | 2 + src/fe-gtk/dccgui.c | 8 - src/fe-gtk/fe-gtk.c | 145 +- src/fe-gtk/fe-gtk.vcxproj | 4 +- src/fe-gtk/fe-gtk.vcxproj.filters | 6 + src/fe-gtk/fkeys.c | 2 + src/fe-gtk/gtkutil.c | 6 + src/fe-gtk/ignoregui.c | 2 + src/fe-gtk/joind.c | 2 + src/fe-gtk/maingui.c | 6 +- src/fe-gtk/menu.c | 4 + src/fe-gtk/meson.build | 31 + src/fe-gtk/notifygui.c | 2 + src/fe-gtk/rawlog.c | 5 +- src/fe-gtk/servlistgui.c | 4 + src/fe-gtk/setup.c | 23 +- src/fe-gtk/textgui.c | 5 +- .../theme/tests/test-theme-access-routing.c | 113 ++ .../theme/tests/test-theme-gtk3-settings.c | 325 +++++ src/fe-gtk/theme/tests/test-theme-gtk3-stub.c | 71 + .../tests/test-theme-manager-auto-refresh.c | 43 + .../test-theme-manager-dispatch-routing.c | 11 + .../theme/tests/test-theme-manager-policy.c | 11 + .../test-theme-preferences-gtk3-populate.c | 280 ++++ .../tests/test-theme-runtime-persistence.c | 92 ++ src/fe-gtk/theme/theme-access.c | 53 + src/fe-gtk/theme/theme-access.h | 2 + src/fe-gtk/theme/theme-application.c | 52 +- src/fe-gtk/theme/theme-css.c | 55 +- src/fe-gtk/theme/theme-gtk3.c | 941 +++++++++++++ src/fe-gtk/theme/theme-gtk3.h | 22 + src/fe-gtk/theme/theme-manager.c | 232 +++- src/fe-gtk/theme/theme-manager.h | 1 + src/fe-gtk/theme/theme-preferences.c | 1186 ++++++++++++++--- src/fe-gtk/theme/theme-preferences.h | 4 +- src/fe-gtk/theme/theme-runtime.c | 132 +- src/fe-gtk/theme/theme-runtime.h | 14 + src/fe-gtk/xtext.c | 147 +- src/fe-gtk/xtext.h | 7 + win32/installer/zoitechat.iss.tt | 2 + win32/zoitechat.props | 23 +- 56 files changed, 5642 insertions(+), 754 deletions(-) create mode 100644 src/common/gtk3-theme-service.c create mode 100644 src/common/gtk3-theme-service.h create mode 100644 src/common/tests/test-gtk3-theme-service.c create mode 100644 src/fe-gtk/theme/tests/test-theme-gtk3-settings.c create mode 100644 src/fe-gtk/theme/tests/test-theme-gtk3-stub.c create mode 100644 src/fe-gtk/theme/tests/test-theme-preferences-gtk3-populate.c create mode 100644 src/fe-gtk/theme/theme-gtk3.c create mode 100644 src/fe-gtk/theme/theme-gtk3.h 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 From ca043be19710cd7f5f5974bea82ad65552ccabdc Mon Sep 17 00:00:00 2001 From: deepend Date: Sat, 7 Mar 2026 19:21:37 -0700 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20KDE=20Wayland=20alert=20flash?= =?UTF-8?q?=E2=80=94detect=20Plasma+Wayland,=20present()=20before=20urgenc?= =?UTF-8?q?y=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fe-gtk/maingui.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index 4729c060..7d37ac21 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -432,12 +432,47 @@ set_window_urgency (GtkWidget *win, gboolean set) gtk_window_set_urgency_hint (GTK_WINDOW (win), set); } +static gboolean +is_wayland_display (void) +{ + GdkDisplay *display = gdk_display_get_default (); + const char *name; + + if (!display) + return FALSE; + + name = gdk_display_get_name (display); + if (!name) + return FALSE; + + return g_str_has_prefix (name, "wayland"); +} + +static gboolean +is_kde_desktop (void) +{ + const char *desktop = g_getenv ("XDG_CURRENT_DESKTOP"); + + if (desktop && strstr (desktop, "KDE")) + return TRUE; + + return g_getenv ("KDE_FULL_SESSION") != NULL; +} + +static gboolean +is_kde_wayland (void) +{ + return is_wayland_display () && is_kde_desktop (); +} + static void flash_window (GtkWidget *win) { #ifdef HAVE_GTK_MAC gtkosx_application_attention_request (osx_app, INFO_REQUEST); #endif + if (is_kde_wayland ()) + gtk_window_present (GTK_WINDOW (win)); set_window_urgency (win, TRUE); } From f2354a7fa24d01f226ae8b3589aa5cb5665adc5d Mon Sep 17 00:00:00 2001 From: deepend Date: Sat, 7 Mar 2026 22:11:09 -0700 Subject: [PATCH 3/5] refactor: unify tree/xtext/input colors via ThemeWidgetStyleValues (canonical widget-style path) --- src/fe-gtk/chanview-tree.c | 15 +++------------ src/fe-gtk/chanview.c | 1 + src/fe-gtk/theme/theme-access.c | 30 +++++++++++++++++++++++++----- src/fe-gtk/theme/theme-css.c | 19 ++++++++----------- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/fe-gtk/chanview-tree.c b/src/fe-gtk/chanview-tree.c index 4b45cc7e..cc6ad08e 100644 --- a/src/fe-gtk/chanview-tree.c +++ b/src/fe-gtk/chanview-tree.c @@ -138,20 +138,11 @@ cv_tree_init (chanview *cv) gtk_widget_set_hexpand (view, TRUE); gtk_widget_set_vexpand (view, TRUE); gtk_widget_set_name (view, "zoitechat-tree"); - if ( - cv->font_desc - ) { - GdkRGBA bg; - GdkRGBA fg; - const GdkRGBA *bg_color = NULL; - const GdkRGBA *fg_color = NULL; + ThemeWidgetStyleValues style_values; - if (theme_get_color (THEME_TOKEN_TEXT_BACKGROUND, &bg)) - bg_color = &bg; - if (theme_get_color (THEME_TOKEN_TEXT_FOREGROUND, &fg)) - fg_color = &fg; - gtkutil_apply_palette (view, bg_color, fg_color, + theme_get_widget_style_values_for_widget (view, &style_values); + gtkutil_apply_palette (view, &style_values.background, &style_values.foreground, cv->font_desc); } /*gtk_widget_modify_base (view, GTK_STATE_NORMAL, &colors[THEME_LEGACY_TEXT_BACKGROUND]);*/ diff --git a/src/fe-gtk/chanview.c b/src/fe-gtk/chanview.c index ae789f94..6c43b546 100644 --- a/src/fe-gtk/chanview.c +++ b/src/fe-gtk/chanview.c @@ -29,6 +29,7 @@ #include "gtkutil.h" #include "chanview.h" #include "theme/theme-manager.h" +#include "theme/theme-access.h" /* treeStore columns */ #define COL_NAME 0 /* (char *) */ diff --git a/src/fe-gtk/theme/theme-access.c b/src/fe-gtk/theme/theme-access.c index a2d10613..517936a7 100644 --- a/src/fe-gtk/theme/theme-access.c +++ b/src/fe-gtk/theme/theme-access.c @@ -4,6 +4,13 @@ #include "theme-gtk3.h" + +enum +{ + THEME_XTEXT_FG_INDEX = 34, + THEME_XTEXT_BG_INDEX = 35 +}; + static gboolean theme_token_to_rgb16 (ThemeSemanticToken token, guint16 *red, guint16 *green, guint16 *blue) { @@ -115,12 +122,25 @@ 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) { - ThemeGtkPaletteMap gtk_map = { 0 }; + ThemeWidgetStyleValues style_values; - if (theme_access_get_gtk_palette_map (widget, >k_map)) - { - theme_runtime_get_xtext_colors_mapped (>k_map, palette, palette_len); + if (!palette) return; - } + + theme_get_widget_style_values_for_widget (widget, &style_values); theme_runtime_get_xtext_colors (palette, palette_len); + if (palette_len > THEME_XTEXT_FG_INDEX) + { + palette[THEME_XTEXT_FG_INDEX].red = style_values.foreground.red; + palette[THEME_XTEXT_FG_INDEX].green = style_values.foreground.green; + palette[THEME_XTEXT_FG_INDEX].blue = style_values.foreground.blue; + palette[THEME_XTEXT_FG_INDEX].alpha = style_values.foreground.alpha; + } + if (palette_len > THEME_XTEXT_BG_INDEX) + { + palette[THEME_XTEXT_BG_INDEX].red = style_values.background.red; + palette[THEME_XTEXT_BG_INDEX].green = style_values.background.green; + palette[THEME_XTEXT_BG_INDEX].blue = style_values.background.blue; + palette[THEME_XTEXT_BG_INDEX].alpha = style_values.background.alpha; + } } diff --git a/src/fe-gtk/theme/theme-css.c b/src/fe-gtk/theme/theme-css.c index ca59056e..d8480e05 100644 --- a/src/fe-gtk/theme/theme-css.c +++ b/src/fe-gtk/theme/theme-css.c @@ -2,6 +2,7 @@ #include "theme-runtime.h" #include "theme-gtk3.h" +#include "theme-access.h" #include "../gtkutil.h" #include @@ -169,18 +170,14 @@ theme_css_reload_input_style (gboolean enabled, const PangoFontDescription *font next.theme_name = g_strdup (theme_name); { - GdkRGBA color; + ThemeWidgetStyleValues style_values; - if (theme_runtime_get_color (THEME_TOKEN_TEXT_FOREGROUND, &color)) - { - theme_palette_color_get_rgb16 (&color, &next.fg_red, &next.fg_green, &next.fg_blue); - next.colors_set = TRUE; - } - if (theme_runtime_get_color (THEME_TOKEN_TEXT_BACKGROUND, &color)) - { - theme_palette_color_get_rgb16 (&color, &next.bg_red, &next.bg_green, &next.bg_blue); - next.colors_set = TRUE; - } + theme_get_widget_style_values_for_widget (NULL, &style_values); + theme_palette_color_get_rgb16 (&style_values.foreground, + &next.fg_red, &next.fg_green, &next.fg_blue); + theme_palette_color_get_rgb16 (&style_values.background, + &next.bg_red, &next.bg_green, &next.bg_blue); + next.colors_set = TRUE; } if (theme_css_input_fingerprint_matches (&next)) From b47c45d4cc235d3a6c16624dfd657a71ba7fd8bc Mon Sep 17 00:00:00 2001 From: deepend Date: Sun, 8 Mar 2026 10:10:59 -0600 Subject: [PATCH 4/5] feat: merge GTK3 Theme into Appearance (new Advanced section); fix+scale chat bg image + use FileChooserNative picker --- src/fe-gtk/pixmaps.c | 32 ++++++++++ src/fe-gtk/setup.c | 116 +++++++++++++++++++++++++++++----- src/fe-gtk/theme/theme-gtk3.c | 5 +- src/fe-gtk/xtext.c | 83 +++++++++++++++++++----- 4 files changed, 203 insertions(+), 33 deletions(-) diff --git a/src/fe-gtk/pixmaps.c b/src/fe-gtk/pixmaps.c index f786670b..7b506065 100644 --- a/src/fe-gtk/pixmaps.c +++ b/src/fe-gtk/pixmaps.c @@ -118,11 +118,43 @@ pixmap_load_from_file_real (char *file) { GdkPixbuf *img; cairo_surface_t *surface; + int width; + int height; + const int max_dimension = 4096; img = gdk_pixbuf_new_from_file (file, 0); if (!img) return NULL; + width = gdk_pixbuf_get_width (img); + height = gdk_pixbuf_get_height (img); + if (width > max_dimension || height > max_dimension) + { + GdkPixbuf *scaled; + double scale; + int target_width; + int target_height; + + if (width >= height) + scale = (double)max_dimension / (double)width; + else + scale = (double)max_dimension / (double)height; + + target_width = (int)(width * scale); + target_height = (int)(height * scale); + if (target_width < 1) + target_width = 1; + if (target_height < 1) + target_height = 1; + + scaled = gdk_pixbuf_scale_simple (img, target_width, target_height, GDK_INTERP_BILINEAR); + if (scaled) + { + g_object_unref (img); + img = scaled; + } + } + surface = pixbuf_to_cairo_surface (img); g_object_unref (img); diff --git a/src/fe-gtk/setup.c b/src/fe-gtk/setup.c index 37554c23..9d015e80 100644 --- a/src/fe-gtk/setup.c +++ b/src/fe-gtk/setup.c @@ -161,10 +161,6 @@ static const setting appearance_settings[] = {ST_TOGGLE, N_("Colored nick names"), P_OFFINTNL(hex_text_color_nicks), N_("Give each person on IRC a different color"),0,0}, {ST_TOGGLR, N_("Indent nick names"), P_OFFINTNL(hex_text_indent), N_("Make nick names right-justified"),0,0}, {ST_TOGGLE, N_ ("Show marker line"), P_OFFINTNL (hex_text_show_marker), N_ ("Insert a red line after the last read text."), 0, 0}, - {ST_EFILE, N_ ("Background image:"), P_OFFSETNL (hex_text_background), 0, 0, sizeof prefs.hex_text_background}, - - {ST_HEADER, N_("Transparency Settings"), 0,0,0}, - {ST_HSCALE, N_("Window opacity:"), P_OFFINTNL(hex_gui_transparency),0,0,0}, {ST_HEADER, N_("Timestamps"),0,0,0}, {ST_TOGGLE, N_("Enable timestamps"), P_OFFINTNL(hex_stamp_text),0,0,1}, @@ -183,6 +179,15 @@ static const setting appearance_settings[] = {ST_END, 0, 0, 0, 0, 0} }; +static const setting appearance_advanced_settings[] = +{ + {ST_HEADER, N_("Advanced"),0,0,0}, + {ST_EFILE, N_ ("Background image:"), P_OFFSETNL (hex_text_background), 0, 0, sizeof prefs.hex_text_background}, + {ST_HSCALE, N_("Window opacity:"), P_OFFINTNL(hex_gui_transparency),0,0,0}, + + {ST_END, 0, 0, 0, 0, 0} +}; + static const char *const tabcompmenu[] = { N_("A-Z"), @@ -1141,22 +1146,64 @@ setup_filereq_cb (GtkWidget *entry, char *file) } } +static void +setup_browsefile_response_cb (GtkNativeDialog *dialog, gint response, gpointer user_data) +{ + GtkWidget *entry = user_data; + + if (response == GTK_RESPONSE_ACCEPT) + { + char *file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); + setup_filereq_cb (entry, file); + g_free (file); + } + + g_object_unref (dialog); +} + static void setup_browsefile_cb (GtkWidget *button, GtkWidget *entry) { - /* used for background image only */ - char *filter; - int filter_type; + GtkFileChooserNative *dialog; + GtkFileFilter *filefilter; + const char *current; + char *dirname; + (void)button; + dialog = gtk_file_chooser_native_new (_("Select an Image File"), + GTK_WINDOW (setup_window), + GTK_FILE_CHOOSER_ACTION_OPEN, + _("_Open"), + _("_Cancel")); + gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (dialog), TRUE); + + current = gtk_entry_get_text (GTK_ENTRY (entry)); + if (current && current[0]) + { + dirname = g_path_get_dirname (current); + if (dirname && dirname[0] && g_file_test (dirname, G_FILE_TEST_IS_DIR)) + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), dirname); + else if (g_file_test (current, G_FILE_TEST_IS_DIR)) + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), current); + g_free (dirname); + } + + filefilter = gtk_file_filter_new (); #ifdef WIN32 - filter = "*png;*.tiff;*.gif;*.jpeg;*.jpg"; - filter_type = FRF_EXTENSIONS; + gtk_file_filter_add_pattern (filefilter, "*.png"); + gtk_file_filter_add_pattern (filefilter, "*.tiff"); + gtk_file_filter_add_pattern (filefilter, "*.gif"); + gtk_file_filter_add_pattern (filefilter, "*.jpeg"); + gtk_file_filter_add_pattern (filefilter, "*.jpg"); #else - filter = "image/*"; - filter_type = FRF_MIMETYPES; + gtk_file_filter_add_mime_type (filefilter, "image/*"); #endif - gtkutil_file_req (GTK_WINDOW (setup_window), _("Select an Image File"), setup_filereq_cb, - entry, NULL, filter, filter_type|FRF_RECENTLYUSED|FRF_MODAL); + gtk_file_filter_set_name (filefilter, _("Images")); + gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filefilter); + + g_signal_connect (G_OBJECT (dialog), "response", + G_CALLBACK (setup_browsefile_response_cb), entry); + gtk_native_dialog_show (GTK_NATIVE_DIALOG (dialog)); } @@ -1442,6 +1489,41 @@ setup_create_theme_page (void) &color_change); } +static GtkWidget * +setup_create_appearance_page (void) +{ + GtkWidget *box; + GtkWidget *appearance_page; + GtkWidget *theme_label; + GtkWidget *theme_page; + GtkWidget *advanced_page; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + appearance_page = setup_create_page (appearance_settings); + theme_label = gtk_label_new (NULL); + theme_page = setup_create_theme_page (); + advanced_page = setup_create_page (appearance_advanced_settings); + + { + char *markup = g_markup_printf_escaped ("%s", _("GTK3 Theme")); + gtk_label_set_markup (GTK_LABEL (theme_label), markup); + g_free (markup); + } + gtk_widget_set_halign (theme_label, GTK_ALIGN_START); + gtk_widget_set_valign (theme_label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_start (theme_label, 2); + gtk_widget_set_margin_end (theme_label, 2); + gtk_widget_set_margin_top (theme_label, 1); + gtk_widget_set_margin_bottom (theme_label, 1); + + gtk_box_pack_start (GTK_BOX (box), appearance_page, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), theme_label, FALSE, FALSE, 2); + gtk_box_pack_start (GTK_BOX (box), theme_page, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (box), advanced_page, FALSE, FALSE, 0); + + return box; +} + /* === GLOBALS for sound GUI === */ static GtkWidget *sndfile_entry; @@ -1729,7 +1811,6 @@ static const char *const cata_interface[] = N_("Input box"), N_("User list"), N_("Channel switcher"), - N_("GTK3 Theme"), NULL }; @@ -1755,15 +1836,15 @@ static GtkWidget * setup_create_pages (GtkWidget *box) { GtkWidget *book; - GtkWindow *win = GTK_WINDOW(gtk_widget_get_toplevel (box)); + GtkWindow *win = GTK_WINDOW (setup_window); + (void)box; book = gtk_notebook_new (); - setup_add_page (cata_interface[0], book, setup_create_page (appearance_settings)); + setup_add_page (cata_interface[0], book, setup_create_appearance_page ()); setup_add_page (cata_interface[1], book, setup_create_page (inputbox_settings)); 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_chatting[0], book, setup_create_page (general_settings)); @@ -2124,6 +2205,7 @@ setup_window_open (void) g_snprintf(buf, sizeof(buf), _("Preferences - %s"), _(DISPLAY_NAME)); win = gtkutil_window_new (buf, "prefs", 0, 600, 2); + setup_window = win; vbox = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 5); gtk_container_set_border_width (GTK_CONTAINER (vbox), 6); diff --git a/src/fe-gtk/theme/theme-gtk3.c b/src/fe-gtk/theme/theme-gtk3.c index 5d31a435..847e7ded 100644 --- a/src/fe-gtk/theme/theme-gtk3.c +++ b/src/fe-gtk/theme/theme-gtk3.c @@ -689,12 +689,15 @@ settings_apply_from_file (const char *theme_root, const char *css_dir) for (i = 0; keys && i < n_keys; i++) { char *raw_value; + char *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); + value = g_strstrip (raw_value); + if (value[0] != '\0') + settings_apply_property (settings, keys[i], value); g_free (raw_value); } diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 4229651b..448e3ace 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -364,20 +364,30 @@ xtext_draw_bg_offset (GtkXText *xtext, int x, int y, int width, int height, int if (xtext->background_surface) { - 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; + GtkAllocation allocation; + int clip_x; + int clip_y; + int clip_w; + int clip_h; - if (clip_w < 1 || clip_h < 1) + if (cairo_surface_status (xtext->background_surface) != CAIRO_STATUS_SUCCESS) { - GtkAllocation allocation; + xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height); + cairo_destroy (cr); + return; + } - gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation); - clip_x = 0; - clip_y = 0; - clip_w = allocation.width; - clip_h = allocation.height; + gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation); + clip_x = 0; + clip_y = 0; + clip_w = allocation.width; + clip_h = allocation.height; + + if (clip_w < 1 || clip_h < 1 || clip_w > 8192 || clip_h > 8192) + { + xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height); + cairo_destroy (cr); + return; } if (xtext->background_clip_surface == NULL || @@ -396,11 +406,54 @@ xtext_draw_bg_offset (GtkXText *xtext, int x, int y, int width, int height, int } xtext->background_clip_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, clip_w, clip_h); + if (cairo_surface_status (xtext->background_clip_surface) != CAIRO_STATUS_SUCCESS) + { + cairo_surface_destroy (xtext->background_clip_surface); + xtext->background_clip_surface = NULL; + xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height); + cairo_destroy (cr); + return; + } 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); + if (cairo_surface_get_type (xtext->background_surface) == CAIRO_SURFACE_TYPE_IMAGE) + { + int src_w = cairo_image_surface_get_width (xtext->background_surface); + int src_h = cairo_image_surface_get_height (xtext->background_surface); + if (src_w > 0 && src_h > 0) + { + double scale_x = (double)clip_w / (double)src_w; + double scale_y = (double)clip_h / (double)src_h; + double scale = scale_x < scale_y ? scale_x : scale_y; + double draw_w = src_w * scale; + double draw_h = src_h * scale; + double draw_x = ((double)clip_w - draw_w) / 2.0; + double draw_y = ((double)clip_h - draw_h) / 2.0; + cairo_set_source_rgb (bg_cr, 0.0, 0.0, 0.0); + cairo_paint (bg_cr); + cairo_save (bg_cr); + cairo_translate (bg_cr, draw_x, draw_y); + cairo_scale (bg_cr, scale, scale); + cairo_set_source_surface (bg_cr, xtext->background_surface, 0.0, 0.0); + cairo_pattern_set_extend (cairo_get_source (bg_cr), CAIRO_EXTEND_NONE); + cairo_rectangle (bg_cr, 0.0, 0.0, (double)src_w, (double)src_h); + cairo_fill (bg_cr); + cairo_restore (bg_cr); + } + else + { + 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); + } + } + else + { + 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; From a53802cfef9ba29398e64184fc4862fb59402f0d Mon Sep 17 00:00:00 2001 From: deepend Date: Sun, 8 Mar 2026 10:40:20 -0600 Subject: [PATCH 5/5] fix(win): tolerate tar/PS nonzero if files extracted; add path_tree_has_entries() to verify real output --- src/common/gtk3-theme-service.c | 102 +++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/src/common/gtk3-theme-service.c b/src/common/gtk3-theme-service.c index 147981cf..d15824e3 100644 --- a/src/common/gtk3-theme-service.c +++ b/src/common/gtk3-theme-service.c @@ -55,6 +55,38 @@ remove_tree (const char *path) g_rmdir (path); } +static gboolean +path_tree_has_entries (const char *path) +{ + GDir *dir; + const char *name; + + if (!path || !g_file_test (path, G_FILE_TEST_EXISTS)) + return FALSE; + + if (!g_file_test (path, G_FILE_TEST_IS_DIR)) + return TRUE; + + dir = g_dir_open (path, 0, NULL); + if (!dir) + return FALSE; + + while ((name = g_dir_read_name (dir)) != NULL) + { + char *child = g_build_filename (path, name, NULL); + gboolean has_entries = path_tree_has_entries (child); + g_free (child); + if (has_entries) + { + g_dir_close (dir); + return TRUE; + } + } + + g_dir_close (dir); + return FALSE; +} + static gboolean gtk3_css_dir_parse_minor (const char *name, gint *minor) { @@ -268,8 +300,8 @@ resolve_parent_theme_root (const char *child_theme_root, const char *parent_name static void build_inheritance_chain_recursive (const char *theme_root, - GPtrArray *ordered_roots, - GHashTable *visited) + GPtrArray *ordered_roots, + GHashTable *visited) { char **parents; guint i; @@ -365,7 +397,7 @@ path_read_display_name (const char *root) 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)) + 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) @@ -475,8 +507,8 @@ path_normalize_theme_root (const char *path) { char *base = g_path_get_dirname (canonical); char *resolved = g_path_is_absolute (target) - ? g_strdup (target) - : g_build_filename (base, target, NULL); + ? g_strdup (target) + : g_build_filename (base, target, NULL); g_free (canonical); canonical = g_canonicalize_filename (resolved, NULL); g_free (resolved); @@ -774,8 +806,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) 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); + "Invalid GTK3 theme at '%s': missing required index.theme at '%s'.", + theme_root, index_theme); g_free (index_theme); return FALSE; } @@ -784,8 +816,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) 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); + "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); @@ -795,8 +827,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) 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); + "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; @@ -806,8 +838,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) 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); + "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; @@ -817,8 +849,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) 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); + "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); @@ -849,8 +881,8 @@ validate_theme_root_for_import (const char *theme_root, GError **error) 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); + "Invalid GTK3 theme at '%s': parent theme '%s' from Inherits could not be resolved.", + theme_root, parent_name); g_strfreev (inherits); return FALSE; } @@ -865,7 +897,7 @@ static char * extract_archive (const char *source, GError **error) { char *tmp = g_dir_make_tmp ("zoitechat-gtk3-theme-XXXXXX", error); -#ifdef G_OS_WIN32 + #ifdef G_OS_WIN32 char *stdout_text = NULL; char *stderr_text = NULL; char *system_tar = NULL; @@ -895,7 +927,7 @@ extract_archive (const char *source, GError **error) }; extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, - &stdout_text, &stderr_text, &status, NULL); + &stdout_text, &stderr_text, &status, NULL); } else { @@ -927,7 +959,7 @@ extract_archive (const char *source, GError **error) }; extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, - &stdout_text, &stderr_text, &status, NULL); + &stdout_text, &stderr_text, &status, NULL); } else { @@ -941,25 +973,37 @@ extract_archive (const char *source, GError **error) }; extracted = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, - &stdout_text, &stderr_text, &status, 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) + if (!extracted || (status != 0 && !path_tree_has_entries (tmp))) { + g_free (tar_program); + g_free (system_tar); + g_free (system_root); + g_free (stdout_text); + g_free (stderr_text); 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; } + /* + * Windows archive tools often return a non-zero exit status for Unix-style + * symlink entries that cannot be materialized without extra privileges. + * If regular theme files were extracted, continue and let theme validation + * decide whether the imported theme is usable. + */ + g_free (tar_program); + g_free (system_tar); + g_free (system_root); + g_free (stdout_text); + g_free (stderr_text); + return tmp; -#else + #else struct archive *archive = NULL; struct archive *disk = NULL; struct archive_entry *entry; @@ -1046,7 +1090,7 @@ extract_archive (const char *source, GError **error) } return tmp; -#endif + #endif } gboolean