Compare commits

..
Author SHA1 Message Date
deepend 58208b81f2 Fix away tracking
(cherry picked from commit 87410aea50)
2026-09-15 13:04:10 -06:00
deepend e7631701ce release: prepare 2.19.1 2026-09-15 09:28:29 -06:00
deepend b5c3be8ff1 defer channel tree scrolling until realized
(cherry picked from commit e94ffd9d79)
2026-09-15 09:12:34 -06:00
deepend 2b9a39783b fix theme service tests
(cherry picked from commit ff4217c3d1)
2026-09-15 09:12:30 -06:00
Christopher Byrne ecdb462ff8 meson.build: Reflect usage of 0.59 Meson features
src/fe-gtk/meson.build calls feature_opt.allowed() which wasn't
added until 0.59.

Signed-off-by: Christopher Byrne <[email protected]>
(cherry picked from commit 5018a3a848)
2026-09-15 09:12:25 -06:00
Christopher Byrne 78eb1882cb data/misc/meson.build: Fix referencing possibly undefined variable "appstreamcli"
If meson is passed -Dinstall-plugin-metainfo=false, the "appstreamcli"
will not be defined, but it is later referenced in global scope, which
causes and "undefined variable" error in Meson

Signed-off-by: Christopher Byrne <[email protected]>
(cherry picked from commit df1c74a91d)
2026-09-15 09:12:21 -06:00
deepend 6c95db8539 Fix channel ACTION routing after self-message handling
(cherry picked from commit bb3893d36c)
2026-09-15 09:12:16 -06:00
ThePeGaSuS 75286642df Fix CTCP ACTION not rerouting self-sent /me to correct PM window
Mirrors the PRIVMSG fix: replayed /me messages sent by our own nick
via WeeChat relay backlog were always passed with fromme=FALSE,
so they never got rerouted to the actual PM query window.

(cherry picked from commit c725adaa09)
2026-09-15 09:12:13 -06:00
ThePeGaSuS 8044b8562d Fix: private messages split into two windows over WeeChat's IRC relay
When you send a private message, ZoiteChat sometimes gets the same
message echoed back by the server so it can show it in the right
window. Whether it recognizes that echo depended on the server first
confirming a feature called "echo-message" during connection setup.

WeeChat's relay doesn't seem to confirm that feature the way other
servers (or ZNC) do, so ZoiteChat didn't recognize its own echoed
messages and treated them as brand new incoming messages from
yourself, opening a second, separate conversation window instead of
using the one you were already typing in.

This makes ZoiteChat recognize its own message by checking who sent
it, instead of only trusting that feature confirmation. That way,
sent and received messages always end up in the same conversation
window, regardless of how well the server/relay announces its
capabilities.

(cherry picked from commit 0ba523d071)
2026-09-15 09:12:09 -06:00
deepend de6e16cbdf fix startup error on flatpak
(cherry picked from commit 36f0d7450c)
2026-09-15 09:12:02 -06:00
deepend 909309cc2e WIN32: fix installer theme download URL. 2026-08-15 12:49:58 -06:00
deepend-tildeclub 73d958911f Merge pull request #364 from ZoiteChat/master
run Ci for release branches
2026-08-15 12:10:29 -06:00
deepend 6b4a2b5406 fix unmark away option 2026-08-15 12:07:41 -06:00
deepend cd8d1c9b10 Run Ci on Release/version branches on commit 2026-08-04 12:11:30 -06:00
deepend-tildeclub a79fedda77 Merge pull request #360 from ZoiteChat/release/2.19
fix duplicate /msg output + Escaping of the intial slash
2026-08-04 12:10:53 -06:00
deepend 558ef7fbdf fix Escaping of the intial slash 2026-08-04 11:58:30 -06:00
deepend 7c6609a414 fix duplicate /msg output 2026-08-04 11:35:48 -06:00
deepend 54514f494f Version bump for 2.19.0 Release. 2026-07-28 10:26:04 -06:00
deepend-tildeclub 67aa7ca79f Merge pull request #355 from ZoiteChat/centralize_version_management
Centralize release version in root VERSION file.
2026-07-28 09:31:12 -06:00
deepend 344f5af01b Centralize release version in root VERSION file. 2026-07-28 09:12:53 -06:00
deepend-tildeclub 8150d6c48c Merge pull request #351 from ZoiteChat/reduce-client-restarts
Reduce needed client restarts for settings changes.
2026-07-19 17:02:04 -06:00
deepend 05ee0dbdb5 Reduce needed client restarts for settings changes. 2026-07-16 08:48:33 -06:00
deepend-tildeclub 2db3076c92 Merge pull request #350 from ZoiteChat/fix-gtk3-quirks
fix a few GTK3 theming quirks
2026-07-16 02:29:43 -06:00
deepend 90daed18df fix a few GTK3 theming quirks 2026-07-15 23:56:05 -06:00
deepend-tildeclub 65dd2c645a Merge pull request #345 from ZoiteChat/gtk3-consistency-fixes
theming: respect system theme by default
2026-07-14 14:27:08 -06:00
deepend 89d70a940c theming: respect system theme by default 2026-07-13 13:26:55 -06:00
deepend-tildeclub 4944517a0e Merge pull request #344 from ZoiteChat/ask_sasl_password_reconnect
Prompt SASL password again on reconnect
2026-07-13 09:53:41 -06:00
deepend 2a7ee7f0b2 Prompt SASL password again on reconnect 2026-07-13 09:18:19 -06:00
deepend-tildeclub 6edbd47c9a Merge pull request #342 from ZoiteChat/sasl_password_prompt
Add manual SASL password prompt
2026-07-10 05:12:19 -06:00
deepend d755c39e4a Add manual SASL password prompt 2026-07-09 12:50:13 -06:00
deepend-tildeclub b7f8327209 Merge pull request #341 from ZoiteChat/theme_manager_get_channel_tree_palette_behavior
Use owned channel tree font for palette behavior
2026-07-09 10:57:13 -06:00
deepend 27c82d8411 Use owned channel tree font for palette behavior 2026-07-09 10:38:52 -06:00
deepend e0b159c069 Remove Manjaro package. Handled seperately. 2026-07-08 12:24:08 -06:00
deepend-tildeclub cfb19820fa Merge pull request #330 from ZoiteChat/Windows/python38-support
Add Python 3.8 support for older windows versions.
2026-07-08 10:00:31 -06:00
deepend-tildeclub e05ca13608 Merge pull request #335 from ZoiteChat/color-tooltips
Add mouse over tooltips for each color selection.
2026-07-08 09:44:10 -06:00
deepend-tildeclub 15cf96c01f Merge pull request #336 from ZoiteChat/win32-taskbar-fix
Fix Windows taskbar click not minimizing the window
2026-07-08 09:31:04 -06:00
deepend 2caec91a0f Improve color tooltip design 2026-07-08 00:58:34 -06:00
deepend 9465a1e0a8 Fix Windows taskbar click not minimizing the window 2026-07-07 23:44:22 -06:00
deepend 251272c708 Applying: Fix Windows 7 crash after connecting when Python plugin init fails 2026-07-07 19:04:51 -06:00
deepend f068a0e0fa Fix Windows Python plugin DLL selection 2026-07-07 15:34:11 -06:00
deepend d0034cd112 Fixed missing comma in color tooltips 2026-07-07 14:14:36 -06:00
deepend ae6d5ebeef Add mouse over tooltips for each color selection. 2026-07-07 09:08:00 -06:00
deepend c3d8f27f48 Add Python 3.8 support for older windows versions. 2026-07-06 01:37:59 -06:00
59 changed files with 2145 additions and 380 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: AppImage Build
on: on:
push: push:
branches: [master] branches: [master, 'release/**']
pull_request: pull_request:
branches: [master] branches: [master]
+1 -1
View File
@@ -2,7 +2,7 @@ name: Flatpak Build
on: on:
push: push:
branches: [master] branches: [master, 'release/**']
pull_request: pull_request:
branches: [master] branches: [master]
@@ -1,79 +0,0 @@
name: Manjaro Package Build
on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
workflow_dispatch:
permissions:
contents: read
jobs:
manjaro-package:
runs-on: ubuntu-latest
container:
image: manjarolinux/base:latest
steps:
- name: Install packaging toolchain
run: |
set -euxo pipefail
pacman -Syu --noconfirm
pacman -S --noconfirm --needed \
base-devel \
git \
namcap \
meson \
ninja \
pkgconf \
glib2 \
glib2-devel \
gtk3 \
openssl \
libcanberra \
libayatana-appindicator \
iso-codes \
lua \
perl \
python \
python-cffi \
pciutils
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: true
- name: Build package and generate .SRCINFO (non-root)
run: |
set -euxo pipefail
id -u builder >/dev/null 2>&1 || useradd -m builder
chown -R builder:builder "$GITHUB_WORKSPACE"
su builder -c "cd '$GITHUB_WORKSPACE/packaging/manjaro' && makepkg --noconfirm --syncdeps --cleanbuild"
su builder -c "cd '$GITHUB_WORKSPACE/packaging/manjaro' && makepkg --printsrcinfo > .SRCINFO"
- name: Lint PKGBUILD and package with namcap
run: |
set -euxo pipefail
cd "$GITHUB_WORKSPACE/packaging/manjaro"
namcap PKGBUILD
pkg_file="$(find . -maxdepth 1 -type f -name '*.pkg.tar.*' | head -n1)"
namcap "$pkg_file"
- name: Collect artifacts
run: |
set -euxo pipefail
mkdir -p artifacts
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/*.pkg.tar.* artifacts/
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/PKGBUILD artifacts/
cp -v "$GITHUB_WORKSPACE"/packaging/manjaro/.SRCINFO artifacts/
- name: Upload package artifacts
uses: actions/upload-artifact@v6
with:
name: zoitechat-manjaro-package
path: artifacts/*
if-no-files-found: error
+42
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- master - master
- 'release/**'
pull_request: pull_request:
branches: branches:
- master - master
@@ -113,6 +114,26 @@ jobs:
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
} }
- uses: actions/setup-python@v6
with:
python-version: '3.8.10'
architecture: ${{ matrix.arch }}
- name: Prepare Python 3.8
run: |
python -m pip install --upgrade pip
python -m pip install cffi
$pyRoot = $env:pythonLocation
if (-not $pyRoot) { $pyRoot = & python -c "import sys; print(sys.prefix)" }
foreach ($pyDir in @("C:\gtk-build\python-3.8.10", "C:\gtk-build\python-3.8")) {
New-Item -Path $pyDir -ItemType Directory -Force | Out-Null
$target = Join-Path $pyDir "${{ matrix.platform }}"
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
New-Item -Path $pyDir -Name "${{ matrix.platform }}" -ItemType Junction -Value $pyRoot | Out-Null
}
- name: Build - name: Build
run: | run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -124,12 +145,33 @@ jobs:
exit /b 1 exit /b 1
) )
set "PYTHON38_DIR=C:\gtk-build\python-3.8\${{ matrix.platform }}"
if not exist "%PYTHON38_DIR%\libs\python38.lib" (
echo Missing %PYTHON38_DIR%\libs\python38.lib
dir "%PYTHON38_DIR%\libs"
exit /b 1
)
if not exist "%PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd" (
echo Missing %PYTHON38_DIR%\Lib\site-packages\_cffi_backend.cp38*.pyd
exit /b 1
)
set "LIB=%PYTHON_DIR%\libs;%LIB%" set "LIB=%PYTHON_DIR%\libs;%LIB%"
set "INCLUDE=%PYTHON_DIR%\include;%INCLUDE%" 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 } }" 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 }} msbuild win32\zoitechat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
if errorlevel 1 exit /b 1
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\plugins\hcpython38.dll" (
echo hcpython38.dll was not built
exit /b 1
)
if not exist "..\zoitechat-build\${{ matrix.platform }}\rel\_cffi_backend.cp38*.pyd" (
echo _cffi_backend for Python 3.8 was not staged for the installer
exit /b 1
)
shell: cmd shell: cmd
- name: Preparing Artifacts - name: Preparing Artifacts
+1
View File
@@ -0,0 +1 @@
2.19.1
+14
View File
@@ -1,6 +1,20 @@
ZoiteChat ChangeLog ZoiteChat ChangeLog
================= =================
2.19.0 (2026-07-28)
-------------------
- Added Python 3.8 compatibility for older Windows releases.
- Added on-demand SASL password prompts, including renewed prompts after reconnecting.
- Corrected NickServ authentication to use the decrypted password for the active network.
- Made system theme matching the default and polished GTK3 theme integration.
- Applied more settings changes immediately, reducing the need to restart ZoiteChat.
- Added descriptive tooltips to the color picker choices and refined default text-event colors.
- Stabilized channel tree font handling across palette and theme updates.
- Corrected the chat scrollbar increment used by its arrow buttons.
- Improved Windows URL launching through the native shell.
- Restored expected minimize behavior when clicking ZoiteChat on the Windows taskbar.
2.18.3 (2026-06-29) 2.18.3 (2026-06-29)
------------------- -------------------
+1 -1
View File
@@ -2,6 +2,7 @@ appdir = join_paths(get_option('datadir'), 'applications')
metainfodir = join_paths(get_option('datadir'), 'metainfo') metainfodir = join_paths(get_option('datadir'), 'metainfo')
mimedir = join_paths(get_option('datadir'), 'mime', 'packages') mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
desktop_utils = find_program('desktop-file-validate', required: false) desktop_utils = find_program('desktop-file-validate', required: false)
appstreamcli = find_program('appstreamcli', required: false)
if get_option('gtk-frontend') if get_option('gtk-frontend')
if get_option('install-appdata') if get_option('install-appdata')
@@ -13,7 +14,6 @@ if get_option('gtk-frontend')
install_dir: metainfodir install_dir: metainfodir
) )
appstreamcli = find_program('appstreamcli', required: false)
if appstreamcli.found() if appstreamcli.found()
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli, test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
args: ['validate', zoitechat_appdata] args: ['validate', zoitechat_appdata]
@@ -29,6 +29,22 @@
<id>zoitechat.desktop</id> <id>zoitechat.desktop</id>
</provides> </provides>
<releases> <releases>
<release date="2026-07-28" version="2.19.0">
<description>
<ul>
<li>Added Python 3.8 compatibility for older Windows releases.</li>
<li>Added on-demand SASL password prompts, including renewed prompts after reconnecting.</li>
<li>Corrected NickServ authentication to use the decrypted password for the active network.</li>
<li>Made system theme matching the default and polished GTK3 theme integration.</li>
<li>Applied more settings changes immediately, reducing the need to restart ZoiteChat.</li>
<li>Added descriptive tooltips to the color picker choices and refined default text-event colors.</li>
<li>Stabilized channel tree font handling across palette and theme updates.</li>
<li>Corrected the chat scrollbar increment used by its arrow buttons.</li>
<li>Improved Windows URL launching through the native shell.</li>
<li>Restored expected minimize behavior when clicking ZoiteChat on the Windows taskbar.</li>
</ul>
</description>
</release>
<release date="2026-06-29" version="2.18.3"> <release date="2026-06-29" version="2.18.3">
<description> <description>
<ul> <ul>
+2 -2
View File
@@ -1,6 +1,6 @@
project('zoitechat', 'c', project('zoitechat', 'c',
version: '2.18.3', version: run_command('cat', 'VERSION', check: true).stdout().strip(),
meson_version: '>= 0.55.0', meson_version: '>= 0.59.0',
default_options: [ default_options: [
'c_std=c17', 'c_std=c17',
'buildtype=debugoptimized', 'buildtype=debugoptimized',
+1 -1
View File
@@ -1,5 +1,5 @@
Name: zoitechat Name: zoitechat
Version: 2.18.3 Version: %(cat VERSION)
Release: %autorelease Release: %autorelease
Summary: HexChat-based IRC client Summary: HexChat-based IRC client
License: GPL-2.0-or-later WITH cryptsetup-OpenSSL-exception License: GPL-2.0-or-later WITH cryptsetup-OpenSSL-exception
-69
View File
@@ -1,69 +0,0 @@
# Maintainer: ZoiteChat Maintainers <[email protected]>
pkgname=zoitechat-git
pkgver=0
pkgrel=1
pkgdesc='IRC client for GNOME (ZoiteChat development snapshot)'
arch=('x86_64')
url='https://github.com/zoitechat/zoitechat'
license=('GPL-2.0-or-later')
depends=(
'glib2'
'gtk3'
'iso-codes'
'libayatana-appindicator'
'libcanberra'
'lua'
'openssl'
'perl'
'python-cffi'
)
makedepends=(
'git'
'meson'
'ninja'
'pkgconf'
'python'
)
optdepends=(
'pciutils: sysinfo plugin hardware detection details'
)
provides=('zoitechat')
conflicts=('zoitechat')
_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source=("zoitechat::git+file://${_repo_root}")
sha256sums=('SKIP')
pkgver() {
cd "$srcdir/zoitechat"
if git describe --long --tags --abbrev=7 >/dev/null 2>&1; then
git describe --long --tags --abbrev=7 \
| sed 's/^v//' \
| sed -E 's/([^-]+)-([0-9]+)-g/\1.r\2.g/' \
| sed 's/-/./g'
else
printf '0.r%s.%s\n' \
"$(git rev-list --count HEAD)" \
"$(git rev-parse --short=7 HEAD)"
fi
}
build() {
arch-meson zoitechat build \
-Dtext-frontend=false \
-Dwith-checksum=true \
-Dwith-fishlim=true \
-Dwith-lua=lua \
-Dwith-perl=perl \
-Dwith-python=python3 \
-Dwith-sysinfo=true \
-Dinstall-plugin-metainfo=true
meson compile -C build
}
package() {
meson install -C build --destdir "$pkgdir"
}
+24
View File
@@ -53,6 +53,16 @@ builder.set_source('_zoitechat_embedded', '''
#include "config.h" #include "config.h"
#include "zoitechat-plugin.h" #include "zoitechat-plugin.h"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
static zoitechat_plugin *ph; static zoitechat_plugin *ph;
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *); CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
CFFI_DLLEXPORT int _on_plugin_deinit(void); CFFI_DLLEXPORT int _on_plugin_deinit(void);
@@ -61,6 +71,20 @@ int zoitechat_plugin_init(zoitechat_plugin *plugin_handle,
char **name_out, char **description_out, char **name_out, char **description_out,
char **version_out, char *arg) char **version_out, char *arg)
{ {
#ifdef _WIN32
/* CPython cannot be removed from a process once Py_Initialize has run.
If the embedded interpreter fails to start (e.g. _cffi_backend is
missing), the host responds to our 0 return by g_module_close()ing
this plugin, which would also drop the last reference to pythonXY.dll
and unload it mid-flight; the leftover process-global state then
crashes the process later, e.g. when a server connection spawns the
next thread. Pin this module so FreeLibrary can never unload it. */
HMODULE self_handle;
GetModuleHandleExW (GET_MODULE_HANDLE_EX_FLAG_PIN |
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(LPCWSTR)&zoitechat_plugin_init, &self_handle);
#endif
if (ph != NULL) if (ph != NULL)
{ {
puts ("Python plugin already loaded\\n"); puts ("Python plugin already loaded\\n");
+9 -1
View File
@@ -13,8 +13,16 @@ else
python_dep = dependency(python_opt, version: '>= 2.7') python_dep = dependency(python_opt, version: '>= 2.7')
endif endif
python_py_conf = configuration_data()
python_py_conf.set('VERSION', meson.project_version())
python_py = configure_file(
input: 'python.py',
output: 'python.py',
configuration: python_py_conf
)
python3_source = custom_target('python-bindings', python3_source = custom_target('python-bindings',
input: ['../../src/common/zoitechat-plugin.h', 'python.py'], input: ['../../src/common/zoitechat-plugin.h', python_py],
output: 'python.c', output: 'python.c',
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@'] command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
) )
+1 -1
View File
@@ -19,7 +19,7 @@ else:
if not hasattr(sys, 'argv'): if not hasattr(sys, 'argv'):
sys.argv = ['<zoitechat>'] sys.argv = ['<zoitechat>']
VERSION = b'2.18.3' VERSION = b'@VERSION@'
PLUGIN_NAME = ffi.new('char[]', b'Python') PLUGIN_NAME = ffi.new('char[]', b'Python')
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1])) PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
PLUGIN_VERSION = ffi.new('char[]', VERSION) PLUGIN_VERSION = ffi.new('char[]', VERSION)
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{D58924D2-1492-4AC3-B058-7A214424EEDC}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>python38</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\win32\zoitechat.props" />
<PropertyGroup>
<TargetName>$(Python38Output)</TargetName>
<OutDir>$(ZoiteChatRel)plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PreprocessorDefinitions>WIN32;_WIN64;_AMD64_;NDEBUG;_WINDOWS;_USRDLL;PYTHON_EXPORTS;Py_NO_LINK_LIB;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UndefinePreprocessorDefinitions>_DEBUG;Py_DEBUG;Py_REF_DEBUG;Py_TRACE_REFS;%(UndefinePreprocessorDefinitions)</UndefinePreprocessorDefinitions>
<AdditionalOptions>/U_DEBUG /UPy_DEBUG /UPy_REF_DEBUG /UPy_TRACE_REFS %(AdditionalOptions)</AdditionalOptions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<AdditionalIncludeDirectories>$(Glib);$(Python38Path)\include;..\..\src\common;$(ZoiteChatLib);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<ModuleDefinitionFile>python.def</ModuleDefinitionFile>
<AdditionalDependencies>"$(Python38Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python38Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
<PreBuildEvent>
<Command>"$(Python38Path)\python.exe" generate_plugin.py ..\..\src\common\zoitechat-plugin.h python.py "$(IntDir)python.c"</Command>
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<None Include="generate_plugin.py" />
<None Include="zoitechat.py" />
<None Include="python.def" />
<None Include="python.py" />
<None Include="xchat.py" />
<None Include="hexchat.py" />
<None Include="_zoitechat.py" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(IntDir)python.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
+15 -1
View File
@@ -126,7 +126,21 @@ ctcp_handle (session *sess, char *to, char *nick, char *ip,
if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset)) if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset))
goto generic; goto generic;
inbound_action (sess, to, nick, ip, msg + 7, FALSE, tags_data->identified, tags_data); {
gboolean private_fromme = !serv->p_cmp (nick, serv->nick) && !is_channel (serv, to);
if (private_fromme)
{
session *target = find_dialog (serv, to);
if (target)
sess = target;
else if (serv->front_session)
sess = serv->front_session;
}
inbound_action (sess, to, nick, ip, msg + 7, private_fromme, tags_data->identified, tags_data);
}
return; return;
} }
+3 -3
View File
@@ -13,9 +13,9 @@ dbus_includes = [
dbus_service_dir = join_paths(get_option('datadir'), 'dbus-1/services') dbus_service_dir = join_paths(get_option('datadir'), 'dbus-1/services')
if get_option('dbus-service-use-appid') if get_option('dbus-service-use-appid')
dbus_service_name = 'io.github.Hexchat' dbus_service_name = 'net.zoite.Zoitechat'
dbus_output_file = 'io.github.Hexchat.service' dbus_output_file = 'net.zoite.Zoitechat.service'
dbus_cargs = '-DDBUS_SERVICE="io.github.Hexchat"' dbus_cargs = '-DDBUS_SERVICE="net.zoite.Zoitechat"'
else else
dbus_service_name = 'org.zoitechat.service' dbus_service_name = 'org.zoitechat.service'
dbus_output_file = 'org.zoitechat.service.service' dbus_output_file = 'org.zoitechat.service.service'
+1
View File
@@ -142,6 +142,7 @@ void fe_set_away (server *serv);
void fe_serverlist_open (session *sess); void fe_serverlist_open (session *sess);
void fe_get_bool (char *title, char *prompt, void *callback, void *userdata); void fe_get_bool (char *title, char *prompt, void *callback, void *userdata);
void fe_get_str (char *prompt, char *def, void *callback, void *ud); void fe_get_str (char *prompt, char *def, void *callback, void *ud);
void fe_get_password (char *prompt, void *callback, void *ud);
void fe_get_int (char *prompt, int def, void *callback, void *ud); void fe_get_int (char *prompt, int def, void *callback, void *ud);
#define FRF_WRITE 1 /* save file */ #define FRF_WRITE 1 /* save file */
#define FRF_MULTIPLE 2 /* multi-select */ #define FRF_MULTIPLE 2 /* multi-select */
+10 -3
View File
@@ -695,10 +695,15 @@ zoitechat_gtk3_theme_service_discover (void)
else else
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL); user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
} }
/* Scan the app's own themes dir first: imported themes get a symlink
* under the XDG data themes dir so GTK can resolve gtk-theme-name, and
* scanning that link first would make the canonical-root dedup keep
* the symlink path - changing the theme's path-derived id and breaking
* the saved gui_gtk3_theme reference on the next start. */
add_theme_root (user_roots, seen_user_roots, user_dir);
add_theme_root (user_roots, seen_user_roots, user_data_themes); add_theme_root (user_roots, seen_user_roots, user_data_themes);
g_free (user_data_themes); g_free (user_data_themes);
add_theme_root (user_roots, seen_user_roots, home_themes); add_theme_root (user_roots, seen_user_roots, home_themes);
add_theme_root (user_roots, seen_user_roots, user_dir);
{ {
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS"); const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
@@ -853,8 +858,10 @@ path_depth_from_root (const char *base, const char *path)
static gint static gint
theme_root_candidate_compare (gconstpointer a, gconstpointer b) theme_root_candidate_compare (gconstpointer a, gconstpointer b)
{ {
const ThemeRootCandidate *ca = a; /* g_ptr_array_sort() passes pointers to the array slots, not the values
const ThemeRootCandidate *cb = b; * stored in those slots. Dereference once before comparing candidates. */
const ThemeRootCandidate *ca = *(ThemeRootCandidate * const *) a;
const ThemeRootCandidate *cb = *(ThemeRootCandidate * const *) b;
if (ca->has_index_theme != cb->has_index_theme) if (ca->has_index_theme != cb->has_index_theme)
return ca->has_index_theme ? -1 : 1; return ca->has_index_theme ? -1 : 1;
if (ca->depth != cb->depth) if (ca->depth != cb->depth)
+2 -1
View File
@@ -675,7 +675,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
if (fromme) if (fromme)
{ {
if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp) if (prefs.hex_away_auto_unmark && serv->is_away &&
(!tags_data->timestamp || serv->have_echo_message))
sess->server->p_set_back (sess->server); sess->server->p_set_back (sess->server);
reply_context_print (sess, tags_data); reply_context_print (sess, tags_data);
EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL, EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,
+6 -1
View File
@@ -3025,7 +3025,7 @@ cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[])
newsess->server->nick, msg + offset, TRUE, FALSE, newsess->server->nick, msg + offset, TRUE, FALSE,
&no_tags); &no_tags);
} }
else else if (!sess->server->have_echo_message)
{ {
/* mask out passwords */ /* mask out passwords */
if (g_ascii_strcasecmp (nick, "nickserv") == 0) if (g_ascii_strcasecmp (nick, "nickserv") == 0)
@@ -5058,6 +5058,11 @@ handle_user_input (session *sess, char *text, int history, int nocommand)
handle_say (sess, text, TRUE); handle_say (sess, text, TRUE);
return 1; return 1;
} }
if (text[0] == cmd_char && text[1] == cmd_char)
{
handle_say (sess, text + 1, TRUE);
return 1;
}
for (i = 1; text[i]; i++) for (i = 1; text[i]; i++)
{ {
+34
View File
@@ -25,6 +25,7 @@
#ifdef WIN32 #ifdef WIN32
#include <io.h> #include <io.h>
#include <windows.h>
#else #else
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -56,6 +57,36 @@ typedef struct session zoitechat_context;
#endif #endif
#define DEBUG(x) {x;} #define DEBUG(x) {x;}
#ifdef WIN32
/* Python >= 3.9 dropped support for Windows versions before 10, so those
* systems get a Python 3.8 build of the plugin instead. RtlGetVersion is
* used because GetVersionEx reports a version capped by the compatibility
* manifest rather than the real one. */
static gboolean
plugin_windows_needs_python38 (void)
{
typedef LONG (WINAPI *rtl_get_version_func) (PRTL_OSVERSIONINFOW);
rtl_get_version_func rtl_get_version;
RTL_OSVERSIONINFOW version;
HMODULE ntdll;
ntdll = GetModuleHandleW (L"ntdll.dll");
if (ntdll == NULL)
return FALSE;
rtl_get_version = (rtl_get_version_func) GetProcAddress (ntdll, "RtlGetVersion");
if (rtl_get_version == NULL)
return FALSE;
memset (&version, 0, sizeof (version));
version.dwOSVersionInfoSize = sizeof (version);
if (rtl_get_version (&version) != 0)
return FALSE;
return version.dwMajorVersion < 10;
}
#endif
/* crafted to be an even 32 bytes */ /* crafted to be an even 32 bytes */
struct _zoitechat_hook struct _zoitechat_hook
@@ -491,6 +522,9 @@ plugin_auto_load (session *sess)
for_files (lib_dir, "hcfishlim.dll", plugin_auto_load_cb); for_files (lib_dir, "hcfishlim.dll", plugin_auto_load_cb);
for_files(lib_dir, "hclua.dll", plugin_auto_load_cb); for_files(lib_dir, "hclua.dll", plugin_auto_load_cb);
for_files (lib_dir, "hcperl.dll", plugin_auto_load_cb); for_files (lib_dir, "hcperl.dll", plugin_auto_load_cb);
if (plugin_windows_needs_python38 ())
for_files (lib_dir, "hcpython38.dll", plugin_auto_load_cb);
else
for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb); for_files (lib_dir, "hcpython3.dll", plugin_auto_load_cb);
for_files (lib_dir, "hcupd.dll", plugin_auto_load_cb); for_files (lib_dir, "hcupd.dll", plugin_auto_load_cb);
for_files (lib_dir, "hcsysinfo.dll", plugin_auto_load_cb); for_files (lib_dir, "hcsysinfo.dll", plugin_auto_load_cb);
+1 -1
View File
@@ -1368,7 +1368,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
{ {
if (ignore_check (word[1], IG_PRIV)) if (ignore_check (word[1], IG_PRIV))
return; return;
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick)) if (!serv->p_cmp (nick, serv->nick))
{ {
session *target_sess = find_dialog (serv, to); session *target_sess = find_dialog (serv, to);
+53
View File
@@ -109,6 +109,58 @@ static void server_disconnect (session * sess, int sendquit, int err);
static int server_cleanup (server * serv); static int server_cleanup (server * serv);
static void server_connect (server *serv, char *hostname, int port, int no_login); static void server_connect (server *serv, char *hostname, int port, int no_login);
typedef struct
{
server *serv;
char hostname[128];
int port;
} reconnect_password_prompt;
static gboolean
server_login_uses_password_sasl (int logintype)
{
return logintype == LOGIN_SASL ||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
logintype == LOGIN_SASL_SCRAM_SHA_512;
}
static void
server_reconnect_password_cb (int cancel, char *text, void *userdata)
{
reconnect_password_prompt *prompt = userdata;
if (!cancel && is_server (prompt->serv) && !prompt->serv->connected && !prompt->serv->connecting && prompt->serv->server_session)
{
prompt->serv->password[0] = 0;
if (text && *text)
safe_strcpy (prompt->serv->password, text, sizeof (prompt->serv->password));
server_connect (prompt->serv, prompt->hostname, prompt->port, FALSE);
}
g_free (prompt);
}
static gboolean
server_prompt_reconnect_password (server *serv)
{
reconnect_password_prompt *prompt;
char *msg;
ircnet *net = serv->network;
if (!net || !(net->flags & FLAG_PROMPT_PASSWORD) || !server_login_uses_password_sasl (serv->loginmethod))
return FALSE;
prompt = g_new0 (reconnect_password_prompt, 1);
prompt->serv = serv;
safe_strcpy (prompt->hostname, serv->hostname, sizeof (prompt->hostname));
prompt->port = serv->port;
msg = g_strdup_printf (_("Enter SASL password for %s:"), net->name ? net->name : _("this network"));
fe_get_password (msg, server_reconnect_password_cb, prompt);
g_free (msg);
return TRUE;
}
static void static void
write_error (char *message, GError **error) write_error (char *message, GError **error)
{ {
@@ -734,6 +786,7 @@ timeout_auto_reconnect (server *serv)
serv->recondelay_tag = 0; serv->recondelay_tag = 0;
if (!serv->connected && !serv->connecting && serv->server_session) if (!serv->connected && !serv->connecting && serv->server_session)
{ {
if (!server_prompt_reconnect_password (serv))
server_connect (serv, serv->hostname, serv->port, FALSE); server_connect (serv, serv->hostname, serv->port, FALSE);
} }
} }
+96 -1
View File
@@ -439,8 +439,99 @@ servlist_favchan_copy (favchannel *fav)
return newfav; return newfav;
} }
static void servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const char *password);
typedef struct
{
session *sess;
ircnet *net;
gboolean join;
} servlist_password_prompt;
static gboolean
servlist_login_uses_password_sasl (int logintype)
{
return logintype == LOGIN_SASL ||
logintype == LOGIN_SASL_SCRAM_SHA_1 ||
logintype == LOGIN_SASL_SCRAM_SHA_256 ||
logintype == LOGIN_SASL_SCRAM_SHA_512;
}
static void servlist_prompt_sasl_password (servlist_password_prompt *prompt);
static void
servlist_connect_without_password_cb (int value, void *userdata)
{
servlist_password_prompt *prompt = userdata;
if (value)
{
servlist_connect_with_password (prompt->sess, prompt->net, prompt->join, NULL);
g_free (prompt);
}
else
{
servlist_prompt_sasl_password (prompt);
}
}
static void
servlist_connect_password_cb (int cancel, char *text, void *userdata)
{
servlist_password_prompt *prompt = userdata;
char *msg;
if (cancel)
{
fe_serverlist_open (prompt->sess);
g_free (prompt);
return;
}
if (text && *text)
{
servlist_connect_with_password (prompt->sess, prompt->net, prompt->join, text);
g_free (prompt);
return;
}
msg = g_strdup_printf (_("Continue connecting to %s without a SASL password?"), prompt->net->name ? prompt->net->name : _("this network"));
fe_get_bool (_("No SASL password"), msg, servlist_connect_without_password_cb, prompt);
g_free (msg);
}
static void
servlist_prompt_sasl_password (servlist_password_prompt *prompt)
{
char *msg;
msg = g_strdup_printf (_("Enter SASL password for %s:"), prompt->net->name ? prompt->net->name : _("this network"));
fe_get_password (msg, servlist_connect_password_cb, prompt);
g_free (msg);
}
void void
servlist_connect (session *sess, ircnet *net, gboolean join) servlist_connect (session *sess, ircnet *net, gboolean join)
{
int logintype;
servlist_password_prompt *prompt;
logintype = net && net->logintype ? net->logintype : LOGIN_DEFAULT_REAL;
if (net && (net->flags & FLAG_PROMPT_PASSWORD) && servlist_login_uses_password_sasl (logintype))
{
prompt = g_new0 (servlist_password_prompt, 1);
prompt->sess = sess;
prompt->net = net;
prompt->join = join;
servlist_prompt_sasl_password (prompt);
return;
}
servlist_connect_with_password (sess, net, join, NULL);
}
static void
servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const char *password)
{ {
ircserver *ircserv; ircserver *ircserv;
GSList *list; GSList *list;
@@ -487,7 +578,11 @@ servlist_connect (session *sess, ircnet *net, gboolean join)
} }
serv->password[0] = 0; serv->password[0] = 0;
if ((net->flags & FLAG_USE_KEYRING) && net->name) if (password && *password)
{
safe_strcpy (serv->password, password, sizeof (serv->password));
}
else if ((net->flags & FLAG_USE_KEYRING) && net->name)
{ {
char *stored_pass = secretstore_get_network_password (net->name); char *stored_pass = secretstore_get_network_password (net->name);
if (stored_pass && *stored_pass) if (stored_pass && *stored_pass)
+2 -1
View File
@@ -63,7 +63,8 @@ extern GSList *network_list;
#define FLAG_ALLOW_INVALID 32 #define FLAG_ALLOW_INVALID 32
#define FLAG_FAVORITE 64 #define FLAG_FAVORITE 64
#define FLAG_USE_KEYRING 128 #define FLAG_USE_KEYRING 128
#define FLAG_COUNT 8 #define FLAG_PROMPT_PASSWORD 256
#define FLAG_COUNT 9
/* Login methods. Use server password by default - if we had a NickServ password, it'd be set to 2 already by servlist_load() */ /* Login methods. Use server password by default - if we had a NickServ password, it'd be set to 2 already by servlist_load() */
#define LOGIN_DEFAULT_REAL LOGIN_PASS /* this is to set the default login method for unknown servers */ #define LOGIN_DEFAULT_REAL LOGIN_PASS /* this is to set the default login method for unknown servers */
+91
View File
@@ -1695,6 +1695,97 @@ pevent_load (char *filename)
return 0; return 0;
} }
static gboolean
text_event_uses_color (const char *text, int color)
{
const char *p;
if (!text || color < 0 || color > 31)
return FALSE;
for (p = text; *p; p++)
{
int value;
int digits;
if ((unsigned char) *p == 3)
p++;
else if (*p == '%' && p[1] == 'C')
p += 2;
else
continue;
if (!g_ascii_isdigit (*p))
{
p--;
continue;
}
value = *p - '0';
digits = 1;
if (g_ascii_isdigit (p[1]))
{
value = value * 10 + p[1] - '0';
digits = 2;
}
if (value == color)
return TRUE;
p += digits - 1;
if (*p == ',' && g_ascii_isdigit (p[1]))
{
p++;
value = *p - '0';
if (g_ascii_isdigit (p[1]))
{
value = value * 10 + p[1] - '0';
p++;
}
if (value == color)
return TRUE;
}
}
return FALSE;
}
char **
text_color_event_names (int color, int *count)
{
GPtrArray *names;
int found = 0;
int i;
if (count)
*count = 0;
names = g_ptr_array_new ();
for (i = 0; i < NUM_XP; i++)
{
const char *text = pntevts_text[i] ? pntevts_text[i] : te[i].def;
if (!text_event_uses_color (text, color))
continue;
g_ptr_array_add (names, g_strdup (_(te[i].name)));
found++;
}
if (!found)
{
g_ptr_array_free (names, TRUE);
return NULL;
}
g_ptr_array_add (names, NULL);
if (count)
*count = found;
return (char **) g_ptr_array_free (names, FALSE);
}
static void static void
pevent_check_all_loaded (void) pevent_check_all_loaded (void)
{ {
+1
View File
@@ -53,6 +53,7 @@ int pevt_build_string (const char *input, char **output, int *max_arg);
int pevent_load (char *filename); int pevent_load (char *filename);
void pevent_make_pntevts (void); void pevent_make_pntevts (void);
int text_color_of (char *name); int text_color_of (char *name);
char **text_color_event_names (int color, int *count);
void text_emit (int index, session *sess, char *a, char *b, char *c, char *d, void text_emit (int index, session *sess, char *a, char *b, char *c, char *d,
time_t timestamp); time_t timestamp);
int text_emit_by_name (char *name, session *sess, time_t timestamp, int text_emit_by_name (char *name, session *sess, time_t timestamp,
-1
View File
@@ -463,7 +463,6 @@ doover:
while (list) while (list)
{ {
sess = list->data; sess = list->data;
if (!sess->server->have_awaynotify)
sess->done_away_check = FALSE; sess->done_away_check = FALSE;
list = list->next; list = list->next;
} }
+9
View File
@@ -300,6 +300,14 @@ cv_tree_focus (chan *ch)
path = gtk_tree_model_get_path (model, &ch->iter); path = gtk_tree_model_get_path (model, &ch->iter);
if (path) if (path)
{
if (!gtk_widget_get_realized (GTK_WIDGET (tree)))
{
/* Layout changes can restore focus before the new tree is realized.
* Let GTK track the row and scroll once its geometry is available. */
gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
}
else
{ {
/* This full section does what /* This full section does what
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5); * gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
@@ -322,6 +330,7 @@ cv_tree_focus (chan *ch)
dest_y = 0; dest_y = 0;
gtk_tree_view_scroll_to_point (tree, -1, dest_y); gtk_tree_view_scroll_to_point (tree, -1, dest_y);
} }
}
/* theft done, now make it focused like */ /* theft done, now make it focused like */
gtk_tree_view_set_cursor (tree, path, NULL, FALSE); gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
gtk_tree_path_free (path); gtk_tree_path_free (path);
+104 -1
View File
@@ -174,7 +174,7 @@ chanview_apply_theme (chanview *cv)
chanview_set_font_desc (cv, font); chanview_set_font_desc (cv, font);
theme_manager_apply_channel_tree_style (w, theme_manager_apply_channel_tree_style (w,
theme_manager_get_channel_tree_palette_behavior (font)); theme_manager_get_channel_tree_palette_behavior (cv->font_desc));
} }
static void static void
@@ -307,6 +307,109 @@ chanview_set_impl (chanview *cv, int type)
cv->func_focus (cv->focused); cv->func_focus (cv->focused);
} }
void
chanview_set_sorted (chanview *cv, gboolean sorted)
{
cv->sorted = sorted ? 1 : 0;
}
void
chanview_set_use_icons (chanview *cv, gboolean use_icons)
{
cv->use_icons = use_icons ? 1 : 0;
}
void
chanview_set_trunc_len (chanview *cv, int trunc_len)
{
cv->trunc_len = trunc_len;
}
typedef struct
{
chan *ch;
int old_pos;
} chanview_sort_entry;
/* re-sort the session rows below one parent with cb_compare, leaving
* utility rows (tag != 0) in place; mirrors chanview_insert_sorted() */
static void
chanview_resort_children (chanview *cv, GtkTreeIter *parent)
{
GtkTreeModel *model = GTK_TREE_MODEL (cv->store);
GtkTreeIter iter;
chanview_sort_entry *entries;
int *positions;
gint *new_order;
chan *ch;
int n, i, j, count;
n = gtk_tree_model_iter_n_children (model, parent);
if (n < 2)
return;
entries = g_new0 (chanview_sort_entry, n);
positions = g_new0 (int, n);
new_order = g_new0 (gint, n);
count = 0;
for (i = 0; i < n; i++)
{
new_order[i] = i;
if (!gtk_tree_model_iter_nth_child (model, &iter, parent, i))
continue;
gtk_tree_model_get (model, &iter, COL_CHAN, &ch, -1);
if (ch && ch->tag == 0 && ch->userdata)
{
entries[count].ch = ch;
entries[count].old_pos = i;
positions[count] = i;
count++;
}
}
/* stable insertion sort; tab groups stay small so this is cheap */
for (i = 1; i < count; i++)
{
chanview_sort_entry key = entries[i];
j = i - 1;
while (j >= 0 && cv->cb_compare (entries[j].ch->userdata, key.ch->userdata) > 0)
{
entries[j + 1] = entries[j];
j--;
}
entries[j + 1] = key;
}
for (i = 0; i < count; i++)
new_order[positions[i]] = entries[i].old_pos;
gtk_tree_store_reorder (cv->store, parent, new_order);
g_free (entries);
g_free (positions);
g_free (new_order);
}
/* top-level rows keep their order (insertion never sorts them either);
* only the children within each family are sorted */
void
chanview_resort (chanview *cv)
{
GtkTreeIter iter;
if (!cv->cb_compare)
return;
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (cv->store), &iter))
{
do
chanview_resort_children (cv, &iter);
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (cv->store), &iter));
}
}
static void static void
chanview_free_ch (chanview *cv, GtkTreeIter *iter) chanview_free_ch (chanview *cv, GtkTreeIter *iter)
{ {
+4
View File
@@ -34,6 +34,10 @@ void chanview_set_callbacks (chanview *cv,
gboolean (*cb_contextmenu) (chanview *, chan *, int tag, void *userdata, GdkEventButton *), gboolean (*cb_contextmenu) (chanview *, chan *, int tag, void *userdata, GdkEventButton *),
int (*cb_compare) (void *a, void *b)); int (*cb_compare) (void *a, void *b));
void chanview_set_impl (chanview *cv, int type); void chanview_set_impl (chanview *cv, int type);
void chanview_set_sorted (chanview *cv, gboolean sorted);
void chanview_set_use_icons (chanview *cv, gboolean use_icons);
void chanview_set_trunc_len (chanview *cv, int trunc_len);
void chanview_resort (chanview *cv);
chan *chanview_add (chanview *cv, char *name, void *family, void *userdata, gboolean allow_closure, int tag, GdkPixbuf *icon); chan *chanview_add (chanview *cv, char *name, void *family, void *userdata, gboolean allow_closure, int tag, GdkPixbuf *icon);
int chanview_get_size (chanview *cv); int chanview_get_size (chanview *cv);
GtkWidget *chanview_get_box (chanview *cv); GtkWidget *chanview_get_box (chanview *cv);
+71 -27
View File
@@ -527,6 +527,43 @@ gtkutil_destroy (GtkWidget * igad, GtkWidget * dgad)
gtk_widget_destroy (dgad); gtk_widget_destroy (dgad);
} }
static void
gtkutil_place_dialog (GtkWidget *dialog)
{
GList *list;
GList *item;
GtkWindow *anchor = NULL;
gint x;
gint y;
extern GtkWidget *parent_window;
list = gtk_window_list_toplevels ();
for (item = list; item; item = item->next)
{
if (item->data != dialog && GTK_IS_WINDOW (item->data) &&
gtk_widget_get_visible (GTK_WIDGET (item->data)) &&
gtk_window_is_active (GTK_WINDOW (item->data)))
{
anchor = GTK_WINDOW (item->data);
break;
}
}
g_list_free (list);
if (!anchor && parent_window && parent_window != dialog && GTK_IS_WINDOW (parent_window))
anchor = GTK_WINDOW (parent_window);
if (anchor)
{
gtk_window_get_position (anchor, &x, &y);
gtk_window_move (GTK_WINDOW (dialog), x, y);
}
else
{
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
}
}
static void static void
gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry) gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry)
{ {
@@ -540,15 +577,15 @@ gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry)
switch (arg1) switch (arg1)
{ {
case GTK_RESPONSE_REJECT:
callback (TRUE, text, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
case GTK_RESPONSE_ACCEPT: case GTK_RESPONSE_ACCEPT:
callback (FALSE, text, user_data); callback (FALSE, text, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog)); break;
default:
callback (TRUE, text, user_data);
break; break;
} }
gtk_widget_destroy (GTK_WIDGET (dialog));
} }
static void static void
@@ -557,8 +594,8 @@ gtkutil_str_enter (GtkWidget *entry, GtkWidget *dialog)
gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
} }
void static void
fe_get_str (char *msg, char *def, void *callback, void *userdata) gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean visible)
{ {
GtkWidget *dialog; GtkWidget *dialog;
GtkWidget *entry; GtkWidget *entry;
@@ -572,18 +609,11 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
NULL); NULL);
theme_manager_attach_window (dialog); theme_manager_attach_window (dialog);
if (parent_window && GTK_IS_WINDOW (parent_window))
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window)); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE); gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
if (userdata == (void *)1) /* nick box is usually on the very bottom, make it centered */
{
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER);
}
else
{
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
}
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0); hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
g_object_set_data (G_OBJECT (dialog), "cb", callback); g_object_set_data (G_OBJECT (dialog), "cb", callback);
@@ -592,7 +622,8 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
entry = gtk_entry_new (); entry = gtk_entry_new ();
g_signal_connect (G_OBJECT (entry), "activate", g_signal_connect (G_OBJECT (entry), "activate",
G_CALLBACK (gtkutil_str_enter), dialog); G_CALLBACK (gtkutil_str_enter), dialog);
gtk_entry_set_text (GTK_ENTRY (entry), def); gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0); gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
label = gtk_label_new (msg); label = gtk_label_new (msg);
@@ -606,6 +637,18 @@ fe_get_str (char *msg, char *def, void *callback, void *userdata)
gtk_widget_show_all (dialog); gtk_widget_show_all (dialog);
} }
void
fe_get_str (char *msg, char *def, void *callback, void *userdata)
{
gtkutil_get_str (msg, def, callback, userdata, TRUE);
}
void
fe_get_password (char *msg, void *callback, void *userdata)
{
gtkutil_get_str (msg, "", callback, userdata, FALSE);
}
static void static void
gtkutil_get_number_response (GtkDialog *dialog, gint arg1, gpointer spin) gtkutil_get_number_response (GtkDialog *dialog, gint arg1, gpointer spin)
{ {
@@ -641,15 +684,15 @@ gtkutil_get_bool_response (GtkDialog *dialog, gint arg1, gpointer spin)
switch (arg1) switch (arg1)
{ {
case GTK_RESPONSE_REJECT:
callback (0, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
case GTK_RESPONSE_ACCEPT: case GTK_RESPONSE_ACCEPT:
callback (1, user_data); callback (1, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog)); break;
default:
callback (0, user_data);
break; break;
} }
gtk_widget_destroy (GTK_WIDGET (dialog));
} }
void void
@@ -667,9 +710,10 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
_("_OK"), GTK_RESPONSE_ACCEPT, _("_OK"), GTK_RESPONSE_ACCEPT,
NULL); NULL);
theme_manager_attach_window (dialog); theme_manager_attach_window (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE); if (parent_window && GTK_IS_WINDOW (parent_window))
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window)); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0); hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
@@ -707,10 +751,10 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
_("_Yes"), GTK_RESPONSE_ACCEPT, _("_Yes"), GTK_RESPONSE_ACCEPT,
NULL); NULL);
theme_manager_attach_window (dialog); theme_manager_attach_window (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE); if (parent_window && GTK_IS_WINDOW (parent_window))
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window)); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
g_object_set_data (G_OBJECT (dialog), "cb", callback); g_object_set_data (G_OBJECT (dialog), "cb", callback);
g_object_set_data (G_OBJECT (dialog), "ud", userdata); g_object_set_data (G_OBJECT (dialog), "ud", userdata);
+253 -10
View File
@@ -67,6 +67,34 @@
#include <shellapi.h> #include <shellapi.h>
#include <gdk/gdkwin32.h> #include <gdk/gdkwin32.h>
/* The shell only minimizes a window from its taskbar button when the
* window style carries WS_MINIMIZEBOX; without it the click merely
* deactivates the window (issue #65). GTK3 older than 3.24.50 can drop
* the bit from top-level windows (https://gitlab.gnome.org/GNOME/gtk/-/issues/7494),
* so guarantee it ourselves for session windows. */
static void
mg_win32_ensure_minimizable (GdkWindow *gdk_window)
{
HWND hwnd;
LONG_PTR window_style;
if (!gdk_window)
return;
hwnd = gdk_win32_window_get_handle (gdk_window);
if (!hwnd)
return;
window_style = GetWindowLongPtr (hwnd, GWL_STYLE);
if (!window_style || (window_style & WS_MINIMIZEBOX))
return;
SetWindowLongPtr (hwnd, GWL_STYLE, window_style | WS_MINIMIZEBOX);
SetWindowPos (hwnd, NULL, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
}
static void static void
mg_win32_allow_autohide_taskbar (GtkWindow *window, GdkEventWindowState *event) mg_win32_allow_autohide_taskbar (GtkWindow *window, GdkEventWindowState *event)
{ {
@@ -541,6 +569,18 @@ mg_create_tab_colors (void)
away_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_AWAY], FALSE); away_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_AWAY], FALSE);
} }
static PangoAttrList *
mg_attr_list_for_tab_state (int state)
{
if (state & TAB_STATE_NEW_HILIGHT)
return nickseen_list;
if (state & TAB_STATE_NEW_MSG)
return newmsg_list;
if (state & TAB_STATE_NEW_DATA)
return newdata_list;
return plain_list;
}
static void static void
set_window_urgency (GtkWidget *win, gboolean set) set_window_urgency (GtkWidget *win, gboolean set)
{ {
@@ -686,7 +726,10 @@ mg_set_access_icon (session_gui *gui, GdkPixbuf *pix, gboolean away)
{ {
if (gui->op_xpm) if (gui->op_xpm)
{ {
if (pix == gtk_image_get_pixbuf (GTK_IMAGE (gui->op_xpm))) /* no change? */ /* no change? (the pref check makes sure a live settings
change can still remove the icon) */
if (pix == gtk_image_get_pixbuf (GTK_IMAGE (gui->op_xpm)) &&
prefs.hex_gui_input_icon)
{ {
mg_set_myself_away (gui, away); mg_set_myself_away (gui, away);
return; return;
@@ -3568,6 +3611,48 @@ mg_create_topicbar (session *sess, GtkWidget *box)
mg_create_chanmodebuttons (gui, bbox); mg_create_chanmodebuttons (gui, bbox);
} }
/* re-apply the multi-line topic and inline mode button preferences to an
* already built topic bar (see mg_create_topicbar () for the layout) */
static void
mg_update_topicbar_layout (session_gui *gui)
{
GtkWidget *topic = gui->topic_entry;
GtkWidget *vbox = gui->topic_bar;
GtkWidget *topic_scroll;
GtkWidget *hbox;
GtkWidget *mode_hbox;
GtkWidget *desired_parent;
GtkWidget *current_parent;
gboolean wrap;
if (!topic || !GTK_IS_TEXT_VIEW (topic) || !vbox || !gui->topicbutton_box)
return;
wrap = prefs.hex_gui_topicbar_multiline && !prefs.hex_gui_mode_buttons_inline;
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (topic),
wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE);
topic_scroll = gtk_widget_get_parent (topic);
hbox = topic_scroll ? gtk_widget_get_parent (topic_scroll) : NULL;
mode_hbox = gtk_widget_get_parent (gui->topicbutton_box);
if (mode_hbox && hbox)
{
desired_parent = prefs.hex_gui_mode_buttons_inline ? hbox : vbox;
current_parent = gtk_widget_get_parent (mode_hbox);
if (current_parent && current_parent != desired_parent)
{
g_object_ref (mode_hbox);
gtk_container_remove (GTK_CONTAINER (current_parent), mode_hbox);
gtk_box_pack_start (GTK_BOX (desired_parent), mode_hbox, 0, 0, 0);
g_object_unref (mode_hbox);
}
}
mg_topicbar_update_height (topic);
}
/* check if a word is clickable */ /* check if a word is clickable */
static int static int
@@ -3976,8 +4061,12 @@ mg_create_userlist (session_gui *gui, GtkWidget *box)
gtk_widget_set_margin_end (gui->namelistinfo, 0); gtk_widget_set_margin_end (gui->namelistinfo, 0);
gtk_widget_set_hexpand (gui->namelistinfo, TRUE); gtk_widget_set_hexpand (gui->namelistinfo, TRUE);
gtk_widget_set_halign (gui->namelistinfo, GTK_ALIGN_FILL); gtk_widget_set_halign (gui->namelistinfo, GTK_ALIGN_FILL);
if (prefs.hex_gui_ulist_count) /* always pack it so the preference can be toggled without a
restart; visibility is controlled explicitly */
gtk_widget_set_no_show_all (gui->namelistinfo, TRUE);
gtk_box_pack_start (GTK_BOX (vbox), gui->namelistinfo, 0, 0, 0); gtk_box_pack_start (GTK_BOX (vbox), gui->namelistinfo, 0, 0, 0);
if (prefs.hex_gui_ulist_count)
gtk_widget_show (gui->namelistinfo);
gui->user_tree = ulist = userlist_create (vbox); gui->user_tree = ulist = userlist_create (vbox);
@@ -4876,7 +4965,11 @@ mg_create_menu (session_gui *gui, GtkWidget *table, int away_state)
gui->menu_item); gui->menu_item);
gtk_widget_set_hexpand (gui->menu, TRUE); gtk_widget_set_hexpand (gui->menu, TRUE);
gtk_widget_set_vexpand (gui->menu, FALSE); gtk_widget_set_vexpand (gui->menu, FALSE);
gtk_widget_set_halign (gui->menu, GTK_ALIGN_START); /* Fill the row: with GTK_ALIGN_START the menu bar stops after its
* last item and the rest of the row shows bare window background,
* which reads as a two-colored menu bar whenever the theme styles
* menubar and window differently. */
gtk_widget_set_halign (gui->menu, GTK_ALIGN_FILL);
gtk_widget_set_valign (gui->menu, GTK_ALIGN_FILL); gtk_widget_set_valign (gui->menu, GTK_ALIGN_FILL);
gtk_grid_attach (GTK_GRID (table), gui->menu, 0, 0, 3, 1); gtk_grid_attach (GTK_GRID (table), gui->menu, 0, 0, 3, 1);
} }
@@ -4985,6 +5078,7 @@ mg_create_topwindow (session *sess)
#ifdef G_OS_WIN32 #ifdef G_OS_WIN32
parent_win = gtk_widget_get_window (win); parent_win = gtk_widget_get_window (win);
gdk_window_add_filter (parent_win, mg_win32_filter, NULL); gdk_window_add_filter (parent_win, mg_win32_filter, NULL);
mg_win32_ensure_minimizable (parent_win);
#endif #endif
} }
@@ -5021,6 +5115,18 @@ mg_win32_filter (GdkXEvent *xevent, GdkEvent *event, gpointer data)
if (!msg) if (!msg)
return GDK_FILTER_CONTINUE; return GDK_FILTER_CONTINUE;
if (msg->message == WM_STYLECHANGING && msg->wParam == (WPARAM)GWL_STYLE)
{
STYLESTRUCT *style = (STYLESTRUCT *)msg->lParam;
/* Keep the window minimizable from its taskbar button; see
* mg_win32_ensure_minimizable (). */
if (style)
style->styleNew |= WS_MINIMIZEBOX;
return GDK_FILTER_CONTINUE;
}
if (msg->message == WM_TIMECHANGE) if (msg->message == WM_TIMECHANGE)
{ {
_tzset(); _tzset();
@@ -5170,6 +5276,7 @@ mg_create_tabwindow (session *sess)
#ifdef G_OS_WIN32 #ifdef G_OS_WIN32
parent_win = gtk_widget_get_window (win); parent_win = gtk_widget_get_window (win);
gdk_window_add_filter (parent_win, mg_win32_filter, NULL); gdk_window_add_filter (parent_win, mg_win32_filter, NULL);
mg_win32_ensure_minimizable (parent_win);
#endif #endif
} }
@@ -5187,6 +5294,10 @@ mg_apply_setup (void)
sess = list->data; sess = list->data;
gtk_xtext_set_time_stamp (sess->res->buffer, prefs.hex_stamp_text); gtk_xtext_set_time_stamp (sess->res->buffer, prefs.hex_stamp_text);
((xtext_buffer *)sess->res->buffer)->needs_recalc = TRUE; ((xtext_buffer *)sess->res->buffer)->needs_recalc = TRUE;
/* the attribute lists were just recreated, so re-apply the
color matching each tab's activity state */
if (sess->res->tab)
chan_set_color (sess->res->tab, mg_attr_list_for_tab_state (sess->tab_state));
if (!sess->gui->is_tab || !done_main) if (!sess->gui->is_tab || !done_main)
mg_place_userlist_and_chanview (sess->gui); mg_place_userlist_and_chanview (sess->gui);
if (sess->gui->is_tab) if (sess->gui->is_tab)
@@ -5229,14 +5340,12 @@ fe_buttons_update (session *sess)
gtk_widget_hide (sess->gui->button_box); gtk_widget_hide (sess->gui->button_box);
} }
void /* tab label for a session that has no joined channel: the channel we are
fe_clear_channel (session *sess) * waiting to join in parentheses, or "<none>". tbuf must hold CHANLEN+6. */
{
char tbuf[CHANLEN+6];
session_gui *gui = sess->gui;
if (sess->gui->is_tab) static void
{ mg_tab_pending_name (session *sess, char *tbuf)
{
if (sess->waitchannel[0]) if (sess->waitchannel[0])
{ {
if (prefs.hex_gui_tab_trunc > 2 && g_utf8_strlen (sess->waitchannel, -1) > prefs.hex_gui_tab_trunc) if (prefs.hex_gui_tab_trunc > 2 && g_utf8_strlen (sess->waitchannel, -1) > prefs.hex_gui_tab_trunc)
@@ -5253,6 +5362,36 @@ fe_clear_channel (session *sess)
} }
else else
strcpy (tbuf, _("<none>")); strcpy (tbuf, _("<none>"));
}
/* re-apply the tab label truncation preference to a session's tab */
static void
mg_retrunc_tab (session *sess)
{
char tbuf[CHANLEN+6];
if (!sess->res->tab)
return;
if (sess->channel[0])
chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc);
else
{
mg_tab_pending_name (sess, tbuf);
chan_rename (sess->res->tab, tbuf, prefs.hex_gui_tab_trunc);
}
}
void
fe_clear_channel (session *sess)
{
char tbuf[CHANLEN+6];
session_gui *gui = sess->gui;
if (sess->gui->is_tab)
{
mg_tab_pending_name (sess, tbuf);
chan_rename (sess->res->tab, tbuf, prefs.hex_gui_tab_trunc); chan_rename (sess->res->tab, tbuf, prefs.hex_gui_tab_trunc);
} }
@@ -5376,6 +5515,110 @@ fe_set_channel (session *sess)
chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc); chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc);
} }
static void
mg_apply_live_prefs_to_gui (session_gui *gui, session *sess, const mg_live_prefs *changes)
{
if (changes->meters)
mg_update_meters (gui);
if (changes->ulist_columns && gui->user_tree)
userlist_rebuild_columns (gui->user_tree);
if (changes->ulist_count && gui->namelistinfo)
{
if (prefs.hex_gui_ulist_count)
gtk_widget_show (gui->namelistinfo);
else
gtk_widget_hide (gui->namelistinfo);
}
if (changes->input_box && gui->nick_box)
{
if (prefs.hex_gui_input_nick)
gtk_widget_show (gui->nick_box);
else
gtk_widget_hide (gui->nick_box);
}
/* refresh the access icon; re-filling the user list rows can
clear it as a side effect, so restore it here too */
if ((changes->input_box || changes->ulist_rows) && sess && is_session (sess))
{
if (sess->type == SESS_DIALOG)
mg_set_access_icon (gui, NULL, sess->server->is_away);
else
mg_set_access_icon (gui, get_user_icon (sess->server, sess->me), sess->server->is_away);
}
if (changes->topic_bar)
mg_update_topicbar_layout (gui);
if (changes->transparency && gui->window)
gtk_widget_set_opacity (gui->window, (prefs.hex_gui_transparency / 255.));
}
/* apply preference changes that used to require a restart. called by
* setup.c after the new prefs have been committed. */
void
mg_apply_live_prefs (const mg_live_prefs *changes)
{
GSList *list;
session *sess;
int done_main = FALSE;
if (!changes)
return;
/* the tab bar / channel tree lives in the shared tab window */
if (mg_gui && mg_gui->chanview)
{
if (changes->tab_resort)
{
chanview_set_sorted (mg_gui->chanview, prefs.hex_gui_tab_sort);
chanview_resort (mg_gui->chanview);
}
if (changes->tab_trunc)
chanview_set_trunc_len (mg_gui->chanview, prefs.hex_gui_tab_trunc);
if (changes->chanview || changes->tab_resort)
{
chanview_set_use_icons (mg_gui->chanview,
prefs.hex_gui_tab_icons && (pix_tree_channel || pix_tree_dialog ||
pix_tree_server || pix_tree_util));
/* rebuild the tab bar / tree widgets from the model */
chanview_set_impl (mg_gui->chanview, prefs.hex_gui_tab_layout);
}
}
list = sess_list;
while (list)
{
sess = list->data;
if (changes->tab_trunc)
mg_retrunc_tab (sess);
if (changes->ulist_sort)
userlist_apply_sort (sess);
if (changes->ulist_rows)
userlist_refill (sess);
if (sess->gui->is_tab)
{
/* only apply to the shared tab window once, using the
front-most session for session-dependent bits */
if (!done_main)
{
done_main = TRUE;
mg_apply_live_prefs_to_gui (sess->gui, current_tab, changes);
}
}
else
mg_apply_live_prefs_to_gui (sess->gui, sess, changes);
list = list->next;
}
}
void void
fe_set_typing (session *sess, const char *nick, const char *state) fe_set_typing (session *sess, const char *nick, const char *state)
{ {
+18
View File
@@ -37,6 +37,24 @@ GtkWidget *mg_create_generic_tab (char *name, char *title, int force_toplevel, i
void mg_set_title (GtkWidget *button, char *title); void mg_set_title (GtkWidget *button, char *title);
void mg_set_access_icon (session_gui *gui, GdkPixbuf *pix, gboolean away); void mg_set_access_icon (session_gui *gui, GdkPixbuf *pix, gboolean away);
void mg_apply_setup (void); void mg_apply_setup (void);
/* which parts of the GUI need a live rebuild after a preferences change */
typedef struct
{
unsigned int chanview:1; /* rebuild the tab bar / channel tree */
unsigned int tab_resort:1; /* re-sort channel tabs */
unsigned int tab_trunc:1; /* re-truncate tab labels */
unsigned int ulist_columns:1; /* rebuild user list columns */
unsigned int ulist_rows:1; /* re-fill user list rows */
unsigned int ulist_sort:1; /* re-apply user list sort order */
unsigned int ulist_count:1; /* user count label visibility */
unsigned int meters:1; /* rebuild lag/throttle meters */
unsigned int input_box:1; /* nick box visibility and access icon */
unsigned int topic_bar:1; /* topic wrap mode and mode button placement */
unsigned int transparency:1; /* main window opacity */
} mg_live_prefs;
void mg_apply_live_prefs (const mg_live_prefs *changes);
void mg_apply_session_font_prefs (session_gui *gui); void mg_apply_session_font_prefs (session_gui *gui);
void mg_close_sess (session *); void mg_close_sess (session *);
void mg_tab_close (session *sess); void mg_tab_close (session *sess);
+15 -1
View File
@@ -50,7 +50,7 @@ zoitechat_gtk_deps = [
gtk_dep = dependency('gtk+-3.0', version: '>= 3.22') gtk_dep = dependency('gtk+-3.0', version: '>= 3.22')
zoitechat_theme_deps = [gtk_dep] zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep]
if host_machine.system() != 'windows' if host_machine.system() != 'windows'
appindicator_opt = get_option('appindicator') appindicator_opt = get_option('appindicator')
@@ -227,6 +227,20 @@ test('Theme Access Routing Tests', theme_access_tests,
timeout: 120, timeout: 120,
) )
theme_gtk3_settings_tests = executable('theme_gtk3_settings_tests',
[
'theme/tests/test-theme-gtk3-settings.c',
'theme/theme-gtk3.c',
],
include_directories: [config_h_include],
dependencies: [gtk_dep],
)
test('Theme GTK3 Settings Tests', theme_gtk3_settings_tests,
protocol: 'tap',
timeout: 120,
)
theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests', theme_preferences_gtk3_populate_tests = executable('theme_preferences_gtk3_populate_tests',
'theme/tests/test-theme-preferences-gtk3-populate.c', 'theme/tests/test-theme-preferences-gtk3-populate.c',
include_directories: [config_h_include], include_directories: [config_h_include],
+59 -17
View File
@@ -88,6 +88,7 @@ static GtkWidget *edit_entry_real;
static GtkWidget *edit_entry_pass; static GtkWidget *edit_entry_pass;
static GtkWidget *edit_check_show_pass; static GtkWidget *edit_check_show_pass;
static GtkWidget *edit_check_use_keyring; static GtkWidget *edit_check_use_keyring;
static GtkWidget *edit_check_ask_pass;
static GtkWidget *edit_button_encrypt_pass; static GtkWidget *edit_button_encrypt_pass;
static GtkWidget *edit_button_import_pass; static GtkWidget *edit_button_import_pass;
static int edit_pass_changed; static int edit_pass_changed;
@@ -122,7 +123,11 @@ servlist_update_password_tools (ircnet *net)
return; return;
use_keyring = net && (net->flags & FLAG_USE_KEYRING); use_keyring = net && (net->flags & FLAG_USE_KEYRING);
has_local = net && net->pass && *net->pass && !use_keyring && !edit_pass_changed; if ((net && (net->flags & FLAG_PROMPT_PASSWORD)) ||
(edit_check_ask_pass && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass))))
use_keyring = FALSE;
has_local = net && net->pass && *net->pass && !use_keyring && !edit_pass_changed &&
(!edit_check_ask_pass || !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass)));
gtk_widget_set_sensitive (edit_button_encrypt_pass, has_local && !servlist_password_is_encrypted (net->pass)); gtk_widget_set_sensitive (edit_button_encrypt_pass, has_local && !servlist_password_is_encrypted (net->pass));
gtk_widget_set_sensitive (edit_button_import_pass, has_local); gtk_widget_set_sensitive (edit_button_import_pass, has_local);
} }
@@ -135,6 +140,17 @@ servlist_entry_set_text_silent (GtkWidget *entry, const char *text)
g_signal_handlers_unblock_by_func (G_OBJECT (entry), G_CALLBACK (servlist_password_changed_cb), NULL); g_signal_handlers_unblock_by_func (G_OBJECT (entry), G_CALLBACK (servlist_password_changed_cb), NULL);
} }
static void
servlist_toggle_ask_pass_cb (GtkToggleButton *toggle, gpointer userdata)
{
gboolean active = gtk_toggle_button_get_active (toggle);
gtk_widget_set_sensitive (edit_check_use_keyring, !active);
gtk_widget_set_sensitive (edit_entry_pass, !active && (!selected_net || selected_net->logintype != LOGIN_SASLEXTERNAL));
gtk_widget_set_sensitive (edit_check_show_pass, !active);
servlist_update_password_tools (selected_net);
}
static char * static char *
servlist_display_password (ircnet *net) servlist_display_password (ircnet *net)
{ {
@@ -1261,6 +1277,7 @@ static void
servlist_edit_update (ircnet *net) servlist_edit_update (ircnet *net)
{ {
gboolean use_keyring; gboolean use_keyring;
gboolean ask_pass;
gboolean keyring_changed; gboolean keyring_changed;
char *password = NULL; char *password = NULL;
servlist_update_from_entry (&net->nick, edit_entry_nick); servlist_update_from_entry (&net->nick, edit_entry_nick);
@@ -1269,8 +1286,23 @@ servlist_edit_update (ircnet *net)
servlist_update_from_entry (&net->real, edit_entry_real); servlist_update_from_entry (&net->real, edit_entry_real);
if (net && net->name) if (net && net->name)
{ {
use_keyring = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring)); ask_pass = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass));
use_keyring = !ask_pass && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring));
keyring_changed = !!(net->flags & FLAG_USE_KEYRING) != !!use_keyring; keyring_changed = !!(net->flags & FLAG_USE_KEYRING) != !!use_keyring;
if (ask_pass)
{
secretstore_delete_network_password (net->name);
if (net->pass)
{
memset (net->pass, 0, strlen (net->pass));
g_free (net->pass);
net->pass = NULL;
}
net->flags &= ~FLAG_USE_KEYRING;
net->flags |= FLAG_PROMPT_PASSWORD;
return;
}
net->flags &= ~FLAG_PROMPT_PASSWORD;
if (!edit_pass_changed && !keyring_changed) if (!edit_pass_changed && !keyring_changed)
return; return;
if (edit_pass_changed) if (edit_pass_changed)
@@ -1338,6 +1370,7 @@ servlist_edit_close_cb (GtkWidget *button, gpointer userdata)
edit_win = NULL; edit_win = NULL;
edit_entry_pass = NULL; edit_entry_pass = NULL;
edit_check_show_pass = NULL; edit_check_show_pass = NULL;
edit_check_ask_pass = NULL;
edit_button_encrypt_pass = NULL; edit_button_encrypt_pass = NULL;
edit_button_import_pass = NULL; edit_button_import_pass = NULL;
} }
@@ -2565,7 +2598,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
/* Checkboxes and entries */ /* Checkboxes and entries */
table3 = gtkutil_grid_new (17, 2, FALSE); table3 = gtkutil_grid_new (19, 2, FALSE);
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
gtk_grid_set_row_spacing (GTK_GRID (table3), 2); gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
gtk_grid_set_column_spacing (GTK_GRID (table3), 8); gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
@@ -2584,34 +2617,43 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
#endif #endif
servlist_create_check (1, net->flags & FLAG_USE_GLOBAL, table3, 5, 0, _("Use global user information")); servlist_create_check (1, net->flags & FLAG_USE_GLOBAL, table3, 5, 0, _("Use global user information"));
edit_check_ask_pass = gtk_check_button_new_with_mnemonic (_("Ask for password on connect"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass), net->flags & FLAG_PROMPT_PASSWORD);
servlist_table_attach (table3, edit_check_ask_pass, 0, 2, 6, 7,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_check_ask_pass), "toggled",
G_CALLBACK (servlist_toggle_ask_pass_cb), NULL);
edit_check_use_keyring = gtk_check_button_new_with_mnemonic (_("Use system keyring")); edit_check_use_keyring = gtk_check_button_new_with_mnemonic (_("Use system keyring"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring), net->flags & FLAG_USE_KEYRING); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring), net->flags & FLAG_USE_KEYRING);
servlist_table_attach (table3, edit_check_use_keyring, 0, 2, 6, 7, servlist_table_attach (table3, edit_check_use_keyring, 0, 2, 7, 8,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING); SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_check_use_keyring), "toggled", g_signal_connect (G_OBJECT (edit_check_use_keyring), "toggled",
G_CALLBACK (servlist_toggle_keyring_cb), NULL); G_CALLBACK (servlist_toggle_keyring_cb), NULL);
edit_entry_nick = servlist_create_entry (table3, _("_Nick name:"), 7, net->nick, &edit_label_nick, 0); edit_entry_nick = servlist_create_entry (table3, _("_Nick name:"), 8, net->nick, &edit_label_nick, 0);
edit_entry_nick2 = servlist_create_entry (table3, _("Second choice:"), 8, net->nick2, &edit_label_nick2, 0); edit_entry_nick2 = servlist_create_entry (table3, _("Second choice:"), 9, net->nick2, &edit_label_nick2, 0);
edit_entry_real = servlist_create_entry (table3, _("Rea_l name:"), 9, net->real, &edit_label_real, 0); edit_entry_real = servlist_create_entry (table3, _("Rea_l name:"), 10, net->real, &edit_label_real, 0);
edit_entry_user = servlist_create_entry (table3, _("_User name:"), 10, net->user, &edit_label_user, 0); edit_entry_user = servlist_create_entry (table3, _("_User name:"), 11, net->user, &edit_label_user, 0);
label_logintype = gtk_label_new (_("Login method:")); label_logintype = gtk_label_new (_("Login method:"));
servlist_table_attach (table3, label_logintype, 0, 1, 11, 12, servlist_table_attach (table3, label_logintype, 0, 1, 12, 13,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING); SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label_logintype, GTK_ALIGN_START); gtk_widget_set_halign (label_logintype, GTK_ALIGN_START);
gtk_widget_set_valign (label_logintype, GTK_ALIGN_CENTER); gtk_widget_set_valign (label_logintype, GTK_ALIGN_CENTER);
combobox_logintypes = servlist_create_logintypecombo (notebook); combobox_logintypes = servlist_create_logintypecombo (notebook);
servlist_table_attach (table3, combobox_logintypes, 1, 2, 11, 12, servlist_table_attach (table3, combobox_logintypes, 1, 2, 12, 13,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
4, 2); 4, 2);
edit_entry_pass = servlist_create_entry (table3, _("Password:"), 12, NULL, 0, _("Password used for login. If in doubt, leave blank.")); edit_entry_pass = servlist_create_entry (table3, _("Password:"), 13, NULL, 0, _("Password used for login. If in doubt, leave blank."));
if (edit_loaded_password) if (edit_loaded_password)
{ {
memset (edit_loaded_password, 0, strlen (edit_loaded_password)); memset (edit_loaded_password, 0, strlen (edit_loaded_password));
@@ -2644,7 +2686,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
if (selected_net && selected_net->logintype == LOGIN_SASLEXTERNAL) if (selected_net && selected_net->logintype == LOGIN_SASLEXTERNAL)
gtk_widget_set_sensitive (edit_entry_pass, FALSE); gtk_widget_set_sensitive (edit_entry_pass, FALSE);
edit_check_show_pass = gtk_check_button_new_with_mnemonic (_("Show password")); edit_check_show_pass = gtk_check_button_new_with_mnemonic (_("Show password"));
servlist_table_attach (table3, edit_check_show_pass, 0, 2, 13, 14, servlist_table_attach (table3, edit_check_show_pass, 0, 2, 14, 15,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
4, 2); 4, 2);
@@ -2652,14 +2694,14 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
G_CALLBACK (servlist_toggle_show_password_cb), edit_entry_pass); G_CALLBACK (servlist_toggle_show_password_cb), edit_entry_pass);
edit_button_encrypt_pass = gtk_button_new_with_mnemonic (_("Encrypt saved password")); edit_button_encrypt_pass = gtk_button_new_with_mnemonic (_("Encrypt saved password"));
servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 14, 15, servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 15, 16,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING); SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_button_encrypt_pass), "clicked", g_signal_connect (G_OBJECT (edit_button_encrypt_pass), "clicked",
G_CALLBACK (servlist_encrypt_password_cb), net); G_CALLBACK (servlist_encrypt_password_cb), net);
edit_button_import_pass = gtk_button_new_with_mnemonic (_("Move password to keyring")); edit_button_import_pass = gtk_button_new_with_mnemonic (_("Move password to keyring"));
servlist_table_attach (table3, edit_button_import_pass, 1, 2, 14, 15, servlist_table_attach (table3, edit_button_import_pass, 1, 2, 15, 16,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
4, 2); 4, 2);
@@ -2667,14 +2709,14 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
G_CALLBACK (servlist_import_password_cb), net); G_CALLBACK (servlist_import_password_cb), net);
label34 = gtk_label_new (_("Character set:")); label34 = gtk_label_new (_("Character set:"));
servlist_table_attach (table3, label34, 0, 1, 15, 16, servlist_table_attach (table3, label34, 0, 1, 16, 17,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING); SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label34, GTK_ALIGN_START); gtk_widget_set_halign (label34, GTK_ALIGN_START);
gtk_widget_set_valign (label34, GTK_ALIGN_CENTER); gtk_widget_set_valign (label34, GTK_ALIGN_CENTER);
comboboxentry_charset = servlist_create_charsetcombo (); comboboxentry_charset = servlist_create_charsetcombo ();
servlist_table_attach (table3, comboboxentry_charset, 1, 2, 15, 16, servlist_table_attach (table3, comboboxentry_charset, 1, 2, 16, 17,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
4, 2); 4, 2);
@@ -2700,7 +2742,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
G_CALLBACK (servlist_delete_client_cert_cb), net); G_CALLBACK (servlist_delete_client_cert_cb), net);
gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_delete, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_delete, FALSE, FALSE, 0);
servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 16, 17, servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 17, 18,
FALSE, FALSE, FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER, SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING); SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
+73 -35
View File
@@ -1045,28 +1045,30 @@ setup_create_spin (GtkWidget *table, int row, const setting *set)
return wid; return wid;
} }
static guint setup_trans_apply_tag = 0;
/* the window opacity the dialog opened with, so cancelling can undo the
* live preview done by the slider */
static int setup_transparency_snapshot;
static gint static gint
setup_apply_trans (int *tag) setup_apply_trans (gpointer userdata)
{ {
prefs.hex_gui_transparency = setup_prefs.hex_gui_transparency; prefs.hex_gui_transparency = setup_prefs.hex_gui_transparency;
if (current_sess)
gtk_widget_set_opacity (current_sess->gui->window, gtk_widget_set_opacity (current_sess->gui->window,
(prefs.hex_gui_transparency / 255.)); (prefs.hex_gui_transparency / 255.));
*tag = 0; setup_trans_apply_tag = 0;
return 0; return 0;
} }
static void static void
setup_hscale_cb (GtkRange *wid, const setting *set) setup_hscale_cb (GtkRange *wid, const setting *set)
{ {
static int tag = 0;
setup_set_int (&setup_prefs, set, (int) gtk_range_get_value (wid)); setup_set_int (&setup_prefs, set, (int) gtk_range_get_value (wid));
if (tag == 0) if (setup_trans_apply_tag == 0)
{ setup_trans_apply_tag = g_idle_add ((GSourceFunc) setup_apply_trans, NULL);
tag = g_idle_add ((GSourceFunc) setup_apply_trans, &tag);
}
} }
static void static void
@@ -1091,7 +1093,8 @@ setup_create_hscale (GtkWidget *table, int row, const setting *set)
#ifndef WIN32 /* Windows always supports this */ #ifndef WIN32 /* Windows always supports this */
/* Only used for transparency currently */ /* Only used for transparency currently */
if (!gdk_screen_is_composited (gtk_widget_get_screen (current_sess->gui->window))) if (current_sess &&
!gdk_screen_is_composited (gtk_widget_get_screen (current_sess->gui->window)))
gtk_widget_set_sensitive (wid, FALSE); gtk_widget_set_sensitive (wid, FALSE);
#endif #endif
} }
@@ -2293,7 +2296,9 @@ setup_apply (struct zoitechatprefs *pr)
ThemeChangedEvent event; ThemeChangedEvent event;
struct zoitechatprefs old_prefs = prefs; struct zoitechatprefs old_prefs = prefs;
int old_dark_mode = prefs.hex_gui_dark_mode; int old_dark_mode = prefs.hex_gui_dark_mode;
mg_live_prefs live_changes;
memset (&live_changes, 0, sizeof (live_changes));
#define DIFF(a) (pr->a != prefs.a) #define DIFF(a) (pr->a != prefs.a)
@@ -2301,40 +2306,49 @@ setup_apply (struct zoitechatprefs *pr)
if (DIFF (hex_gui_lang)) if (DIFF (hex_gui_lang))
noapply = TRUE; noapply = TRUE;
#endif #endif
if (DIFF (hex_gui_compact)) /* server tabs are only created when a connection is opened, so
noapply = TRUE; existing connections keep their current tabs until a restart;
if (DIFF (hex_gui_input_icon)) the channel tree indentation is still re-applied below */
noapply = TRUE;
if (DIFF (hex_gui_input_nick))
noapply = TRUE;
if (DIFF (hex_gui_lagometer))
noapply = TRUE;
if (DIFF (hex_gui_mode_buttons_inline))
noapply = TRUE;
if (DIFF (hex_gui_tab_icons))
noapply = TRUE;
if (DIFF (hex_gui_tab_closebuttons))
noapply = TRUE;
if (DIFF (hex_gui_tab_server)) if (DIFF (hex_gui_tab_server))
{
noapply = TRUE; noapply = TRUE;
if (DIFF (hex_gui_tab_small)) live_changes.chanview = TRUE;
noapply = TRUE; }
if (DIFF (hex_gui_compact))
{
live_changes.chanview = TRUE;
live_changes.ulist_columns = TRUE;
}
if (DIFF (hex_gui_input_icon) || DIFF (hex_gui_input_nick))
live_changes.input_box = TRUE;
if (DIFF (hex_gui_lagometer) || DIFF (hex_gui_throttlemeter))
live_changes.meters = TRUE;
if (DIFF (hex_gui_mode_buttons_inline) || DIFF (hex_gui_topicbar_multiline))
live_changes.topic_bar = TRUE;
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
DIFF (hex_gui_tab_small))
live_changes.chanview = TRUE;
if (DIFF (hex_gui_tab_sort)) if (DIFF (hex_gui_tab_sort))
noapply = TRUE; live_changes.tab_resort = TRUE;
if (DIFF (hex_gui_tab_trunc)) if (DIFF (hex_gui_tab_trunc))
noapply = TRUE; live_changes.tab_trunc = TRUE;
if (DIFF (hex_gui_throttlemeter))
noapply = TRUE;
if (DIFF (hex_gui_topicbar_multiline))
noapply = TRUE;
if (DIFF (hex_gui_ulist_count)) if (DIFF (hex_gui_ulist_count))
noapply = TRUE; live_changes.ulist_count = TRUE;
if (DIFF (hex_gui_ulist_icons)) if (DIFF (hex_gui_ulist_icons))
noapply = TRUE; live_changes.ulist_rows = TRUE;
if (DIFF (hex_gui_ulist_show_hosts)) if (DIFF (hex_gui_ulist_show_hosts))
noapply = TRUE; {
live_changes.ulist_columns = TRUE;
live_changes.ulist_rows = TRUE; /* hostnames may be stale */
}
if (DIFF (hex_gui_ulist_sort)) if (DIFF (hex_gui_ulist_sort))
noapply = TRUE; live_changes.ulist_sort = TRUE;
/* compare against the pre-dialog value: the slider previews the
opacity live on the current window only, so OK still needs to
apply it to the other windows */
if (pr->hex_gui_transparency != setup_transparency_snapshot)
live_changes.transparency = TRUE;
if ((pr->hex_gui_tab_pos == 5 || pr->hex_gui_tab_pos == 6) && 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) pr->hex_gui_tab_layout == 2 && pr->hex_gui_tab_pos != prefs.hex_gui_tab_pos)
@@ -2378,6 +2392,8 @@ setup_apply (struct zoitechatprefs *pr)
theme_manager_dispatch_setup_apply (&event); theme_manager_dispatch_setup_apply (&event);
mg_apply_live_prefs (&live_changes);
if (noapply) if (noapply)
fe_message (_("Some settings were changed that require a" fe_message (_("Some settings were changed that require a"
" restart to take full effect."), FE_MSG_WARN); " restart to take full effect."), FE_MSG_WARN);
@@ -2410,6 +2426,9 @@ setup_ok_cb (GtkWidget *but, GtkWidget *win)
if (save_result.success) if (save_result.success)
{ {
theme_preferences_stage_commit (); theme_preferences_stage_commit ();
/* the previewed values were accepted, so the close handler
must not revert them */
setup_transparency_snapshot = prefs.hex_gui_transparency;
gtk_widget_destroy (win); gtk_widget_destroy (win);
return; return;
} }
@@ -2472,6 +2491,24 @@ setup_close_cb (GtkWidget *win, GtkWidget **swin)
{ {
*swin = NULL; *swin = NULL;
/* drop any pending transparency preview and undo the previewed
opacity when the dialog is cancelled; after OK the snapshot
was updated so this is a no-op */
if (setup_trans_apply_tag)
{
g_source_remove (setup_trans_apply_tag);
setup_trans_apply_tag = 0;
}
if (prefs.hex_gui_transparency != setup_transparency_snapshot)
{
mg_live_prefs revert_changes;
prefs.hex_gui_transparency = setup_transparency_snapshot;
memset (&revert_changes, 0, sizeof (revert_changes));
revert_changes.transparency = TRUE;
mg_apply_live_prefs (&revert_changes);
}
theme_preferences_stage_discard (); theme_preferences_stage_discard ();
if (font_dialog) if (font_dialog)
@@ -2493,6 +2530,7 @@ setup_open (void)
memcpy (&setup_prefs, &prefs, sizeof (prefs)); memcpy (&setup_prefs, &prefs, sizeof (prefs));
color_change = FALSE; color_change = FALSE;
setup_transparency_snapshot = prefs.hex_gui_transparency;
theme_preferences_stage_begin (); theme_preferences_stage_begin ();
setup_window = setup_window_open (); setup_window = setup_window_open ();
@@ -23,6 +23,7 @@
#include <math.h> #include <math.h>
#include "../theme-access.h" #include "../theme-access.h"
#include "../theme-css.h"
#include "../theme-manager.h" #include "../theme-manager.h"
#include "../theme-runtime.h" #include "../theme-runtime.h"
#include "../../xtext-color.h" #include "../../xtext-color.h"
@@ -54,6 +55,13 @@ setup_apply_real (const ThemeChangedEvent *event)
(void) event; (void) event;
} }
void
gtkutil_append_font_css (GString *css, const PangoFontDescription *font_desc)
{
(void) css;
(void) font_desc;
}
gboolean gboolean
fe_dark_mode_is_enabled_for (unsigned int mode) fe_dark_mode_is_enabled_for (unsigned int mode)
{ {
@@ -173,6 +181,14 @@ theme_gtk3_is_active (void)
return stub_gtk3_active; return stub_gtk3_active;
} }
gboolean
theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color)
{
(void) name;
(void) out_color;
return FALSE;
}
static gboolean static gboolean
rgba_equal (const GdkRGBA *a, const GdkRGBA *b) rgba_equal (const GdkRGBA *a, const GdkRGBA *b)
{ {
@@ -336,6 +352,79 @@ test_access_xtext_palette_widget_mapping_when_gtk3_active (void)
g_object_unref (provider); g_object_unref (provider);
} }
static void
test_access_palette_css_does_not_feed_back_into_sampling (void)
{
GtkWidget *window;
GtkWidget *label;
GtkCssProvider *theme1;
GtkCssProvider *theme2;
GdkScreen *screen;
XTextColor palette[2] = { 0 };
GdkRGBA old_bg;
GdkRGBA old_fg;
GdkRGBA expected;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
reset_stubs ();
stub_gtk3_active = TRUE;
screen = gdk_screen_get_default ();
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
label = gtk_label_new ("mapped");
gtk_container_add (GTK_CONTAINER (window), label);
gtk_widget_realize (window);
/* Old GTK3 theme active at screen level. */
theme1 = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme1,
"label { color: #112233; background-color: #445566; }"
"label:selected { color: #778899; background-color: #aabbcc; }",
-1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme1), GTK_STYLE_PROVIDER_PRIORITY_USER);
/* The app paints the sampled theme colors onto the widget, like the
* user list / channel tree / input box palette provider does. */
g_assert_true (gdk_rgba_parse (&old_bg, "#445566"));
g_assert_true (gdk_rgba_parse (&old_fg, "#112233"));
theme_css_apply_palette_widget (label, &old_bg, &old_fg, NULL);
/* The user switches to a new GTK3 theme. */
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (theme1));
theme2 = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme2,
"label { color: #00ff00; background-color: #ff0000; }"
"label:selected { color: #0000ff; background-color: #ffff00; }",
-1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme2), GTK_STYLE_PROVIDER_PRIORITY_USER);
gtk_style_context_reset_widgets (screen);
/* Re-sampling must see the new theme, not the palette colors the
* app applied for the old theme. */
theme_get_xtext_colors_for_widget (label, palette, G_N_ELEMENTS (palette));
g_assert_true (stub_last_gtk_map_valid);
g_assert_true (gdk_rgba_parse (&expected, "#00ff00"));
g_assert_true (rgba_close (&stub_last_gtk_map.text_foreground, &expected));
g_assert_true (gdk_rgba_parse (&expected, "#ff0000"));
g_assert_true (rgba_close (&stub_last_gtk_map.text_background, &expected));
/* Sampling must not permanently strip the palette class. */
g_assert_true (gtk_style_context_has_class (gtk_widget_get_style_context (label),
theme_css_palette_class_name ()));
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (theme2));
gtk_widget_destroy (window);
g_object_unref (theme1);
g_object_unref (theme2);
}
static void static void
test_access_dark_light_switch_affects_token_consumers (void) test_access_dark_light_switch_affects_token_consumers (void)
{ {
@@ -363,6 +452,8 @@ main (int argc, char **argv)
g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding); g_test_add_func ("/theme/access/xtext_palette_forwarding", test_access_xtext_palette_forwarding);
g_test_add_func ("/theme/access/xtext_palette_widget_mapping_when_gtk3_active", g_test_add_func ("/theme/access/xtext_palette_widget_mapping_when_gtk3_active",
test_access_xtext_palette_widget_mapping_when_gtk3_active); test_access_xtext_palette_widget_mapping_when_gtk3_active);
g_test_add_func ("/theme/access/palette_css_does_not_feed_back_into_sampling",
test_access_palette_css_does_not_feed_back_into_sampling);
g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding); g_test_add_func ("/theme/access/widget_style_forwarding", test_access_widget_style_forwarding);
g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers", g_test_add_func ("/theme/access/dark_light_switch_affects_token_consumers",
test_access_dark_light_switch_affects_token_consumers); test_access_dark_light_switch_affects_token_consumers);
@@ -24,6 +24,7 @@
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <gio/gio.h>
#include "../theme-gtk3.h" #include "../theme-gtk3.h"
#include "../../../common/gtk3-theme-service.h" #include "../../../common/gtk3-theme-service.h"
@@ -39,11 +40,14 @@ static char *xdg_data_home;
static char *theme_parent_root; static char *theme_parent_root;
static char *theme_child_root; static char *theme_child_root;
static char *theme_switch_root; static char *theme_switch_root;
static char *theme_chrome_root;
static gboolean stub_system_prefers_dark;
gboolean gboolean
theme_policy_system_prefers_dark (void) theme_policy_system_prefers_dark (void)
{ {
return FALSE; return stub_system_prefers_dark;
} }
static void static void
@@ -131,6 +135,8 @@ zoitechat_gtk3_theme_find_by_id (const char *theme_id)
return make_theme (theme_id, theme_child_root); return make_theme (theme_id, theme_child_root);
if (g_strcmp0 (theme_id, "switch") == 0) if (g_strcmp0 (theme_id, "switch") == 0)
return make_theme (theme_id, theme_switch_root); return make_theme (theme_id, theme_switch_root);
if (g_strcmp0 (theme_id, "chrome") == 0)
return make_theme (theme_id, theme_chrome_root);
return NULL; return NULL;
} }
@@ -177,6 +183,11 @@ zoitechat_gtk3_theme_build_inheritance_chain (const char *theme_root)
g_ptr_array_add (chain, g_strdup (theme_switch_root)); g_ptr_array_add (chain, g_strdup (theme_switch_root));
return chain; return chain;
} }
if (g_strcmp0 (theme_root, theme_chrome_root) == 0)
{
g_ptr_array_add (chain, g_strdup (theme_chrome_root));
return chain;
}
g_ptr_array_unref (chain); g_ptr_array_unref (chain);
return NULL; return NULL;
} }
@@ -247,6 +258,22 @@ setup_themes (void)
"gtk-enable-animations=false\n" "gtk-enable-animations=false\n"
"gtk-cursor-blink-time=444\n"); "gtk-cursor-blink-time=444\n");
theme_chrome_root = g_build_filename (temp_root, "chrome", NULL);
g_assert_cmpint (g_mkdir_with_parents (theme_chrome_root, 0700), ==, 0);
ensure_css_dir (theme_chrome_root, "gtk-3.24");
/* Defines named colors but no window/menubar/menu rules of its own,
* so all chrome styling must come from the backfill provider. */
{
char *css = g_build_filename (theme_chrome_root, "gtk-3.24", "gtk.css", NULL);
write_file (css,
"@define-color theme_bg_color #e8f4e8;\n"
"@define-color theme_fg_color #102010;\n"
"@define-color theme_selected_bg_color #3465a4;\n"
"@define-color theme_selected_fg_color #ffffff;\n"
"label { color: #102010; }\n");
g_free (css);
}
path = g_build_filename (theme_parent_root, "index.theme", NULL); path = g_build_filename (theme_parent_root, "index.theme", NULL);
write_file (path, "[Desktop Entry]\nName=parent\n"); write_file (path, "[Desktop Entry]\nName=parent\n");
g_free (path); g_free (path);
@@ -266,11 +293,13 @@ teardown_themes (void)
g_free (theme_parent_root); g_free (theme_parent_root);
g_free (theme_child_root); g_free (theme_child_root);
g_free (theme_switch_root); g_free (theme_switch_root);
g_free (theme_chrome_root);
g_free (xdg_data_home); g_free (xdg_data_home);
g_free (temp_root); g_free (temp_root);
theme_parent_root = NULL; theme_parent_root = NULL;
theme_child_root = NULL; theme_child_root = NULL;
theme_switch_root = NULL; theme_switch_root = NULL;
theme_chrome_root = NULL;
xdg_data_home = NULL; xdg_data_home = NULL;
temp_root = NULL; temp_root = NULL;
} }
@@ -343,6 +372,205 @@ test_settings_restored_on_disable_and_switch (void)
g_assert_false (theme_gtk3_is_active ()); g_assert_false (theme_gtk3_is_active ());
} }
static void
test_follow_system_variant_resolves_to_theme_variant (void)
{
GError *error = NULL;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
/* An explicitly selected in-app theme must not follow the OS
* light/dark preference: the legacy FOLLOW_SYSTEM variant resolves
* to the variant the theme itself provides, so the window chrome
* (menu bar) stays on the theme even when the system prefers dark. */
stub_system_prefers_dark = TRUE;
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_FOLLOW_SYSTEM, &error));
g_assert_no_error (error);
g_assert_true (theme_gtk3_is_active ());
g_assert_cmpint (theme_gtk3_active_variant (), ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
g_assert_false (get_bool_setting ("gtk-application-prefer-dark-theme"));
stub_system_prefers_dark = FALSE;
theme_gtk3_disable ();
}
static void
test_menu_bar_follows_active_theme_colors (void)
{
GError *error = NULL;
GtkWidget *window;
GtkWidget *menubar;
GtkStyleContext *context;
GdkRGBA bg;
GdkRGBA expected;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
/* The chrome theme defines no menubar rules; without the backfill
* provider the menu bar would fall through to the base (OS) theme
* and follow the OS dark/light mode instead of the selected theme. */
g_assert_true (theme_gtk3_apply ("chrome", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
g_assert_no_error (error);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
menubar = gtk_menu_bar_new ();
gtk_container_add (GTK_CONTAINER (window), menubar);
gtk_widget_show_all (window);
context = gtk_widget_get_style_context (menubar);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_true (gdk_rgba_parse (&expected, "#e8f4e8"));
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
/* The window background must follow the theme too, or the space
* around and beside the menu bar keeps the OS theme's colors. */
context = gtk_widget_get_style_context (window);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
gtk_widget_destroy (window);
theme_gtk3_disable ();
}
static void
test_desktop_color_sync_does_not_leak_into_chrome (void)
{
GError *error = NULL;
GtkWidget *window;
GtkWidget *menubar;
GtkStyleContext *context;
GtkCssProvider *desktop_css;
GdkScreen *screen;
GdkRGBA bg;
GdkRGBA expected;
GdkRGBA injected;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
/* KDE's GTK color-scheme sync (~/.config/gtk-3.0/gtk.css) redefines
* the standard named colors with the OS palette at USER priority.
* The switch theme defines no named colors of its own, so a naive
* @theme_bg_color reference in the chrome backfill would resolve to
* the injected OS dark palette; the backfill must use its neutral
* variant fallback instead. */
screen = gdk_screen_get_default ();
desktop_css = gtk_css_provider_new ();
gtk_css_provider_load_from_data (desktop_css,
"@define-color theme_bg_color #1e1e1e;\n"
"@define-color theme_fg_color #eeeeee;\n"
"menubar { background-color: #1e1e1e; }\n",
-1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (desktop_css), GTK_STYLE_PROVIDER_PRIORITY_USER);
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
g_assert_no_error (error);
{
GtkWidget *item;
GdkRGBA item_bg;
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
menubar = gtk_menu_bar_new ();
item = gtk_menu_item_new_with_label ("File");
gtk_menu_shell_append (GTK_MENU_SHELL (menubar), item);
gtk_container_add (GTK_CONTAINER (window), menubar);
gtk_widget_show_all (window);
context = gtk_widget_get_style_context (menubar);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_true (gdk_rgba_parse (&expected, "#f6f5f4"));
g_assert_true (gdk_rgba_parse (&injected, "#1e1e1e"));
g_assert_cmpfloat (ABS (bg.red - injected.red), >, 0.1);
g_assert_cmpfloat (ABS (bg.red - expected.red), <, 0.01);
g_assert_cmpfloat (ABS (bg.green - expected.green), <, 0.01);
g_assert_cmpfloat (ABS (bg.blue - expected.blue), <, 0.01);
/* The menu bar body and the menu items must be one surface. */
context = gtk_widget_get_style_context (item);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
gtk_style_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &item_bg);
G_GNUC_END_IGNORE_DEPRECATIONS
g_assert_cmpfloat (ABS (item_bg.red - bg.red), <, 0.01);
g_assert_cmpfloat (ABS (item_bg.green - bg.green), <, 0.01);
g_assert_cmpfloat (ABS (item_bg.blue - bg.blue), <, 0.01);
}
gtk_widget_destroy (window);
theme_gtk3_disable ();
gtk_style_context_remove_provider_for_screen (screen, GTK_STYLE_PROVIDER (desktop_css));
g_object_unref (desktop_css);
}
static void
test_stale_theme_symlink_is_replaced (void)
{
GError *error = NULL;
char *themes_dir;
char *link_path;
char *target;
char *active_theme_name = NULL;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
if (!has_default_seat ())
{
g_test_message ("No default seat available");
return;
}
/* A dangling symlink left behind by a removed theme must not block
* pinning gtk-theme-name to the newly applied theme. */
themes_dir = g_build_filename (xdg_data_home, "themes", NULL);
g_assert_cmpint (g_mkdir_with_parents (themes_dir, 0700), ==, 0);
link_path = g_build_filename (themes_dir, "switch", NULL);
g_unlink (link_path);
{
GFile *link_file = g_file_new_for_path (link_path);
g_assert_true (g_file_make_symbolic_link (link_file, "/nonexistent/switch", NULL, NULL));
g_object_unref (link_file);
}
g_assert_true (theme_gtk3_apply ("switch", THEME_GTK3_VARIANT_PREFER_LIGHT, &error));
g_assert_no_error (error);
target = g_file_read_link (link_path, NULL);
g_assert_cmpstr (target, ==, theme_switch_root);
g_object_get (gtk_settings_get_default (), "gtk-theme-name", &active_theme_name, NULL);
g_assert_cmpstr (active_theme_name, ==, "switch");
g_free (active_theme_name);
g_free (target);
g_free (link_path);
g_free (themes_dir);
theme_gtk3_disable ();
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
@@ -354,6 +582,14 @@ main (int argc, char **argv)
g_test_add_func ("/theme/gtk3/settings_layer_precedence", test_settings_layer_precedence); g_test_add_func ("/theme/gtk3/settings_layer_precedence", test_settings_layer_precedence);
g_test_add_func ("/theme/gtk3/settings_restored_on_disable_and_switch", test_settings_restored_on_disable_and_switch); g_test_add_func ("/theme/gtk3/settings_restored_on_disable_and_switch", test_settings_restored_on_disable_and_switch);
g_test_add_func ("/theme/gtk3/follow_system_variant_resolves_to_theme_variant",
test_follow_system_variant_resolves_to_theme_variant);
g_test_add_func ("/theme/gtk3/menu_bar_follows_active_theme_colors",
test_menu_bar_follows_active_theme_colors);
g_test_add_func ("/theme/gtk3/desktop_color_sync_does_not_leak_into_chrome",
test_desktop_color_sync_does_not_leak_into_chrome);
g_test_add_func ("/theme/gtk3/stale_theme_symlink_is_replaced",
test_stale_theme_symlink_is_replaced);
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT; prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_LIGHT;
@@ -89,3 +89,9 @@ theme_gtk3_is_active (void)
{ {
return FALSE; return FALSE;
} }
ThemeGtk3Variant
theme_gtk3_active_variant (void)
{
return THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
}
@@ -33,6 +33,7 @@ InputStyle *input_style;
static gboolean gtk_available; static gboolean gtk_available;
static int apply_current_calls; static int apply_current_calls;
static int set_token_color_calls;
static char applied_theme_id[256]; static char applied_theme_id[256];
static ThemeGtk3Variant applied_variant; static ThemeGtk3Variant applied_variant;
static gboolean removed_selected; static gboolean removed_selected;
@@ -70,6 +71,15 @@ load_text_events (void)
{ {
} }
char **
text_color_event_names (int color, int *count)
{
(void)color;
if (count)
*count = 0;
return NULL;
}
gboolean gboolean
theme_get_color (ThemeSemanticToken token, GdkRGBA *color) theme_get_color (ThemeSemanticToken token, GdkRGBA *color)
{ {
@@ -95,6 +105,7 @@ theme_manager_set_token_color (unsigned int dark_mode, ThemeSemanticToken token,
(void)dark_mode; (void)dark_mode;
(void)token; (void)token;
(void)color; (void)color;
set_token_color_calls++;
if (changed) if (changed)
*changed = FALSE; *changed = FALSE;
} }
@@ -410,6 +421,43 @@ test_select_none_resets_theme_and_applies (void)
gtk_widget_destroy (page); gtk_widget_destroy (page);
} }
static void
test_select_theme_does_not_pin_palette_tokens (void)
{
GtkWidget *page;
theme_preferences_ui *ui;
struct zoitechatprefs setup_prefs;
if (!gtk_available)
{
g_test_message ("GTK display not available");
return;
}
memset (&setup_prefs, 0, sizeof (setup_prefs));
memset (&prefs, 0, sizeof (prefs));
removed_selected = FALSE;
apply_current_calls = 0;
set_token_color_calls = 0;
page = theme_preferences_create_page (NULL, &setup_prefs, NULL);
ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
g_assert_nonnull (ui);
/* 0 = None, 1 = removed-theme, 2 = fallback-theme */
gtk_combo_box_set_active (GTK_COMBO_BOX (ui->gtk3_combo), 2);
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
g_assert_cmpint (apply_current_calls, ==, 1);
/* Selecting a GTK3 theme must leave the mapped palette tokens
* unpinned so the user list, channel tree and input box keep
* resolving their colors from the active theme instead of being
* frozen until the next restart. */
g_assert_cmpint (set_token_color_calls, ==, 0);
gtk_widget_destroy (page);
}
int int
main (int argc, char **argv) main (int argc, char **argv)
{ {
@@ -421,5 +469,7 @@ main (int argc, char **argv)
test_unset_theme_keeps_system_default_without_apply); test_unset_theme_keeps_system_default_without_apply);
g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme", g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme",
test_select_none_resets_theme_and_applies); test_select_none_resets_theme_and_applies);
g_test_add_func ("/theme/preferences/gtk3_select_theme_does_not_pin_palette_tokens",
test_select_theme_does_not_pin_palette_tokens);
return g_test_run (); return g_test_run ();
} }
+29
View File
@@ -20,6 +20,8 @@
#include "theme-access.h" #include "theme-access.h"
#include "theme-css.h"
#include "theme-gtk3.h"
#include "theme-runtime.h" #include "theme-runtime.h"
@@ -116,6 +118,17 @@ theme_access_lookup_named_color (GtkStyleContext *context, const char *const *na
{ {
size_t i; size_t i;
/* With an in-app GTK3 theme selected, prefer the colors resolved from
* the theme itself: desktop integration CSS (e.g. KDE's color-scheme
* sync) redefines the same named colors with the OS palette in the
* widget cascade, which would drag OS dark/light colors into the
* mapped palette. */
for (i = 0; names[i] != NULL; i++)
{
if (theme_gtk3_lookup_theme_color (names[i], out_color))
return TRUE;
}
for (i = 0; names[i] != NULL; i++) for (i = 0; names[i] != NULL; i++)
{ {
if (gtk_style_context_lookup_color (context, names[i], out_color)) if (gtk_style_context_lookup_color (context, names[i], out_color))
@@ -149,6 +162,8 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map
static const char *const selected_bg_fallbacks[] = { "theme_selected_bg_color", NULL }; static const char *const selected_bg_fallbacks[] = { "theme_selected_bg_color", NULL };
static const char *const selected_fg_fallbacks[] = { "theme_selected_fg_color", NULL }; static const char *const selected_fg_fallbacks[] = { "theme_selected_fg_color", NULL };
GtkStyleContext *context; GtkStyleContext *context;
const char *palette_class;
gboolean had_palette_class;
GdkRGBA named; GdkRGBA named;
GdkRGBA accent; GdkRGBA accent;
@@ -160,6 +175,16 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map
if (context == NULL) if (context == NULL)
return FALSE; return FALSE;
/* The application paints these widgets with fixed colors through the
* zoitechat-palette provider. Sampling must see the underlying GTK3
* theme, not the colors we applied earlier: otherwise every theme
* change reads back the previous palette and the user list, channel
* tree and input box keep their stale colors until a restart. */
palette_class = theme_css_palette_class_name ();
had_palette_class = gtk_style_context_has_class (context, palette_class);
if (had_palette_class)
gtk_style_context_remove_class (context, palette_class);
theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground); theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground);
theme_access_resolve_map_background (context, GTK_STATE_FLAG_NORMAL, base_fallbacks, theme_access_resolve_map_background (context, GTK_STATE_FLAG_NORMAL, base_fallbacks,
&out_map->text_background); &out_map->text_background);
@@ -173,6 +198,10 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map
if (accent.alpha <= 0.0) if (accent.alpha <= 0.0)
accent = out_map->selection_background; accent = out_map->selection_background;
out_map->accent = accent; out_map->accent = accent;
if (had_palette_class)
gtk_style_context_add_class (context, palette_class);
out_map->enabled = TRUE; out_map->enabled = TRUE;
return TRUE; return TRUE;
} }
+5 -6
View File
@@ -56,12 +56,11 @@ theme_application_apply_toplevel_theme (gboolean dark)
if (theme_gtk3_is_active ()) if (theme_gtk3_is_active ())
{ {
if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK) /* An explicitly selected in-app theme controls its own
prefer_dark = TRUE; * variant; the OS light/dark preference must not leak into
else if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_LIGHT) * the window chrome (menu bar, dialogs). The active variant
prefer_dark = FALSE; * is always resolved to light or dark. */
else prefer_dark = theme_gtk3_active_variant () == THEME_GTK3_VARIANT_PREFER_DARK;
prefer_dark = theme_policy_system_prefers_dark ();
} }
else else
{ {
+6
View File
@@ -60,6 +60,12 @@ typedef struct
static GtkCssProvider *theme_css_input_provider; static GtkCssProvider *theme_css_input_provider;
static ThemeCssInputFingerprint theme_css_input_fp; static ThemeCssInputFingerprint theme_css_input_fp;
const char *
theme_css_palette_class_name (void)
{
return theme_css_selector_palette_class;
}
void void
theme_css_apply_app_provider (GtkStyleProvider *provider) theme_css_apply_app_provider (GtkStyleProvider *provider)
{ {
+1
View File
@@ -30,6 +30,7 @@
* theme_css_apply_widget_provider: * theme_css_apply_widget_provider:
* Use for widget-local CSS providers attached to a specific widget context. * Use for widget-local CSS providers attached to a specific widget context.
*/ */
const char *theme_css_palette_class_name (void);
void theme_css_apply_app_provider (GtkStyleProvider *provider); void theme_css_apply_app_provider (GtkStyleProvider *provider);
void theme_css_remove_app_provider (GtkStyleProvider *provider); void theme_css_remove_app_provider (GtkStyleProvider *provider);
void theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider); void theme_css_apply_widget_provider (GtkWidget *widget, GtkStyleProvider *provider);
+266 -9
View File
@@ -33,11 +33,60 @@
static GPtrArray *theme_gtk3_providers_base; static GPtrArray *theme_gtk3_providers_base;
static GPtrArray *theme_gtk3_providers_variant; static GPtrArray *theme_gtk3_providers_variant;
static GtkCssProvider *theme_gtk3_chrome_provider;
static GHashTable *theme_gtk3_theme_colors;
static GHashTable *theme_gtk3_provider_cache; static GHashTable *theme_gtk3_provider_cache;
static gboolean theme_gtk3_active; static gboolean theme_gtk3_active;
static char *theme_gtk3_current_id; static char *theme_gtk3_current_id;
static ThemeGtk3Variant theme_gtk3_current_variant; static ThemeGtk3Variant theme_gtk3_current_variant;
/* Menu bar and menus fall back to the base gtk-theme-name theme (the OS
* theme) for any property the selected in-app theme's CSS does not define,
* which mixes OS dark/light colors into the chrome (issue: dark menu bar on
* a light in-app theme when the OS is in dark mode). While an in-app theme
* is active, backfill those widgets with the theme's own named colors. The
* provider sits just below the theme providers, so a theme that styles its
* menus keeps full control. */
#define THEME_GTK3_CHROME_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_USER - 1)
/* Concrete color values are formatted in: desktop integration CSS (e.g.
* KDE's color-scheme sync in ~/.config/gtk-3.0/gtk.css) redefines the
* standard named colors with the OS palette at USER priority, so a
* "@theme_bg_color" reference here would resolve to the OS dark/light
* colors instead of the selected theme's. */
static const char theme_gtk3_chrome_css_template[] =
".background, .background:backdrop {"
" background-color: %s;"
" color: %s;"
"}"
"headerbar, headerbar:backdrop, .titlebar, .titlebar:backdrop {"
" background-color: %s;"
" background-image: none;"
" color: %s;"
"}";
/* The menu bar body and the menu items are separate CSS surfaces; desktop
* integration CSS or a theme can style one but not the other, splitting
* the menu bar row into two colors. These rules sit above the theme and
* the desktop CSS so body and items always render as one surface. */
#define THEME_GTK3_MENU_UNIFORM_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_USER + 40)
static const char theme_gtk3_menu_uniform_template[] =
"menubar, menubar:backdrop, menubar > menuitem, menubar > menuitem:backdrop,"
"menu, menu:backdrop, .menu, .menu:backdrop, menu > menuitem, menu > menuitem:backdrop {"
" background-color: %s;"
" background-image: none;"
" color: %s;"
"}"
"menubar > menuitem:hover, menubar > menuitem:selected,"
"menu > menuitem:hover, menu > menuitem:selected,"
".menu menuitem:hover, .menu menuitem:selected {"
" background-color: %s;"
" background-image: none;"
" color: %s;"
"}"
"menubar > menuitem:disabled, menu > menuitem:disabled {"
" color: alpha(%s, 0.5);"
"}";
typedef struct typedef struct
{ {
GHashTable *defaults; GHashTable *defaults;
@@ -49,6 +98,7 @@ typedef struct
static ThemeGtk3SettingsState theme_gtk3_settings_state; static ThemeGtk3SettingsState theme_gtk3_settings_state;
static gboolean settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value); static gboolean settings_apply_property (GtkSettings *settings, const char *property_name, const char *raw_value);
static void theme_gtk3_menu_uniform_apply (const char *bg, const char *fg, const char *sel_bg, const char *sel_fg);
static gboolean static gboolean
theme_gtk3_theme_name_is_dark (const char *name) theme_gtk3_theme_name_is_dark (const char *name)
@@ -399,6 +449,21 @@ settings_theme_link_search_path (const char *theme_root, const char *theme_name)
} }
link_path = g_build_filename (themes_root, theme_name, NULL); link_path = g_build_filename (themes_root, theme_name, NULL);
/* A leftover symlink from a removed or relocated theme would satisfy
* the EXISTS check while pointing at the wrong (or no) theme, leaving
* the base gtk-theme-name on the OS theme. Replace it. */
{
char *link_target = g_file_read_link (link_path, NULL);
if (link_target)
{
if (g_strcmp0 (link_target, theme_root) != 0 ||
!g_file_test (link_path, G_FILE_TEST_IS_DIR))
g_unlink (link_path);
g_free (link_target);
}
}
if (!g_file_test (link_path, G_FILE_TEST_EXISTS)) if (!g_file_test (link_path, G_FILE_TEST_EXISTS))
{ {
GFile *link_file = g_file_new_for_path (link_path); GFile *link_file = g_file_new_for_path (link_path);
@@ -757,12 +822,176 @@ settings_apply_from_file (const char *theme_root, const char *css_dir)
g_ptr_array_unref (settings_paths); g_ptr_array_unref (settings_paths);
} }
static gboolean
theme_gtk3_providers_define_color (const char *name)
{
char *needle = g_strdup_printf ("@define-color %s ", name);
gboolean found = FALSE;
guint i;
for (i = 0; !found && theme_gtk3_providers_base && i < theme_gtk3_providers_base->len; i++)
{
char *dump = gtk_css_provider_to_string (g_ptr_array_index (theme_gtk3_providers_base, i));
found = dump && strstr (dump, needle) != NULL;
g_free (dump);
}
for (i = 0; !found && theme_gtk3_providers_variant && i < theme_gtk3_providers_variant->len; i++)
{
char *dump = gtk_css_provider_to_string (g_ptr_array_index (theme_gtk3_providers_variant, i));
found = dump && strstr (dump, needle) != NULL;
g_free (dump);
}
g_free (needle);
return found;
}
/* Resolve a named color from the selected theme itself. The lookup
* context layers the theme's providers over the screen cascade, so the
* theme's own @define-color wins; when the theme does not define the
* name at all, fall back to a neutral palette matching the theme's
* variant instead of whatever the desktop injected into the cascade. */
static char *
theme_gtk3_chrome_resolve_color (GtkStyleContext *lookup, const char *name, const char *fallback)
{
GdkRGBA color;
if (!theme_gtk3_providers_define_color (name) ||
!gtk_style_context_lookup_color (lookup, name, &color))
g_assert (gdk_rgba_parse (&color, fallback));
if (!theme_gtk3_theme_colors)
theme_gtk3_theme_colors = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
{
GdkRGBA *stored = g_new (GdkRGBA, 1);
*stored = color;
g_hash_table_insert (theme_gtk3_theme_colors, g_strdup (name), stored);
}
return gdk_rgba_to_string (&color);
}
gboolean
theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color)
{
GdkRGBA *stored;
if (!theme_gtk3_active || !theme_gtk3_theme_colors || !name || !out_color)
return FALSE;
stored = g_hash_table_lookup (theme_gtk3_theme_colors, name);
if (!stored)
return FALSE;
*out_color = *stored;
return TRUE;
}
static void
theme_gtk3_chrome_provider_apply (gboolean prefer_dark)
{
GdkScreen *screen = gdk_screen_get_default ();
GtkStyleContext *lookup;
char *bg;
char *fg;
char *sel_bg;
char *sel_fg;
char *css;
guint i;
if (!screen)
return;
lookup = gtk_style_context_new ();
for (i = 0; theme_gtk3_providers_base && i < theme_gtk3_providers_base->len; i++)
gtk_style_context_add_provider (lookup,
GTK_STYLE_PROVIDER (g_ptr_array_index (theme_gtk3_providers_base, i)),
GTK_STYLE_PROVIDER_PRIORITY_USER);
for (i = 0; theme_gtk3_providers_variant && i < theme_gtk3_providers_variant->len; i++)
gtk_style_context_add_provider (lookup,
GTK_STYLE_PROVIDER (g_ptr_array_index (theme_gtk3_providers_variant, i)),
GTK_STYLE_PROVIDER_PRIORITY_USER);
if (theme_gtk3_theme_colors)
g_hash_table_remove_all (theme_gtk3_theme_colors);
bg = theme_gtk3_chrome_resolve_color (lookup, "theme_bg_color",
prefer_dark ? "#242424" : "#f6f5f4");
fg = theme_gtk3_chrome_resolve_color (lookup, "theme_fg_color",
prefer_dark ? "#eeeeee" : "#2e3436");
sel_bg = theme_gtk3_chrome_resolve_color (lookup, "theme_selected_bg_color", "#3584e4");
sel_fg = theme_gtk3_chrome_resolve_color (lookup, "theme_selected_fg_color", "#ffffff");
/* Not used by the chrome CSS itself, but resolved and stored for the
* mapped-palette sampling fallback (chat area and friends). */
g_free (theme_gtk3_chrome_resolve_color (lookup, "theme_base_color",
prefer_dark ? "#1e1e1e" : "#ffffff"));
g_object_unref (lookup);
css = g_strdup_printf (theme_gtk3_chrome_css_template,
bg, fg,
bg, fg);
if (!theme_gtk3_chrome_provider)
theme_gtk3_chrome_provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme_gtk3_chrome_provider, css, -1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_chrome_provider),
THEME_GTK3_CHROME_PRIORITY);
theme_gtk3_menu_uniform_apply (bg, fg, sel_bg, sel_fg);
g_free (css);
g_free (sel_fg);
g_free (sel_bg);
g_free (fg);
g_free (bg);
}
static GtkCssProvider *theme_gtk3_menu_uniform_provider;
static void
theme_gtk3_menu_uniform_apply (const char *bg, const char *fg, const char *sel_bg, const char *sel_fg)
{
GdkScreen *screen = gdk_screen_get_default ();
char *css;
if (!screen)
return;
css = g_strdup_printf (theme_gtk3_menu_uniform_template, bg, fg, sel_bg, sel_fg, fg);
if (!theme_gtk3_menu_uniform_provider)
theme_gtk3_menu_uniform_provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (theme_gtk3_menu_uniform_provider, css, -1, NULL);
gtk_style_context_add_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_menu_uniform_provider),
THEME_GTK3_MENU_UNIFORM_PRIORITY);
g_free (css);
}
static void
theme_gtk3_chrome_provider_remove (void)
{
GdkScreen *screen = gdk_screen_get_default ();
if (!screen)
return;
if (theme_gtk3_chrome_provider)
gtk_style_context_remove_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_chrome_provider));
if (theme_gtk3_menu_uniform_provider)
gtk_style_context_remove_provider_for_screen (screen,
GTK_STYLE_PROVIDER (theme_gtk3_menu_uniform_provider));
}
static void static void
theme_gtk3_remove_provider (void) theme_gtk3_remove_provider (void)
{ {
GdkScreen *screen = gdk_screen_get_default (); GdkScreen *screen = gdk_screen_get_default ();
guint i; guint i;
theme_gtk3_chrome_provider_remove ();
if (screen && theme_gtk3_providers_variant) if (screen && theme_gtk3_providers_variant)
{ {
for (i = 0; i < theme_gtk3_providers_variant->len; i++) for (i = 0; i < theme_gtk3_providers_variant->len; i++)
@@ -866,6 +1095,7 @@ load_css_with_variant (ZoitechatGtk3Theme *theme, ThemeGtk3Variant variant, GErr
} }
g_ptr_array_unref (chain); g_ptr_array_unref (chain);
theme_gtk3_chrome_provider_apply (prefer_dark);
settings_apply_for_variant (variant); settings_apply_for_variant (variant);
theme_gtk3_reset_widgets (); theme_gtk3_reset_widgets ();
theme_gtk3_active = TRUE; theme_gtk3_active = TRUE;
@@ -876,24 +1106,35 @@ static gboolean
theme_gtk3_apply_internal (const char *theme_id, ThemeGtk3Variant variant, gboolean force_reload, GError **error) theme_gtk3_apply_internal (const char *theme_id, ThemeGtk3Variant variant, gboolean force_reload, GError **error)
{ {
ZoitechatGtk3Theme *theme; ZoitechatGtk3Theme *theme;
char *previous_id = g_strdup (theme_gtk3_current_id); char *previous_id;
ThemeGtk3Variant previous_variant = theme_gtk3_current_variant; ThemeGtk3Variant previous_variant;
gboolean had_previous = theme_gtk3_active && previous_id && previous_id[0]; gboolean had_previous;
gboolean ok; gboolean ok;
theme = zoitechat_gtk3_theme_find_by_id (theme_id);
if (!theme)
return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE;
/* An explicitly selected in-app theme pins the rendering to that
* theme: it must not flip with the OS light/dark preference. Legacy
* configs still carry FOLLOW_SYSTEM (the pre-inference default), so
* resolve it to the variant the theme itself provides. */
if (variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
variant = theme_gtk3_infer_variant (theme);
if (!force_reload && if (!force_reload &&
theme_gtk3_active && theme_gtk3_active &&
g_strcmp0 (theme_gtk3_current_id, theme_id) == 0 && g_strcmp0 (theme_gtk3_current_id, theme_id) == 0 &&
theme_gtk3_current_variant == variant) theme_gtk3_current_variant == variant)
return TRUE;
theme = zoitechat_gtk3_theme_find_by_id (theme_id);
if (!theme)
{ {
g_free (previous_id); zoitechat_gtk3_theme_free (theme);
return g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "GTK3 theme not found."), FALSE; return TRUE;
} }
previous_id = g_strdup (theme_gtk3_current_id);
previous_variant = theme_gtk3_current_variant;
had_previous = theme_gtk3_active && previous_id && previous_id[0];
theme_gtk3_remove_provider (); theme_gtk3_remove_provider ();
if (force_reload) if (force_reload)
theme_gtk3_invalidate_provider_cache (); theme_gtk3_invalidate_provider_cache ();
@@ -961,6 +1202,12 @@ void
theme_gtk3_disable (void) theme_gtk3_disable (void)
{ {
theme_gtk3_remove_provider (); theme_gtk3_remove_provider ();
g_clear_object (&theme_gtk3_chrome_provider);
g_clear_pointer (&theme_gtk3_theme_colors, g_hash_table_destroy);
/* Following the system theme: keep the menu bar body and the menu
* items on one surface too, using the system's own colors. */
theme_gtk3_menu_uniform_apply ("@theme_bg_color", "@theme_fg_color",
"@theme_selected_bg_color", "@theme_selected_fg_color");
g_clear_pointer (&theme_gtk3_current_id, g_free); g_clear_pointer (&theme_gtk3_current_id, g_free);
theme_gtk3_invalidate_provider_cache (); theme_gtk3_invalidate_provider_cache ();
g_clear_pointer (&theme_gtk3_provider_cache, g_hash_table_destroy); g_clear_pointer (&theme_gtk3_provider_cache, g_hash_table_destroy);
@@ -990,3 +1237,13 @@ theme_gtk3_is_active (void)
{ {
return theme_gtk3_active; return theme_gtk3_active;
} }
ThemeGtk3Variant
theme_gtk3_active_variant (void)
{
/* Always resolved to PREFER_LIGHT or PREFER_DARK while a theme is
* active; FOLLOW_SYSTEM only when no in-app theme is applied. */
if (!theme_gtk3_active)
return THEME_GTK3_VARIANT_FOLLOW_SYSTEM;
return theme_gtk3_current_variant;
}
+3
View File
@@ -22,6 +22,7 @@
#define ZOITECHAT_THEME_GTK3_H #define ZOITECHAT_THEME_GTK3_H
#include <glib.h> #include <glib.h>
#include <gdk/gdk.h>
typedef enum typedef enum
{ {
@@ -38,5 +39,7 @@ ThemeGtk3Variant theme_gtk3_variant_for_theme (const char *theme_id);
void theme_gtk3_invalidate_provider_cache (void); void theme_gtk3_invalidate_provider_cache (void);
void theme_gtk3_disable (void); void theme_gtk3_disable (void);
gboolean theme_gtk3_is_active (void); gboolean theme_gtk3_is_active (void);
ThemeGtk3Variant theme_gtk3_active_variant (void);
gboolean theme_gtk3_lookup_theme_color (const char *name, GdkRGBA *out_color);
#endif #endif
+1 -5
View File
@@ -590,11 +590,7 @@ theme_manager_apply_platform_window_theme (GtkWidget *window)
context = gtk_widget_get_style_context (window); context = gtk_widget_get_style_context (window);
if (theme_gtk3_is_active ()) if (theme_gtk3_is_active ())
{ dark = theme_gtk3_active_variant () == THEME_GTK3_VARIANT_PREFER_DARK;
dark = prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_DARK;
if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_FOLLOW_SYSTEM)
dark = theme_policy_system_prefers_dark ();
}
else else
dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode); dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
if (context) if (context)
+156 -25
View File
@@ -38,6 +38,7 @@
#include "theme-runtime.h" #include "theme-runtime.h"
extern void load_text_events (void); extern void load_text_events (void);
extern char **text_color_event_names (int color, int *count);
typedef struct typedef struct
{ {
@@ -105,6 +106,11 @@ typedef struct
gboolean staged_dirty[THEME_TOKEN_COUNT]; gboolean staged_dirty[THEME_TOKEN_COUNT];
GdkRGBA snapshot[THEME_TOKEN_COUNT]; GdkRGBA snapshot[THEME_TOKEN_COUNT];
GdkRGBA staged[THEME_TOKEN_COUNT]; GdkRGBA staged[THEME_TOKEN_COUNT];
/* the GTK3 theme selection is previewed live from the combo, so
* remember what was active when the dialog opened in order to
* restore it when the dialog is cancelled */
char gtk3_theme_snapshot[sizeof ((struct zoitechatprefs *) 0)->hex_gui_gtk3_theme];
int gtk3_variant_snapshot;
} theme_preferences_stage_state; } theme_preferences_stage_state;
static theme_preferences_stage_state theme_preferences_stage; static theme_preferences_stage_state theme_preferences_stage;
@@ -203,6 +209,11 @@ theme_preferences_stage_begin (void)
memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage)); memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage));
theme_preferences_stage.active = TRUE; theme_preferences_stage.active = TRUE;
g_strlcpy (theme_preferences_stage.gtk3_theme_snapshot,
prefs.hex_gui_gtk3_theme,
sizeof (theme_preferences_stage.gtk3_theme_snapshot));
theme_preferences_stage.gtk3_variant_snapshot = prefs.hex_gui_gtk3_variant;
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++) for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{ {
GdkRGBA rgba; GdkRGBA rgba;
@@ -236,12 +247,36 @@ theme_preferences_stage_commit (void)
memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage)); memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage));
} }
/* undo a live GTK3 theme preview: put the selection that was active when
* the dialog opened back and re-apply it */
static void
theme_preferences_stage_restore_gtk3 (void)
{
if (g_strcmp0 (prefs.hex_gui_gtk3_theme, theme_preferences_stage.gtk3_theme_snapshot) == 0 &&
prefs.hex_gui_gtk3_variant == theme_preferences_stage.gtk3_variant_snapshot)
return;
g_strlcpy (prefs.hex_gui_gtk3_theme,
theme_preferences_stage.gtk3_theme_snapshot,
sizeof (prefs.hex_gui_gtk3_theme));
prefs.hex_gui_gtk3_variant = theme_preferences_stage.gtk3_variant_snapshot;
theme_gtk3_apply_current (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);
}
void void
theme_preferences_stage_discard (void) theme_preferences_stage_discard (void)
{ {
if (!theme_preferences_stage.active) if (!theme_preferences_stage.active)
return; return;
/* restore the GTK3 theme first so the color snapshot below wins */
theme_preferences_stage_restore_gtk3 ();
theme_preferences_stage_sync_runtime_to_snapshot (); theme_preferences_stage_sync_runtime_to_snapshot ();
memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage)); memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage));
} }
@@ -249,6 +284,9 @@ theme_preferences_stage_discard (void)
static void static void
theme_preferences_show_import_error (GtkWidget *button, const char *message); theme_preferences_show_import_error (GtkWidget *button, const char *message);
static void
theme_preferences_color_set_tooltip (GtkWidget *widget, ThemeSemanticToken token);
static void static void
theme_preferences_manager_row_free (gpointer data) theme_preferences_manager_row_free (gpointer data)
{ {
@@ -487,6 +525,7 @@ theme_preferences_color_response_cb (GtkDialog *dialog, gint response_id, gpoint
data->color_change_flag, data->color_change_flag,
TRUE); TRUE);
theme_preferences_color_button_apply (data->button, &rgba); theme_preferences_color_button_apply (data->button, &rgba);
theme_preferences_color_set_tooltip (data->button, data->token);
theme_preferences_manager_update_preview ((theme_color_manager_ui *) data->manager_ui); theme_preferences_manager_update_preview ((theme_color_manager_ui *) data->manager_ui);
} }
@@ -565,6 +604,113 @@ theme_preferences_token_display_name (ThemeSemanticToken token)
} }
} }
static char *
theme_preferences_color_tooltip_markup (ThemeSemanticToken token)
{
GString *tip;
GdkRGBA rgba;
char *display;
char *hex;
char *escaped;
char **names;
int count = 0;
if (token < THEME_TOKEN_MIRC_0 || token > THEME_TOKEN_MIRC_31)
return NULL;
if (!theme_preferences_staged_get_color (token, &rgba))
return NULL;
tip = g_string_new (NULL);
display = theme_preferences_token_display_name (token);
hex = theme_preferences_format_hex (&rgba);
escaped = g_markup_escape_text (display, -1);
g_string_append_printf (tip,
"<span background='%s'>\xe2\x80\x83\xe2\x80\x83</span> <b>%s</b> <tt><small>%s</small></tt>",
hex, escaped, hex);
g_free (escaped);
g_free (display);
names = text_color_event_names (token - THEME_TOKEN_MIRC_0, &count);
if (names)
{
int line_target;
int line_len = 0;
int i;
display = g_strdup_printf (ngettext ("Used by %d text event",
"Used by %d text events", count), count);
escaped = g_markup_escape_text (display, -1);
g_string_append_printf (tip,
"\n<small><span alpha='60%%'>%s</span></small>\n\n<small>",
escaped);
g_free (escaped);
g_free (display);
/* Explicit line breaks keep the tooltip geometry stable;
* heavily-used colors get wider lines so the full list
* stays on screen. */
if (count <= 12)
line_target = 44;
else if (count <= 40)
line_target = 60;
else
line_target = 76;
for (i = 0; i < count; i++)
{
int name_len = (int) g_utf8_strlen (names[i], -1);
if (i)
{
if (line_len + name_len + 5 > line_target)
{
g_string_append_c (tip, '\n');
line_len = 0;
}
else
{
g_string_append (tip, " \xc2\xb7 ");
line_len += 5;
}
}
escaped = g_markup_escape_text (names[i], -1);
g_string_append (tip, escaped);
line_len += name_len;
g_free (escaped);
}
g_string_append (tip, "</small>");
g_strfreev (names);
}
else
{
escaped = g_markup_escape_text (_("Not used by any text events"), -1);
g_string_append_printf (tip,
"\n<small><span alpha='60%%'><i>%s</i></span></small>",
escaped);
g_free (escaped);
}
g_free (hex);
return g_string_free (tip, FALSE);
}
/* Static markup tooltips only: building tooltip widgets from a
* query-tooltip handler makes GtkTooltip flicker on some setups, so the
* markup is set once and refreshed whenever the color changes. */
static void
theme_preferences_color_set_tooltip (GtkWidget *widget, ThemeSemanticToken token)
{
char *markup = theme_preferences_color_tooltip_markup (token);
if (!markup)
return;
gtk_widget_set_tooltip_markup (widget, markup);
g_free (markup);
}
static void static void
theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA *rgba) theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA *rgba)
{ {
@@ -575,6 +721,7 @@ theme_preferences_manager_row_apply (theme_color_manager_row *row, const GdkRGBA
hex = theme_preferences_format_hex (rgba); hex = theme_preferences_format_hex (rgba);
gtk_entry_set_text (GTK_ENTRY (row->entry), hex); gtk_entry_set_text (GTK_ENTRY (row->entry), hex);
g_free (hex); g_free (hex);
theme_preferences_color_set_tooltip (row->row, row->token);
} }
static void static void
@@ -893,6 +1040,8 @@ theme_preferences_create_color_manager_dialog (GtkWindow *parent, gboolean *colo
if (theme_preferences_staged_get_color (token, &rgba)) if (theme_preferences_staged_get_color (token, &rgba))
theme_preferences_manager_row_apply (row, &rgba); theme_preferences_manager_row_apply (row, &rgba);
theme_preferences_color_set_tooltip (list_row, token);
g_signal_connect (G_OBJECT (button), "clicked", g_signal_connect (G_OBJECT (button), "clicked",
G_CALLBACK (theme_preferences_manager_pick_cb), row); G_CALLBACK (theme_preferences_manager_pick_cb), row);
g_object_set_data (G_OBJECT (button), "zoitechat-theme-color-manager-ui", ui); g_object_set_data (G_OBJECT (button), "zoitechat-theme-color-manager-ui", ui);
@@ -1207,6 +1356,7 @@ theme_preferences_create_color_button (GtkWidget *table,
g_object_set_data (G_OBJECT (but), "zoitechat-color-box", box); g_object_set_data (G_OBJECT (but), "zoitechat-color-box", box);
g_object_set_data (G_OBJECT (but), "zoitechat-theme-token", GINT_TO_POINTER (token)); g_object_set_data (G_OBJECT (but), "zoitechat-theme-token", GINT_TO_POINTER (token));
g_object_set_data (G_OBJECT (but), "zoitechat-theme-color-change", color_change_flag); g_object_set_data (G_OBJECT (but), "zoitechat-theme-color-change", color_change_flag);
theme_preferences_color_set_tooltip (but, token);
gtk_grid_attach (GTK_GRID (table), but, col, row, 1, 1); gtk_grid_attach (GTK_GRID (table), but, col, row, 1, 1);
g_signal_connect (G_OBJECT (but), "clicked", G_CALLBACK (theme_preferences_color_cb), parent); g_signal_connect (G_OBJECT (but), "clicked", G_CALLBACK (theme_preferences_color_cb), parent);
if (theme_preferences_staged_get_color (token, &color)) if (theme_preferences_staged_get_color (token, &color))
@@ -1451,7 +1601,6 @@ theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
{ {
ThemeWidgetStyleValues style_values; ThemeWidgetStyleValues style_values;
GtkWidget *style_source = NULL; GtkWidget *style_source = NULL;
gboolean follows_system_theme = prefs.hex_gui_gtk3_theme[0] == '\0';
if (ui && ui->parent) if (ui && ui->parent)
style_source = GTK_WIDGET (ui->parent); style_source = GTK_WIDGET (ui->parent);
@@ -1462,34 +1611,16 @@ theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
theme_get_widget_style_values_for_widget (style_source, &style_values); theme_get_widget_style_values_for_widget (style_source, &style_values);
if (follows_system_theme) /* Leave the mapped tokens unpinned so the user list, channel tree and
{ * input box keep resolving their colors from whatever GTK3 theme is
/* Following the system GTK3 theme: leave the mapped tokens * active (system or in-app selected), and only refresh what the
* unpinned so they keep tracking the theme, and only refresh * dialog shows. Pinning theme-derived values here freezes them into
* what the dialog shows. */ * the runtime palette (and colors.conf), which left those widgets
* with stale colors until the next restart. */
theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground); theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_FOREGROUND, &style_values.foreground);
theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_BACKGROUND, &style_values.background); theme_preferences_stage_rebase_color (THEME_TOKEN_TEXT_BACKGROUND, &style_values.background);
theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_FOREGROUND, &style_values.selection_foreground); theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_FOREGROUND, &style_values.selection_foreground);
theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_BACKGROUND, &style_values.selection_background); theme_preferences_stage_rebase_color (THEME_TOKEN_SELECTION_BACKGROUND, &style_values.selection_background);
return;
}
theme_preferences_staged_set_color (THEME_TOKEN_TEXT_FOREGROUND,
&style_values.foreground,
NULL,
TRUE);
theme_preferences_staged_set_color (THEME_TOKEN_TEXT_BACKGROUND,
&style_values.background,
NULL,
TRUE);
theme_preferences_staged_set_color (THEME_TOKEN_SELECTION_FOREGROUND,
&style_values.selection_foreground,
NULL,
TRUE);
theme_preferences_staged_set_color (THEME_TOKEN_SELECTION_BACKGROUND,
&style_values.selection_background,
NULL,
TRUE);
} }
static gboolean static gboolean
+80 -8
View File
@@ -796,16 +796,12 @@ userlist_store_color (GtkListStore *store, GtkTreeIter *iter, ThemeSemanticToken
} }
} }
GtkListStore * static void
userlist_create_model (session *sess) userlist_model_set_sort (GtkListStore *store, session *sess)
{ {
GtkListStore *store;
GtkTreeIterCompareFunc cmp_func; GtkTreeIterCompareFunc cmp_func;
GtkSortType sort_type; GtkSortType sort_type;
store = gtk_list_store_new (6, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_POINTER, THEME_GTK_COLOR_TYPE);
switch (prefs.hex_gui_ulist_sort) switch (prefs.hex_gui_ulist_sort)
{ {
case 0: case 0:
@@ -825,18 +821,77 @@ userlist_create_model (session *sess)
sort_type = GTK_SORT_DESCENDING; sort_type = GTK_SORT_DESCENDING;
break; break;
default: default:
/* No sorting */ /* No sorting; rows keep their current order and new rows
go wherever they are inserted */
gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(store),
GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING);
gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE(store), NULL, NULL, NULL); gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE(store), NULL, NULL, NULL);
return store; return;
} }
gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE(store), cmp_func, sess, NULL); gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE(store), cmp_func, sess, NULL);
gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(store), gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(store),
GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, sort_type); GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, sort_type);
}
GtkListStore *
userlist_create_model (session *sess)
{
GtkListStore *store;
store = gtk_list_store_new (6, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_POINTER, THEME_GTK_COLOR_TYPE);
userlist_model_set_sort (store, sess);
return store; return store;
} }
void
userlist_apply_sort (session *sess)
{
if (!sess->res || !sess->res->user_model)
return;
userlist_model_set_sort (sess->res->user_model, sess);
}
/* remove and re-insert every row, so per-row content that depends on
* preferences (mode icons vs. text prefixes, hostnames) is rebuilt */
void
userlist_refill (session *sess)
{
GtkTreeModel *model;
GtkTreeIter iter;
GSList *users = NULL;
GSList *it;
struct User *user;
if (!sess->res || !sess->res->user_model)
return;
model = GTK_TREE_MODEL (sess->res->user_model);
if (gtk_tree_model_get_iter_first (model, &iter))
{
do
{
gtk_tree_model_get (model, &iter, COL_USER, &user, -1);
if (user)
users = g_slist_prepend (users, user);
}
while (gtk_tree_model_iter_next (model, &iter));
}
fe_userlist_clear (sess);
/* fe_userlist_insert() prepends, so inserting in reverse display
order restores the on-screen order for unsorted lists; sorted
models position the rows themselves */
for (it = users; it; it = it->next)
fe_userlist_insert (sess, it->data, FALSE);
g_slist_free (users);
}
static void static void
userlist_add_columns (GtkTreeView * treeview) userlist_add_columns (GtkTreeView * treeview)
{ {
@@ -906,6 +961,23 @@ userlist_add_columns (GtkTreeView * treeview)
} }
} }
void
userlist_rebuild_columns (GtkWidget *treeview)
{
GList *columns;
GList *it;
if (!treeview)
return;
columns = gtk_tree_view_get_columns (GTK_TREE_VIEW (treeview));
for (it = columns; it; it = it->next)
gtk_tree_view_remove_column (GTK_TREE_VIEW (treeview), it->data);
g_list_free (columns);
userlist_add_columns (GTK_TREE_VIEW (treeview));
}
static gint static gint
userlist_click_cb (GtkWidget *widget, GdkEventButton *event, gpointer userdata) userlist_click_cb (GtkWidget *widget, GdkEventButton *event, gpointer userdata)
{ {
+3
View File
@@ -24,6 +24,9 @@ void userlist_set_value (GtkWidget *treeview, gfloat val);
gfloat userlist_get_value (GtkWidget *treeview); gfloat userlist_get_value (GtkWidget *treeview);
GtkWidget *userlist_create (GtkWidget *box); GtkWidget *userlist_create (GtkWidget *box);
GtkListStore *userlist_create_model (session *sess); GtkListStore *userlist_create_model (session *sess);
void userlist_apply_sort (session *sess);
void userlist_refill (session *sess);
void userlist_rebuild_columns (GtkWidget *treeview);
void userlist_show (session *sess); void userlist_show (session *sess);
void userlist_select (session *sess, char *name); void userlist_select (session *sess, char *name);
void fe_userlist_set_typing (session *sess, const char *nick, const char *state); void fe_userlist_set_typing (session *sess, const char *nick, const char *state);
+4
View File
@@ -857,6 +857,10 @@ fe_get_str (char *prompt, char *def, void *callback, void *ud)
{ {
} }
void void
fe_get_password (char *prompt, void *callback, void *ud)
{
}
void
fe_get_int (char *prompt, int def, void *callback, void *ud) fe_get_int (char *prompt, int def, void *callback, void *ud)
{ {
} }
+5 -4
View File
@@ -66,6 +66,7 @@
<None Include="$(DepsRoot)\bin\*zlib*.dll" /> <None Include="$(DepsRoot)\bin\*zlib*.dll" />
<None Include="$(DepsRoot)\bin\*girepository*.dll" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" /> <None Include="$(DepsRoot)\bin\*girepository*.dll" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" />
<None Include="$(Python3Path)\Lib\site-packages\_cffi_backend.*.pyd" Condition="'$(Python3Enabled)'=='true'" /> <None Include="$(Python3Path)\Lib\site-packages\_cffi_backend.*.pyd" Condition="'$(Python3Enabled)'=='true'" />
<None Include="$(Python38Path)\Lib\site-packages\_cffi_backend.*.pyd" Condition="'$(Python38Enabled)'=='true'" />
<LuaLib Include="$(LuaLibDir)\**\*.dll" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" /> <LuaLib Include="$(LuaLibDir)\**\*.dll" Condition="'$(LuaEnabled)'=='true' and '$(LuaLibDir)'!=''" />
<LuaShare Include="$(LuaShareDir)\*.lua" Condition="'$(LuaEnabled)'=='true' and '$(LuaShareDir)'!=''" /> <LuaShare Include="$(LuaShareDir)\*.lua" Condition="'$(LuaEnabled)'=='true' and '$(LuaShareDir)'!=''" />
@@ -105,10 +106,10 @@
<Copy SourceFiles="@(LuaShare)" DestinationFiles="@(LuaShare->'$(ZoiteChatRel)\share\lua\%(RecursiveDir)%(Filename)%(Extension)')" /> <Copy SourceFiles="@(LuaShare)" DestinationFiles="@(LuaShare->'$(ZoiteChatRel)\share\lua\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(LuaLib)" DestinationFiles="@(LuaLib->'$(ZoiteChatRel)\lib\lua\%(RecursiveDir)%(Filename)%(Extension)')" /> <Copy SourceFiles="@(LuaLib)" DestinationFiles="@(LuaLib->'$(ZoiteChatRel)\lib\lua\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Typelib)" DestinationFiles="@(Typelib->'$(ZoiteChatRel)\lib\girepository-1.0\%(Filename)%(Extension)')" /> <Copy SourceFiles="@(Typelib)" DestinationFiles="@(Typelib->'$(ZoiteChatRel)\lib\girepository-1.0\%(Filename)%(Extension)')" />
<Copy SourceFiles="..\..\plugins\python\xchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" /> <Copy SourceFiles="..\..\plugins\python\xchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" /> <Copy SourceFiles="..\..\plugins\python\hexchat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" /> <Copy SourceFiles="..\..\plugins\python\zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true'" /> <Copy SourceFiles="..\..\plugins\python\_zoitechat.py" DestinationFolder="$(ZoiteChatRel)\python" Condition="'$(Python3Enabled)'=='true' or '$(Python38Enabled)'=='true'" />
<WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" /> <WriteLinesToFile File="$(ZoiteChatRel)portable-mode" Lines="2" Overwrite="true" />
+49 -22
View File
@@ -16,7 +16,7 @@ AppUpdatesURL=http://zoitechat.org/downloads.html
LicenseFile=share\doc\zoitechat\COPYING LicenseFile=share\doc\zoitechat\COPYING
UninstallDisplayIcon={app}\zoitechat.exe UninstallDisplayIcon={app}\zoitechat.exe
UninstallDisplayName=ZoiteChat UninstallDisplayName=ZoiteChat
DefaultDirName={pf64}\ZoiteChat DefaultDirName={commonpf64}\ZoiteChat
DisableDirPage=no DisableDirPage=no
DefaultGroupName=ZoiteChat DefaultGroupName=ZoiteChat
AllowNoIcons=yes AllowNoIcons=yes
@@ -28,11 +28,11 @@ OutputBaseFilename={#APPNAM}-{#APPVER}_x64
FlatComponentsList=no FlatComponentsList=no
PrivilegesRequired=none PrivilegesRequired=none
ShowComponentSizes=no ShowComponentSizes=no
CreateUninstallRegKey=not IsTaskSelected('portable') CreateUninstallRegKey=not WizardIsTaskSelected('portable')
Uninstallable=not IsTaskSelected('portable') Uninstallable=not WizardIsTaskSelected('portable')
ArchitecturesAllowed=x64compatible ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible ArchitecturesInstallIn64BitMode=x64compatible
MinVersion=6.1 MinVersion=6.1sp1
WizardImageFile={#PROJECTDIR}wizardimage.bmp WizardImageFile={#PROJECTDIR}wizardimage.bmp
WizardSmallImageFile={#PROJECTDIR}wizardsmallimage.bmp WizardSmallImageFile={#PROJECTDIR}wizardsmallimage.bmp
SetupIconFile={#PROJECTDIR}..\..\data\icons\zoitechat.ico SetupIconFile={#PROJECTDIR}..\..\data\icons\zoitechat.ico
@@ -59,7 +59,7 @@ Name: "plugins\upd"; Description: "Update Checker"; Types: normal custom; Flags:
Name: "langs"; Description: "Language Interfaces"; Types: custom; Flags: disablenouninstallwarning Name: "langs"; Description: "Language Interfaces"; Types: custom; Flags: disablenouninstallwarning
Name: "langs\lua"; Description: "Lua (LuaJIT 2.1)"; Types: normal custom; Flags: disablenouninstallwarning Name: "langs\lua"; Description: "Lua (LuaJIT 2.1)"; Types: normal custom; Flags: disablenouninstallwarning
Name: "langs\perl"; Description: "Perl (Strawberry Perl 5.42.0.1)"; Types: custom; Flags: disablenouninstallwarning Name: "langs\perl"; Description: "Perl (Strawberry Perl 5.42.0.1)"; Types: custom; Flags: disablenouninstallwarning
Name: "langs\python"; Description: "Python (Python 3.14.3)"; Types: custom; Flags: disablenouninstallwarning Name: "langs\python"; Description: "Python (Python 3.14.3, or 3.8.10 before Windows 10)"; Types: custom; Flags: disablenouninstallwarning
Name: "themes"; Description: "GTK3 Themes"; Types: normal minimal custom; Flags: disablenouninstallwarning Name: "themes"; Description: "GTK3 Themes"; Types: normal minimal custom; Flags: disablenouninstallwarning
Name: "themes\windows10"; Description: "Windows 10"; Types: normal minimal custom; Flags: disablenouninstallwarning Name: "themes\windows10"; Description: "Windows 10"; Types: normal minimal custom; Flags: disablenouninstallwarning
Name: "themes\windows10dark"; Description: "Windows 10 Dark"; Types: normal minimal custom; Flags: disablenouninstallwarning Name: "themes\windows10dark"; Description: "Windows 10 Dark"; Types: normal minimal custom; Flags: disablenouninstallwarning
@@ -181,7 +181,8 @@ Source: "plugins\hcperl.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Co
Source: "python\*.py"; DestDir: "{app}\python"; Flags: ignoreversion; Components: langs\python Source: "python\*.py"; DestDir: "{app}\python"; Flags: ignoreversion; Components: langs\python
Source: "plugins\hcpython3.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\python Source: "plugins\hcpython3.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: langs\python; Check: not NeedsLegacyPython
Source: "plugins\hcpython38.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\python; Check: NeedsLegacyPython
Source: "_cffi_backend.cp3*.pyd"; DestDir: "{app}"; Flags: ignoreversion; Components: langs\python Source: "_cffi_backend.cp3*.pyd"; DestDir: "{app}"; Flags: ignoreversion; Components: langs\python
Source: "zoitechat.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: libs Source: "zoitechat.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
@@ -328,6 +329,32 @@ begin
Result := DirExists(ExpandConstant('{localappdata}') + '\enchant\myspell'); Result := DirExists(ExpandConstant('{localappdata}') + '\enchant\myspell');
end; end;
// Python >= 3.9 dropped support for Windows versions before 10;
// those systems get Python 3.8 and the matching plugin instead.
function NeedsLegacyPython(): Boolean;
var
Version: TWindowsVersion;
begin
GetWindowsVersionEx(Version);
Result := Version.NTPlatform and (Version.Major < 10);
end;
function PythonURL(): String;
begin
if NeedsLegacyPython() then
Result := 'https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe'
else
Result := 'https://www.python.org/ftp/python/3.14.3/python-3.14.3-amd64.exe';
end;
function PythonDLL(): String;
begin
if NeedsLegacyPython() then
Result := 'python38.dll'
else
Result := 'python314.dll';
end;
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
// Sets up the automatic downloads // Sets up the automatic downloads
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
@@ -345,34 +372,34 @@ begin
begin begin
idpClearFiles; idpClearFiles;
if IsComponentSelected('themes\windows10') then if WizardIsComponentSelected('themes\windows10') then
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip')); idpAddFile('https://dl.zoitechat.org/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip'));
if IsComponentSelected('themes\windows10dark') then if WizardIsComponentSelected('themes\windows10dark') then
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip')); idpAddFile('https://dl.zoitechat.org/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip'));
if not IsTaskSelected('portable') then if not WizardIsTaskSelected('portable') then
begin begin
REDIST := 'https://aka.ms/vs/17/release/vc_redist.x64.exe'; REDIST := 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
PERL := 'https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54201_64bit/strawberry-perl-5.42.0.1-64bit.msi'; PERL := 'https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54201_64bit/strawberry-perl-5.42.0.1-64bit.msi';
PY3 := 'https://www.python.org/ftp/python/3.14.3/python-3.14.3-amd64.exe'; PY3 := PythonURL();
SPELL := 'https://github.com/zoitechat/gvsbuild/releases/download/zoitechat-2.16.2/ZoiteChat.Spelling.Dictionaries.r2.exe'; SPELL := 'https://github.com/hexchat/gvsbuild/releases/download/hexchat-2.16.2/HexChat.Spelling.Dictionaries.r2.exe';
if IsComponentSelected('deps\vcredist2015') and not CheckVCInstall() then if WizardIsComponentSelected('deps\vcredist2015') and not CheckVCInstall() then
idpAddFile(REDIST, ExpandConstant('{tmp}\vcredist.exe')); idpAddFile(REDIST, ExpandConstant('{tmp}\vcredist.exe'));
if IsComponentSelected('spell') and not CheckSpellInstall() then if WizardIsComponentSelected('spell') and not CheckSpellInstall() then
idpAddFile(SPELL, ExpandConstant('{tmp}\spelling-dicts.exe')); idpAddFile(SPELL, ExpandConstant('{tmp}\spelling-dicts.exe'));
if not WizardSilent() then if not WizardSilent() then
begin begin
if IsComponentSelected('langs\perl') and not CheckDLL('perl542.dll') then if WizardIsComponentSelected('langs\perl') and not CheckDLL('perl542.dll') then
begin begin
idpAddFile(PERL, ExpandConstant('{tmp}\perl.msi')) idpAddFile(PERL, ExpandConstant('{tmp}\perl.msi'))
end; end;
if IsComponentSelected('langs\python') and not CheckDLL('python314.dll') then if WizardIsComponentSelected('langs\python') and not CheckDLL(PythonDLL()) then
idpAddFile(PY3, ExpandConstant('{tmp}\python.exe')); idpAddFile(PY3, ExpandConstant('{tmp}\python.exe'));
end; end;
end; end;
@@ -397,7 +424,7 @@ begin
if (CurPageID = wpSelectTasks) then if (CurPageID = wpSelectTasks) then
if (WizardForm.TasksList.Checked[1] = True) then if (WizardForm.TasksList.Checked[1] = True) then
if (WizardDirValue() = ExpandConstant('{pf64}\ZoiteChat')) then if (WizardDirValue() = ExpandConstant('{commonpf64}\ZoiteChat')) then
begin begin
WizardForm.TasksList.Checked[1] := False WizardForm.TasksList.Checked[1] := False
MsgBox('Portable mode is only intended for use on portable drives and has been disabled.', mbInformation, MB_OK) MsgBox('Portable mode is only intended for use on portable drives and has been disabled.', mbInformation, MB_OK)
@@ -405,21 +432,21 @@ begin
if CurPageID = wpReady then if CurPageID = wpReady then
begin begin
if IsComponentSelected('themes\windows10') and not FileExists(ExpandConstant('{tmp}\Windows-10-3.2.1.zip')) then if WizardIsComponentSelected('themes\windows10') and not FileExists(ExpandConstant('{tmp}\Windows-10-3.2.1.zip')) then
begin begin
MsgBox('Windows 10 GTK3 theme could not be downloaded. Please retry setup and rerun setup.', mbError, MB_OK); MsgBox('Windows 10 GTK3 theme could not be downloaded. Please retry setup and rerun setup.', mbError, MB_OK);
Result := False; Result := False;
Exit; Exit;
end; end;
if IsComponentSelected('themes\windows10dark') and not FileExists(ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip')) then if WizardIsComponentSelected('themes\windows10dark') and not FileExists(ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip')) then
begin begin
MsgBox('Windows 10 Dark GTK3 theme could not be downloaded. Please retry setup and rerun setup.', mbError, MB_OK); MsgBox('Windows 10 Dark GTK3 theme could not be downloaded. Please retry setup and rerun setup.', mbError, MB_OK);
Result := False; Result := False;
Exit; Exit;
end; end;
if IsComponentSelected('deps\vcredist2015') and not CheckVCInstall() and not FileExists(ExpandConstant('{tmp}\vcredist.exe')) then if WizardIsComponentSelected('deps\vcredist2015') and not CheckVCInstall() and not FileExists(ExpandConstant('{tmp}\vcredist.exe')) then
begin begin
MsgBox('Visual C++ Redistributable could not be downloaded. Please retry setup or install it manually and rerun setup.', mbError, MB_OK); MsgBox('Visual C++ Redistributable could not be downloaded. Please retry setup or install it manually and rerun setup.', mbError, MB_OK);
Result := False; Result := False;
@@ -486,7 +513,7 @@ end;
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
procedure CurStepChanged(CurStep: TSetupStep); procedure CurStepChanged(CurStep: TSetupStep);
begin begin
if not (IsTaskSelected('portable')) then if not (WizardIsTaskSelected('portable')) then
begin begin
if (CurStep=ssInstall) then if (CurStep=ssInstall) then
begin begin
+1 -1
View File
@@ -1,6 +1,6 @@
param ([string] $templateFilename, [string] $outputFilename) param ([string] $templateFilename, [string] $outputFilename)
$versionParts = Select-String -Path "${env:SOLUTIONDIR}meson.build" -Pattern " version: '([^']+)',$" | Select-Object -First 1 | %{ $_.Matches[0].Groups[1].Value.Split('.') } $versionParts = (Get-Content "${env:SOLUTIONDIR}VERSION" -Raw).Trim().Split('.')
[string[]] $contents = Get-Content $templateFilename -Encoding UTF8 | %{ [string[]] $contents = Get-Content $templateFilename -Encoding UTF8 | %{
while ($_ -match '^(.*?)<#=(.*?)#>(.*?)$') { while ($_ -match '^(.*?)<#=(.*?)#>(.*?)$') {
-1
View File
@@ -1 +0,0 @@
2.18.3
+7
View File
@@ -8,6 +8,7 @@
<YourGendefPath>c:\gtk-build\gendef</YourGendefPath> <YourGendefPath>c:\gtk-build\gendef</YourGendefPath>
<YourPerlPath>c:\gtk-build\perl-5.42.0.1</YourPerlPath> <YourPerlPath>c:\gtk-build\perl-5.42.0.1</YourPerlPath>
<YourPython3Path>c:\gtk-build\python-3.14</YourPython3Path> <YourPython3Path>c:\gtk-build\python-3.14</YourPython3Path>
<YourPython38Path>c:\gtk-build\python-3.8</YourPython38Path>
<YourWinSparklePath>c:\gtk-build\WinSparkle</YourWinSparklePath> <YourWinSparklePath>c:\gtk-build\WinSparkle</YourWinSparklePath>
<!-- YOU SHOULDN'T TOUCH ANYTHING BELOW --> <!-- YOU SHOULDN'T TOUCH ANYTHING BELOW -->
@@ -32,6 +33,12 @@
<Python3Enabled Condition="Exists('$(Python3Path)\\python.exe') and Exists('$(Python3Path)\\libs\\$(Python3Lib).lib') and Exists('$(Python3Path)\\Lib\\site-packages\\cffi')">true</Python3Enabled> <Python3Enabled Condition="Exists('$(Python3Path)\\python.exe') and Exists('$(Python3Path)\\libs\\$(Python3Lib).lib') and Exists('$(Python3Path)\\Lib\\site-packages\\cffi')">true</Python3Enabled>
<Python3Enabled Condition="'$(Python3Enabled)'==''">false</Python3Enabled> <Python3Enabled Condition="'$(Python3Enabled)'==''">false</Python3Enabled>
<Python38Path>$(YourPython38Path)\$(ZoiteChatPlatform)</Python38Path>
<Python38Lib>python38</Python38Lib>
<Python38Output>hcpython38</Python38Output>
<Python38Enabled Condition="Exists('$(Python38Path)\\python.exe') and Exists('$(Python38Path)\\libs\\$(Python38Lib).lib') and Exists('$(Python38Path)\\Lib\\site-packages\\cffi')">true</Python38Enabled>
<Python38Enabled Condition="'$(Python38Enabled)'==''">false</Python38Enabled>
<PerlEnabled Condition="Exists('$(PerlPath)\\bin\\perl.exe') and Exists('$(PerlPath)\\bin\\$(PerlLib).dll') and Exists('$(GendefPath)\\gendef.exe')">true</PerlEnabled> <PerlEnabled Condition="Exists('$(PerlPath)\\bin\\perl.exe') and Exists('$(PerlPath)\\bin\\$(PerlLib).dll') and Exists('$(GendefPath)\\gendef.exe')">true</PerlEnabled>
<PerlEnabled Condition="'$(PerlEnabled)'==''">false</PerlEnabled> <PerlEnabled Condition="'$(PerlEnabled)'==''">false</PerlEnabled>
+9
View File
@@ -60,6 +60,7 @@ EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "installer", "installer\installer.vcxproj", "{5A0F4962-E670-4DA2-9E45-52CC47F26E2F}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "installer", "installer\installer.vcxproj", "{5A0F4962-E670-4DA2-9E45-52CC47F26E2F}"
ProjectSection(ProjectDependencies) = postProject ProjectSection(ProjectDependencies) = postProject
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {C2321A03-0BA7-45B3-8740-ABD82B36B0BF} {C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {C2321A03-0BA7-45B3-8740-ABD82B36B0BF}
{D58924D2-1492-4AC3-B058-7A214424EEDC} = {D58924D2-1492-4AC3-B058-7A214424EEDC}
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}
{17E4BE39-76F7-4A06-AD21-EFD0C5091F76} = {17E4BE39-76F7-4A06-AD21-EFD0C5091F76} {17E4BE39-76F7-4A06-AD21-EFD0C5091F76} = {17E4BE39-76F7-4A06-AD21-EFD0C5091F76}
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} {4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {4C0F3940-2EEE-4646-82F7-6CE75B9A72F4}
@@ -80,6 +81,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "perl", "..\plugins\perl\per
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3} {87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
EndProjectSection EndProjectSection
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "python38", "..\plugins\python\python38.vcxproj", "{D58924D2-1492-4AC3-B058-7A214424EEDC}"
ProjectSection(ProjectDependencies) = postProject
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "python3", "..\plugins\python\python3.vcxproj", "{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "python3", "..\plugins\python\python3.vcxproj", "{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}"
ProjectSection(ProjectDependencies) = postProject ProjectSection(ProjectDependencies) = postProject
{87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3} {87554B59-006C-4D94-9714-897B27067BA3} = {87554B59-006C-4D94-9714-897B27067BA3}
@@ -127,6 +133,8 @@ Global
{D90BC3E3-1341-4849-9354-5F40489D39D1}.Release|x64.Build.0 = Release|x64 {D90BC3E3-1341-4849-9354-5F40489D39D1}.Release|x64.Build.0 = Release|x64
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}.Release|x64.ActiveCfg = Release|x64 {C2321A03-0BA7-45B3-8740-ABD82B36B0BF}.Release|x64.ActiveCfg = Release|x64
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF}.Release|x64.Build.0 = Release|x64 {C2321A03-0BA7-45B3-8740-ABD82B36B0BF}.Release|x64.Build.0 = Release|x64
{D58924D2-1492-4AC3-B058-7A214424EEDC}.Release|x64.ActiveCfg = Release|x64
{D58924D2-1492-4AC3-B058-7A214424EEDC}.Release|x64.Build.0 = Release|x64
{C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.ActiveCfg = Release|x64 {C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.ActiveCfg = Release|x64
{C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.Build.0 = Release|x64 {C53145CC-D021-40C9-B97C-0249AB9A43C9}.Release|x64.Build.0 = Release|x64
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}.Release|x64.ActiveCfg = Release|x64 {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A}.Release|x64.ActiveCfg = Release|x64
@@ -151,6 +159,7 @@ Global
{5A0F4962-E670-4DA2-9E45-52CC47F26E2F} = {0FD996A7-464F-4981-8380-3DCA3A244A13} {5A0F4962-E670-4DA2-9E45-52CC47F26E2F} = {0FD996A7-464F-4981-8380-3DCA3A244A13}
{D90BC3E3-1341-4849-9354-5F40489D39D1} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240} {D90BC3E3-1341-4849-9354-5F40489D39D1} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
{C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240} {C2321A03-0BA7-45B3-8740-ABD82B36B0BF} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
{D58924D2-1492-4AC3-B058-7A214424EEDC} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}
{C53145CC-D021-40C9-B97C-0249AB9A43C9} = {561126F4-FA18-45FC-A2BF-8F858F161D6D} {C53145CC-D021-40C9-B97C-0249AB9A43C9} = {561126F4-FA18-45FC-A2BF-8F858F161D6D}
{BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {021EC1D0-FF67-4700-9AB2-EAABF1159C09} {BF0EBC16-68AD-4CD1-864C-5B56836EBE2A} = {021EC1D0-FF67-4700-9AB2-EAABF1159C09}
{4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240} {4C0F3940-2EEE-4646-82F7-6CE75B9A72F4} = {D237DA6B-BD5F-46C0-8BEA-50E9A1340240}