1 Commits

Author SHA1 Message Date
635301f8f3 theming: respect system theme by default 2026-07-09 11:01:08 -06:00
55 changed files with 997 additions and 40153 deletions

View File

@@ -36,7 +36,7 @@ jobs:
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
breeze-gtk-theme \
fonts-noto-color-emoji breeze-gtk-theme \
patchelf file curl
- name: Configure
@@ -119,8 +119,15 @@ jobs:
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
fi
test -f AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
test -f AppDir/usr/share/doc/zoitechat/fonts/OFL.txt
if [ -d "/usr/share/gtk-3.0/emoji" ]; then
install -d AppDir/usr/share/gtk-3.0
cp -a /usr/share/gtk-3.0/emoji AppDir/usr/share/gtk-3.0/
fi
if [ -f "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf" ]; then
install -d AppDir/usr/share/fonts/truetype/noto
cp -a /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf AppDir/usr/share/fonts/truetype/noto/
fi
if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then
install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0
@@ -136,6 +143,7 @@ jobs:
run: |
set -eux
# Include every built-in plugin except the Windows-only update plugin.
find AppDir/usr/lib -maxdepth 4 -type f -name '*.so' -path '*/zoitechat/plugins/*' -print | sort
for plugin in checksum fishlim lua perl python sysinfo; do
@@ -250,10 +258,13 @@ jobs:
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
fi
# OpenSSL trust store override
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
export CURL_CA_BUNDLE="${CURL_CA_BUNDLE:-$SSL_CERT_FILE}"
# Prefer Wayland if the session provides it, but keep X11 fallback.
# Don't override if the user already set GDK_BACKEND explicitly.
if [ -z "${GDK_BACKEND:-}" ]; then
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
export GDK_BACKEND="wayland,x11"

23
COPYING
View File

@@ -336,26 +336,3 @@ proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
======================================================================
ADDITIONAL LICENSE NOTICE
======================================================================
ZoiteChat source code:
GPL version 2
See Above.
Unicode Emoji and CLDR data:
Unicode License v3
data/emoji/LICENSE-UNICODE.txt
gemoji shortcode data:
MIT License
Copyright (c) 2019 GitHub, Inc.
data/emoji/LICENSE-GEMOJI.txt
Noto Color Emoji:
SIL Open Font License 1.1
data/fonts/OFL.txt

View File

@@ -1,39 +0,0 @@
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2026 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.

View File

@@ -1,30 +0,0 @@
# Bundled emoji data
These are pinned copies of the upstream data files that
`tools/gen-emoji-data.py` compiles into ZoiteChat's emoji picker catalog
at build time. Bundling them means every ZoiteChat package exposes the
same emoji, names, categories and search keywords, independent of the
GTK runtime version or the emoji data a distribution installs under
`/usr/share/gtk-3.0/emoji`.
Nothing here affects what is sent over IRC: the picker inserts plain
Unicode sequences. Official builds register the bundled Noto Color Emoji
font privately for consistent rendering, while preserving platform font
fallback if registration is unavailable.
| File | Source | Version |
| ---- | ------ | ------- |
| `emoji-test.txt` | [Unicode emoji data](https://unicode.org/Public/emoji/17.0/emoji-test.txt) (mirrored at [unicode-org/unicodetools](https://github.com/unicode-org/unicodetools/blob/main/unicodetools/data/emoji/17.0/emoji-test.txt)) | Emoji 17.0 |
| `cldr-annotations-en.xml` | [unicode-org/cldr](https://github.com/unicode-org/cldr/blob/release-48/common/annotations/en.xml) `common/annotations/en.xml` | CLDR release 48 |
| `cldr-annotations-derived-en.xml` | [unicode-org/cldr](https://github.com/unicode-org/cldr/blob/release-48/common/annotationsDerived/en.xml) `common/annotationsDerived/en.xml` | CLDR release 48 |
| `gemoji.json` | [github/gemoji](https://github.com/github/gemoji/blob/v4.1.0/db/emoji.json) `db/emoji.json` | v4.1.0 |
The pinned versions are recorded in `VERSIONS`.
## Licenses
- `emoji-test.txt`, `cldr-annotations-en.xml`,
`cldr-annotations-derived-en.xml`: © Unicode, Inc., distributed under
the [Unicode License v3](https://www.unicode.org/license.txt).
- `gemoji.json`: from the gemoji project, MIT licensed,
© GitHub, Inc.

View File

@@ -1,6 +0,0 @@
# Pinned versions of the emoji source data in this directory.
# Keep in sync with the files themselves; tools/gen-emoji-data.py
# verifies unicode-emoji against the emoji-test.txt header.
unicode-emoji=17.0
cldr=48
gemoji=4.1.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,91 +0,0 @@
Copyright 2013 Google LLC
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) shall not be
used to promote, endorse or advertise any Modified Version, except to
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s)
or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,18 +0,0 @@
# Bundled emoji font
`NotoColorEmoji.ttf` reports Noto Color Emoji version 2.051 and covers
Unicode Emoji 17.0. It is vendored from Debian's
`fonts-noto-color-emoji` 2.051 package, built from the corresponding
Google Noto Emoji release.
Official ZoiteChat packages install the font inside the application and
register it only for the ZoiteChat process. It is not installed as a
global operating-system font.
Upstream release: https://github.com/googlefonts/noto-emoji/releases/tag/v2.051
Vendored file SHA-256:
`dac5c27651082d6c53dab5081f50d2022ddf6877d730fa919cf6a4fc2af22de0`
The font is distributed under the SIL Open Font License 1.1. See
`OFL.txt`.

View File

@@ -1,13 +0,0 @@
emoji_font_dir = join_paths(get_option('datadir'), 'fonts', 'zoitechat')
emoji_font_doc_dir = join_paths(get_option('datadir'), 'doc', 'zoitechat', 'fonts')
install_data(
'NotoColorEmoji.ttf',
install_dir: emoji_font_dir,
)
install_data(
'OFL.txt',
'README.md',
install_dir: emoji_font_doc_dir,
)

View File

@@ -3,7 +3,6 @@ if get_option('plugin')
endif
if get_option('gtk-frontend')
subdir('fonts')
subdir('icons')
subdir('misc')
subdir('man')

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ data/misc/net.zoite.Zoitechat.ThemeManager.desktop.in
src/common/cfgfiles.c
src/common/chanopt.c
src/common/dcc.c
src/common/emoji-data.c
src/common/zoitechat.c
src/common/ignore.c
src/common/inbound.c
@@ -25,7 +24,6 @@ src/fe-gtk/banlist.c
src/fe-gtk/chanlist.c
src/fe-gtk/dccgui.c
src/fe-gtk/editlist.c
src/fe-gtk/emoji-picker.c
src/fe-gtk/fe-gtk.c
src/fe-gtk/fkeys.c
src/fe-gtk/gtkutil.c

View File

@@ -46,12 +46,12 @@
#endif
const char * const languages[LANGUAGES_LENGTH] = {
"af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN",
"zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl",
"de", "el", "gu", "hi", "hu", "id", "it", "ja_JP", "kn", "rw",
"ko", "lv", "lt", "mk", "ml", "ms", "nb", "no", "pl", "pt",
"pt_BR", "pa", "ru", "sr", "sk", "sl", "es", "sv", "th", "tr",
"uk", "vi", "wa"
"af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN", /* 0 .. 9 */
"zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl", /* 10 .. 19 */
"de", "el", "gu", "hi", "hu", "id", "it", "ja_JP", "kn", "rw", /* 20 .. 29 */
"ko", "lv", "lt", "mk", "ml", "ms", "nb", "no", "pl", "pt", /* 30 .. 39 */
"pt_BR", "pa", "ru", "sr", "sk", "sl", "es", "sv", "th", "tr", /* 40 .. 49 */
"uk", "vi", "wa" /* 50 .. */
};
void
@@ -78,6 +78,8 @@ list_addentry (GSList ** list, char *cmd, char *name)
*list = g_slist_append (*list, pop);
}
/* read it in from a buffer to our linked list */
static void
list_load_from_data (GSList ** list, char *ibuf, int size)
{
@@ -179,9 +181,9 @@ list_delentry (GSList ** list, char *name)
char *
cfg_get_str (char *cfg, const char *var, char *dest, int dest_len)
{
char buffer[128];
char buffer[128]; /* should be plenty for a variable name */
sprintf (buffer, "%s ", var);
sprintf (buffer, "%s ", var); /* add one space, this way it works against var - var2 checks too */
while (1)
{
@@ -195,6 +197,8 @@ cfg_get_str (char *cfg, const char *var, char *dest, int dest_len)
cfg++;
while (*cfg == ' ')
cfg++;
/*while (*cfg == ' ' || *cfg == '=')
cfg++; */
value = cfg;
while (*cfg != 0 && *cfg != '\n')
cfg++;
@@ -289,7 +293,7 @@ cfg_get_int (char *cfg, char *var)
return atoi (str);
}
char *xdir = NULL;
char *xdir = NULL; /* utf-8 encoding */
#ifdef WIN32
#include <windows.h>
@@ -412,7 +416,6 @@ const struct prefs vars[] =
{"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT},
{"gui_dialog_top", P_OFFINT (hex_gui_dialog_top), TYPE_INT},
{"gui_dialog_width", P_OFFINT (hex_gui_dialog_width), TYPE_INT},
{"gui_emoji_skin_tone", P_OFFINT (hex_gui_emoji_skin_tone), TYPE_INT},
{"gui_filesize_iec", P_OFFINT (hex_gui_filesize_iec), TYPE_BOOL},
{"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL},
{"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL},
@@ -432,6 +435,7 @@ const struct prefs vars[] =
{"gui_pane_right_size_min", P_OFFINT (hex_gui_pane_right_size_min), TYPE_INT},
{"gui_quit_dialog", P_OFFINT (hex_gui_quit_dialog), TYPE_BOOL},
{"gui_search_pos", P_OFFINT (hex_gui_search_pos), TYPE_INT},
/* {"gui_single", P_OFFINT (hex_gui_single), TYPE_BOOL}, */
{"gui_slist_fav", P_OFFINT (hex_gui_slist_fav), TYPE_BOOL},
{"gui_slist_select", P_OFFINT (hex_gui_slist_select), TYPE_INT},
{"gui_slist_skip", P_OFFINT (hex_gui_slist_skip), TYPE_BOOL},

View File

@@ -55,7 +55,6 @@
<ClCompile Include="chanopt.c" />
<ClCompile Include="ctcp.c" />
<ClCompile Include="dcc.c" />
<ClCompile Include="emoji-data.c" />
<ClCompile Include="history.c" />
<ClCompile Include="plugin-identd.c" />
<ClCompile Include="ignore.c" />
@@ -114,7 +113,6 @@
SET SOLUTIONDIR=$(SolutionDir)..\
"$(Python3Path)\python.exe" $(ProjectDir)make-te.py "$(ProjectDir)textevents.in" "$(ZoiteChatLib)textevents.h" "$(ZoiteChatLib)textenums.h"
"$(Python3Path)\python.exe" $(ProjectDir)gen-public-suffix.py "$(ZoiteChatLib)public_suffix_data.h"
"$(Python3Path)\python.exe" "$(SolutionDir)..\tools\gen-emoji-data.py" "$(SolutionDir)..\data\emoji\emoji-test.txt" "$(SolutionDir)..\data\emoji\cldr-annotations-en.xml" "$(SolutionDir)..\data\emoji\cldr-annotations-derived-en.xml" "$(SolutionDir)..\data\emoji\gemoji.json" "$(SolutionDir)..\data\emoji\VERSIONS" "$(ZoiteChatLib)emoji-data-table.h"
powershell -File "$(SolutionDir)..\win32\version-template.ps1" "$(SolutionDir)..\win32\config.h.tt" "$(ZoiteChatLib)config.h"
$(GlibGenMarshal) --prefix=_zoitechat_marshal --header "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.h"
$(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c"

View File

@@ -1,239 +0,0 @@
/* ZoiteChat
* Copyright (C) 2026 deepend-tildeclub.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <string.h>
#include <glib.h>
#include <glib/gi18n.h>
#include "emoji-data.h"
#include "emoji-data-table.h"
gsize
emoji_data_count (void)
{
return G_N_ELEMENTS (emoji_entries_table);
}
const EmojiEntry *
emoji_data_entry (gsize index)
{
g_return_val_if_fail (index < G_N_ELEMENTS (emoji_entries_table), NULL);
return &emoji_entries_table[index];
}
static GHashTable *
emoji_sequence_index (void)
{
static GHashTable *table = NULL;
gsize i;
int tone;
if (table)
return table;
table = g_hash_table_new (g_str_hash, g_str_equal);
for (i = 0; i < G_N_ELEMENTS (emoji_entries_table); i++)
{
const EmojiEntry *entry = &emoji_entries_table[i];
g_hash_table_insert (table, (gpointer) entry->sequence,
GSIZE_TO_POINTER (i * EMOJI_TONE_COUNT + 1));
if (entry->tone_set < 0)
continue;
for (tone = EMOJI_TONE_LIGHT; tone < EMOJI_TONE_COUNT; tone++)
{
const char *seq =
emoji_tone_sets_table[entry->tone_set].sequence[tone - 1];
g_hash_table_insert (table, (gpointer) seq,
GSIZE_TO_POINTER (i * EMOJI_TONE_COUNT + tone + 1));
}
}
return table;
}
const EmojiEntry *
emoji_data_find_by_sequence (const char *sequence, EmojiTone *tone_out)
{
gpointer value;
gsize packed;
if (tone_out)
*tone_out = EMOJI_TONE_NONE;
if (!sequence || !*sequence)
return NULL;
value = g_hash_table_lookup (emoji_sequence_index (), sequence);
if (!value)
return NULL;
packed = GPOINTER_TO_SIZE (value) - 1;
if (tone_out)
*tone_out = (EmojiTone) (packed % EMOJI_TONE_COUNT);
return &emoji_entries_table[packed / EMOJI_TONE_COUNT];
}
const char *
emoji_data_group_display_name (EmojiGroup group)
{
static const char *const names[EMOJI_GROUP_COUNT] =
{
N_("Smileys & Emotion"),
N_("People & Body"),
N_("Animals & Nature"),
N_("Food & Drink"),
N_("Travel & Places"),
N_("Activities"),
N_("Objects"),
N_("Symbols"),
N_("Flags"),
};
g_return_val_if_fail (group < EMOJI_GROUP_COUNT, "");
return _(names[group]);
}
const char *
emoji_data_group_icon_sequence (EmojiGroup group)
{
static const char *const icons[EMOJI_GROUP_COUNT] =
{
"\360\237\230\200", /* 😀 */
"\360\237\221\213", /* 👋 */
"\360\237\220\273", /* 🐻 */
"\360\237\215\224", /* 🍔 */
"\360\237\232\227", /* 🚗 */
"\342\232\275", /* ⚽ */
"\360\237\222\241", /* 💡 */
"\360\237\224\243", /* 🔣 */
"\360\237\217\201", /* 🏁 */
};
g_return_val_if_fail (group < EMOJI_GROUP_COUNT, "");
return icons[group];
}
const char *
emoji_data_tone_display_name (EmojiTone tone)
{
static const char *const names[EMOJI_TONE_COUNT] =
{
N_("No skin tone"),
N_("Light skin tone"),
N_("Medium-light skin tone"),
N_("Medium skin tone"),
N_("Medium-dark skin tone"),
N_("Dark skin tone"),
};
g_return_val_if_fail (tone < EMOJI_TONE_COUNT, "");
return _(names[tone]);
}
const char *
emoji_entry_sequence_for_tone (const EmojiEntry *entry, EmojiTone tone)
{
g_return_val_if_fail (entry != NULL, NULL);
if (tone == EMOJI_TONE_NONE || tone >= EMOJI_TONE_COUNT ||
entry->tone_set < 0)
return entry->sequence;
return emoji_tone_sets_table[entry->tone_set].sequence[tone - 1];
}
char **
emoji_search_tokenize (const char *text)
{
GPtrArray *tokens;
char *folded;
char **split;
gsize i;
if (!text)
return NULL;
folded = g_utf8_casefold (text, -1);
split = g_strsplit_set (folded, " \t\n", -1);
g_free (folded);
tokens = g_ptr_array_new ();
for (i = 0; split[i]; i++)
{
const char *token = split[i];
gsize len;
while (*token == ':')
token++;
len = strlen (token);
while (len > 0 && token[len - 1] == ':')
len--;
if (len > 0)
g_ptr_array_add (tokens, g_strndup (token, len));
}
g_strfreev (split);
if (tokens->len == 0)
{
g_ptr_array_free (tokens, TRUE);
return NULL;
}
g_ptr_array_add (tokens, NULL);
return (char **) g_ptr_array_free (tokens, FALSE);
}
gboolean
emoji_entry_matches (const EmojiEntry *entry, char *const *needle_tokens)
{
gsize i;
g_return_val_if_fail (entry != NULL, FALSE);
if (!needle_tokens || !needle_tokens[0])
return FALSE;
for (i = 0; needle_tokens[i]; i++)
{
if (!strstr (entry->search, needle_tokens[i]))
return FALSE;
}
return TRUE;
}
const char *
emoji_data_unicode_version (void)
{
return emoji_data_unicode_version_str;
}
const char *
emoji_data_cldr_version (void)
{
return emoji_data_cldr_version_str;
}

View File

@@ -1,91 +0,0 @@
/* ZoiteChat
* Copyright (C) 2026 deepend-tildeclub.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
/* Bundled emoji catalog, generated at build time from pinned Unicode,
* CLDR and gemoji data (see data/emoji/ and tools/gen-emoji-data.py).
* Every entry is a fully-qualified Unicode sequence; nothing here depends
* on the GTK runtime's emoji data. The catalog only decides what users
* can find and insert - rendering stays with the platform's fonts, and
* IRC always receives the plain Unicode sequence. */
#ifndef ZOITECHAT_EMOJI_DATA_H
#define ZOITECHAT_EMOJI_DATA_H
#include <glib.h>
typedef enum
{
EMOJI_GROUP_SMILEYS_EMOTION,
EMOJI_GROUP_PEOPLE_BODY,
EMOJI_GROUP_ANIMALS_NATURE,
EMOJI_GROUP_FOOD_DRINK,
EMOJI_GROUP_TRAVEL_PLACES,
EMOJI_GROUP_ACTIVITIES,
EMOJI_GROUP_OBJECTS,
EMOJI_GROUP_SYMBOLS,
EMOJI_GROUP_FLAGS,
EMOJI_GROUP_COUNT
} EmojiGroup;
typedef enum
{
EMOJI_TONE_NONE,
EMOJI_TONE_LIGHT,
EMOJI_TONE_MEDIUM_LIGHT,
EMOJI_TONE_MEDIUM,
EMOJI_TONE_MEDIUM_DARK,
EMOJI_TONE_DARK,
EMOJI_TONE_COUNT
} EmojiTone;
typedef struct
{
const char *sequence;
const char *name;
const char *search;
const char *aliases;
guint8 group;
gint16 tone_set;
} EmojiEntry;
typedef struct
{
const char *sequence[EMOJI_TONE_COUNT - 1];
} EmojiToneSet;
gsize emoji_data_count (void);
const EmojiEntry *emoji_data_entry (gsize index);
const EmojiEntry *emoji_data_find_by_sequence (const char *sequence,
EmojiTone *tone_out);
const char *emoji_data_group_display_name (EmojiGroup group);
const char *emoji_data_group_icon_sequence (EmojiGroup group);
const char *emoji_data_tone_display_name (EmojiTone tone);
const char *emoji_entry_sequence_for_tone (const EmojiEntry *entry,
EmojiTone tone);
char **emoji_search_tokenize (const char *text);
gboolean emoji_entry_matches (const EmojiEntry *entry,
char *const *needle_tokens);
const char *emoji_data_unicode_version (void);
const char *emoji_data_cldr_version (void);
#endif

View File

@@ -142,7 +142,6 @@ void fe_set_away (server *serv);
void fe_serverlist_open (session *sess);
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_password (char *prompt, void *callback, void *ud);
void fe_get_int (char *prompt, int def, void *callback, void *ud);
#define FRF_WRITE 1 /* save file */
#define FRF_MULTIPLE 2 /* multi-select */

View File

@@ -315,6 +315,59 @@ resolve_parent_theme_root (const char *child_theme_root, const char *parent_name
return home_local;
g_free (home_local);
{
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
if (xdg_data_home && xdg_data_home[0])
{
candidate = g_build_filename (xdg_data_home, "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
return candidate;
g_free (candidate);
}
}
candidate = g_build_filename (g_get_user_data_dir (), "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
return candidate;
g_free (candidate);
{
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
guint i;
if (xdg_data_dirs && xdg_data_dirs[0])
{
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
for (i = 0; data_dirs && data_dirs[i]; i++)
{
if (!data_dirs[i][0])
continue;
candidate = g_build_filename (data_dirs[i], "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
{
g_strfreev (data_dirs);
return candidate;
}
g_free (candidate);
}
g_strfreev (data_dirs);
}
else
{
const char *const *system_data_dirs = g_get_system_data_dirs ();
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
{
candidate = g_build_filename (system_data_dirs[i], "themes", parent_name, NULL);
if (path_exists_as_dir (candidate))
return candidate;
g_free (candidate);
}
}
}
user_dir = zoitechat_gtk3_theme_service_get_user_themes_dir ();
candidate = g_build_filename (user_dir, parent_name, NULL);
g_free (user_dir);
@@ -632,12 +685,42 @@ zoitechat_gtk3_theme_service_discover (void)
g_mkdir_with_parents (user_dir, 0700);
/* Prefer the live XDG environment over GLib's process-cached copies so
* theme lookup matches what GTK resolves for the running session. */
{
const char *xdg_data_home = g_getenv ("XDG_DATA_HOME");
if (xdg_data_home && xdg_data_home[0])
user_data_themes = g_build_filename (xdg_data_home, "themes", NULL);
else
user_data_themes = g_build_filename (g_get_user_data_dir (), "themes", NULL);
}
add_theme_root (user_roots, seen_user_roots, user_data_themes);
g_free (user_data_themes);
add_theme_root (user_roots, seen_user_roots, home_themes);
add_theme_root (user_roots, seen_user_roots, user_dir);
{
const char *xdg_data_dirs = g_getenv ("XDG_DATA_DIRS");
if (xdg_data_dirs && xdg_data_dirs[0])
{
char **data_dirs = g_strsplit (xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, -1);
for (i = 0; data_dirs && data_dirs[i]; i++)
{
char *system_themes;
if (!data_dirs[i][0])
continue;
system_themes = g_build_filename (data_dirs[i], "themes", NULL);
add_theme_root (system_roots, seen_system_roots, system_themes);
g_free (system_themes);
}
g_strfreev (data_dirs);
}
else
{
system_data_dirs = g_get_system_data_dirs ();
for (i = 0; system_data_dirs && system_data_dirs[i]; i++)
{
@@ -645,6 +728,8 @@ zoitechat_gtk3_theme_service_discover (void)
add_theme_root (system_roots, seen_system_roots, system_themes);
g_free (system_themes);
}
}
}
for (i = 0; i < system_roots->len; i++)
discover_dir (themes, seen_theme_roots, g_ptr_array_index (system_roots, i), ZOITECHAT_GTK3_THEME_SOURCE_SYSTEM);
@@ -922,7 +1007,7 @@ static gboolean
validate_theme_root_for_import (const char *theme_root, GError **error)
{
char *index_theme;
GKeyFile *keyfile;
GKeyFile *keyfile = NULL;
char *css_dir;
char *css_path;
char *raw_inherits;
@@ -930,16 +1015,12 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
guint i;
GError *load_error = NULL;
/* index.theme is optional for GTK3 CSS themes (GTK only needs
* gtk-3.x/gtk.css), but when it is present it must be well formed so
* broken archives are rejected up front. */
index_theme = g_build_filename (theme_root, "index.theme", NULL);
if (!g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
if (g_file_test (index_theme, G_FILE_TEST_IS_REGULAR))
{
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Invalid GTK3 theme at '%s': missing required index.theme at '%s'.",
theme_root, index_theme);
g_free (index_theme);
return FALSE;
}
keyfile = g_key_file_new ();
if (!g_key_file_load_from_file (keyfile, index_theme, G_KEY_FILE_NONE, &load_error))
{
@@ -961,6 +1042,7 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
g_free (index_theme);
return FALSE;
}
}
css_dir = zoitechat_gtk3_theme_pick_css_dir (theme_root);
if (!css_dir)
@@ -968,6 +1050,7 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Invalid GTK3 theme at '%s': could not resolve a GTK CSS directory (expected gtk-3.x/gtk.css).",
theme_root);
if (keyfile)
g_key_file_unref (keyfile);
g_free (index_theme);
return FALSE;
@@ -981,6 +1064,7 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
theme_root, css_path);
g_free (css_path);
g_free (css_dir);
if (keyfile)
g_key_file_unref (keyfile);
g_free (index_theme);
return FALSE;
@@ -988,8 +1072,12 @@ validate_theme_root_for_import (const char *theme_root, GError **error)
g_free (css_path);
g_free (css_dir);
raw_inherits = NULL;
if (keyfile)
{
raw_inherits = g_key_file_get_string (keyfile, "Desktop Entry", "Inherits", NULL);
g_key_file_unref (keyfile);
}
g_free (index_theme);
if (!raw_inherits)
return TRUE;

View File

@@ -3,7 +3,6 @@ common_sources = [
'chanopt.c',
'ctcp.c',
'dcc.c',
'emoji-data.c',
'gtk3-theme-service.c',
'zoitechat.c',
'history.c',
@@ -110,20 +109,6 @@ public_suffix_data = custom_target('public_suffix_data_h',
command: [python3, files('gen-public-suffix.py'), '@OUTPUT@']
)
emoji_data_table = custom_target('emoji_data_table_h',
input: [
'../../data/emoji/emoji-test.txt',
'../../data/emoji/cldr-annotations-en.xml',
'../../data/emoji/cldr-annotations-derived-en.xml',
'../../data/emoji/gemoji.json',
'../../data/emoji/VERSIONS',
],
output: 'emoji-data-table.h',
command: [python3, files('../../tools/gen-emoji-data.py'),
'@INPUT0@', '@INPUT1@', '@INPUT2@', '@INPUT3@', '@INPUT4@',
'@OUTPUT@']
)
textevents = custom_target('textevents',
input: 'textevents.in',
output: ['textevents.h', 'textenums.h'],
@@ -147,7 +132,7 @@ if get_option('plugin')
endif
zoitechat_common = static_library('zoitechatcommon',
sources: [textevents, public_suffix_data, emoji_data_table] + marshal + common_sources + secretstore_sources,
sources: [textevents, public_suffix_data] + marshal + common_sources + secretstore_sources,
include_directories: config_h_include,
dependencies: common_deps + common_sysinfo_deps,
c_args: common_cflags,
@@ -182,18 +167,3 @@ test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
protocol: 'tap',
timeout: 120,
)
emoji_data_tests = executable('emoji_data_tests',
[
'tests/test-emoji-data.c',
'emoji-data.c',
emoji_data_table,
],
include_directories: [config_h_include, include_directories('.')],
dependencies: [libgio_dep],
)
test('Emoji Data Tests', emoji_data_tests,
protocol: 'tap',
timeout: 120,
)

View File

@@ -439,99 +439,8 @@ servlist_favchan_copy (favchannel *fav)
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
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;
GSList *list;
@@ -578,11 +487,7 @@ servlist_connect_with_password (session *sess, ircnet *net, gboolean join, const
}
serv->password[0] = 0;
if (password && *password)
{
safe_strcpy (serv->password, password, sizeof (serv->password));
}
else if ((net->flags & FLAG_USE_KEYRING) && net->name)
if ((net->flags & FLAG_USE_KEYRING) && net->name)
{
char *stored_pass = secretstore_get_network_password (net->name);
if (stored_pass && *stored_pass)

View File

@@ -63,8 +63,7 @@ extern GSList *network_list;
#define FLAG_ALLOW_INVALID 32
#define FLAG_FAVORITE 64
#define FLAG_USE_KEYRING 128
#define FLAG_PROMPT_PASSWORD 256
#define FLAG_COUNT 9
#define FLAG_COUNT 8
/* 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 */

View File

@@ -1,240 +0,0 @@
/* ZoiteChat
* Copyright (C) 2026 deepend-tildeclub.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <string.h>
#include <glib.h>
#include "../emoji-data.h"
#define GRINNING_FACE "\360\237\230\200" /* 😀 */
#define DUCK "\360\237\246\206" /* 🦆 */
#define THUMBS_UP "\360\237\221\215" /* 👍 */
#define WAVE_DARK "\360\237\221\213\360\237\217\277" /* 👋🏿 */
#define MEDIUM_TONE_MODIFIER "\360\237\217\275" /* U+1F3FD */
#define KEYCAP_HASH "#\357\270\217\342\203\243" /* #️⃣ */
static const EmojiEntry *
find_by_name (const char *name)
{
gsize i;
for (i = 0; i < emoji_data_count (); i++)
{
const EmojiEntry *entry = emoji_data_entry (i);
if (!strcmp (entry->name, name))
return entry;
}
return NULL;
}
static void
test_catalog_integrity (void)
{
gsize i;
g_assert_cmpuint (emoji_data_count (), >=, 1800);
for (i = 0; i < emoji_data_count (); i++)
{
const EmojiEntry *entry = emoji_data_entry (i);
g_assert_nonnull (entry->sequence);
g_assert_true (*entry->sequence != '\0');
g_assert_true (g_utf8_validate (entry->sequence, -1, NULL));
g_assert_nonnull (entry->name);
g_assert_true (*entry->name != '\0');
g_assert_true (g_utf8_validate (entry->name, -1, NULL));
g_assert_nonnull (entry->search);
g_assert_true (*entry->search != '\0');
g_assert_nonnull (entry->aliases);
g_assert_cmpuint (entry->group, <, EMOJI_GROUP_COUNT);
g_assert_cmpint (entry->tone_set, >=, -1);
}
}
static void
test_cldr_order (void)
{
const EmojiEntry *first = emoji_data_entry (0);
g_assert_cmpstr (first->sequence, ==, GRINNING_FACE);
g_assert_cmpuint (first->group, ==, EMOJI_GROUP_SMILEYS_EMOTION);
}
static void
test_groups (void)
{
int group;
gboolean seen[EMOJI_GROUP_COUNT] = { FALSE };
gsize i;
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
{
g_assert_true (*emoji_data_group_display_name (group) != '\0');
g_assert_true (*emoji_data_group_icon_sequence (group) != '\0');
}
for (i = 0; i < emoji_data_count (); i++)
seen[emoji_data_entry (i)->group] = TRUE;
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
g_assert_true (seen[group]);
}
static void
test_search_by_keyword (void)
{
const EmojiEntry *duck = find_by_name ("duck");
char **tokens = emoji_search_tokenize ("Duck");
g_assert_nonnull (duck);
g_assert_cmpstr (duck->sequence, ==, DUCK);
g_assert_nonnull (tokens);
g_assert_true (emoji_entry_matches (duck, tokens));
g_strfreev (tokens);
tokens = emoji_search_tokenize ("bird");
g_assert_true (emoji_entry_matches (duck, tokens));
g_strfreev (tokens);
tokens = emoji_search_tokenize ("no-such-emoji-keyword");
g_assert_false (emoji_entry_matches (duck, tokens));
g_strfreev (tokens);
}
static void
test_search_by_alias (void)
{
const EmojiEntry *thumbs = find_by_name ("thumbs up");
char **tokens;
g_assert_nonnull (thumbs);
g_assert_nonnull (strstr (thumbs->aliases, "thumbsup"));
tokens = emoji_search_tokenize (":thumbsup:");
g_assert_nonnull (tokens);
g_assert_true (emoji_entry_matches (thumbs, tokens));
g_strfreev (tokens);
}
static void
test_multi_token_search (void)
{
const EmojiEntry *flag = find_by_name ("flag: Canada");
char **tokens = emoji_search_tokenize ("flag canada");
g_assert_nonnull (flag);
g_assert_cmpuint (flag->group, ==, EMOJI_GROUP_FLAGS);
g_assert_nonnull (tokens);
g_assert_true (emoji_entry_matches (flag, tokens));
g_strfreev (tokens);
}
static void
test_skin_tones (void)
{
const EmojiEntry *thumbs = find_by_name ("thumbs up");
const EmojiEntry *duck = find_by_name ("duck");
const char *toned;
g_assert_nonnull (thumbs);
g_assert_cmpint (thumbs->tone_set, >=, 0);
toned = emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_MEDIUM);
g_assert_cmpstr (toned, !=, thumbs->sequence);
g_assert_nonnull (strstr (toned, MEDIUM_TONE_MODIFIER));
g_assert_cmpstr (emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_NONE),
==, thumbs->sequence);
g_assert_nonnull (duck);
g_assert_cmpint (duck->tone_set, ==, -1);
g_assert_cmpstr (emoji_entry_sequence_for_tone (duck, EMOJI_TONE_DARK),
==, duck->sequence);
}
static void
test_find_by_sequence (void)
{
EmojiTone tone;
const EmojiEntry *entry;
entry = emoji_data_find_by_sequence (GRINNING_FACE, &tone);
g_assert_nonnull (entry);
g_assert_cmpstr (entry->name, ==, "grinning face");
g_assert_cmpuint (tone, ==, EMOJI_TONE_NONE);
entry = emoji_data_find_by_sequence (WAVE_DARK, &tone);
g_assert_nonnull (entry);
g_assert_cmpstr (entry->name, ==, "waving hand");
g_assert_cmpuint (tone, ==, EMOJI_TONE_DARK);
g_assert_null (emoji_data_find_by_sequence ("not an emoji", &tone));
g_assert_null (emoji_data_find_by_sequence ("", NULL));
g_assert_null (emoji_data_find_by_sequence (NULL, NULL));
}
static void
test_grapheme_sequences_intact (void)
{
const EmojiEntry *keycap = find_by_name ("keycap: #");
const EmojiEntry *family = find_by_name ("family: man, woman, boy");
g_assert_nonnull (keycap);
g_assert_cmpstr (keycap->sequence, ==, KEYCAP_HASH);
g_assert_nonnull (family);
g_assert_nonnull (strstr (family->sequence, "\342\200\215"));
}
static void
test_tokenize_edge_cases (void)
{
g_assert_null (emoji_search_tokenize (NULL));
g_assert_null (emoji_search_tokenize (""));
g_assert_null (emoji_search_tokenize (" "));
g_assert_null (emoji_search_tokenize (":::"));
}
static void
test_versions (void)
{
g_assert_true (*emoji_data_unicode_version () != '\0');
g_assert_true (*emoji_data_cldr_version () != '\0');
}
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/emoji-data/catalog-integrity", test_catalog_integrity);
g_test_add_func ("/emoji-data/cldr-order", test_cldr_order);
g_test_add_func ("/emoji-data/groups", test_groups);
g_test_add_func ("/emoji-data/search-by-keyword", test_search_by_keyword);
g_test_add_func ("/emoji-data/search-by-alias", test_search_by_alias);
g_test_add_func ("/emoji-data/multi-token-search", test_multi_token_search);
g_test_add_func ("/emoji-data/skin-tones", test_skin_tones);
g_test_add_func ("/emoji-data/find-by-sequence", test_find_by_sequence);
g_test_add_func ("/emoji-data/grapheme-sequences-intact",
test_grapheme_sequences_intact);
g_test_add_func ("/emoji-data/tokenize-edge-cases",
test_tokenize_edge_cases);
g_test_add_func ("/emoji-data/versions", test_versions);
return g_test_run ();
}

View File

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

View File

@@ -259,7 +259,6 @@ struct zoitechatprefs
int hex_gui_dialog_left;
int hex_gui_dialog_top;
int hex_gui_dialog_width;
int hex_gui_emoji_skin_tone;
int hex_gui_lagometer;
int hex_gui_lang;
int hex_gui_pane_divider_position;

View File

@@ -1,779 +0,0 @@
/* ZoiteChat
* Copyright (C) 2026 deepend-tildeclub.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
/* ZoiteChat's own emoji picker. Replaces GTK's built-in "show-emoji-icon"
* entry machinery so that every build offers the same catalog (bundled at
* compile time from pinned Unicode/CLDR data, see src/common/emoji-data.h)
* regardless of the GTK runtime version or distribution emoji data.
* Selecting an emoji inserts the plain Unicode sequence at the cursor -
* nothing about what is sent over IRC changes. */
#include <string.h>
#include "fe-gtk.h"
#include "../common/zoitechat.h"
#include "../common/zoitechatc.h"
#include "../common/cfgfiles.h"
#include "../common/emoji-data.h"
#include "emoji-picker.h"
#include "preferences-persistence.h"
#define EMOJI_CATEGORY_RECENT -1
#define EMOJI_RECENTS_MAX 32
#define EMOJI_SEARCH_MAX_RESULTS 256
#define EMOJI_GRID_COLUMNS 9
typedef struct
{
GtkWidget *button;
GtkWidget *target; /* weak pointer to the input entry */
GtkWidget *popover;
GtkWidget *search_entry;
GtkWidget *stack;
GtkWidget *grid; /* GtkFlowBox */
GtkWidget *scroller;
GtkWidget *empty_label;
GtkWidget *category_buttons[EMOJI_GROUP_COUNT + 1]; /* [0] = recents */
GtkWidget *tone_buttons[EMOJI_TONE_COUNT];
int category; /* EMOJI_CATEGORY_RECENT or EmojiGroup */
gboolean populated;
gboolean updating_buttons;
} EmojiPicker;
/* ------------------------------------------------------------------------ *
* Recently used emoji, shared by all windows and persisted across runs
* ------------------------------------------------------------------------ */
static GPtrArray *recent_emoji = NULL;
static char *
emoji_recents_filename (void)
{
return g_build_filename (get_xdir (), "emoji-recents.conf", NULL);
}
static void
emoji_recents_load (void)
{
char *filename, *contents = NULL;
char **lines;
gsize i;
if (recent_emoji)
return;
recent_emoji = g_ptr_array_new_with_free_func (g_free);
filename = emoji_recents_filename ();
if (g_file_get_contents (filename, &contents, NULL, NULL))
{
lines = g_strsplit (contents, "\n", -1);
for (i = 0; lines[i] && recent_emoji->len < EMOJI_RECENTS_MAX; i++)
{
g_strstrip (lines[i]);
/* only keep sequences that exist in the bundled catalog */
if (*lines[i] && emoji_data_find_by_sequence (lines[i], NULL))
g_ptr_array_add (recent_emoji, g_strdup (lines[i]));
}
g_strfreev (lines);
g_free (contents);
}
g_free (filename);
}
static void
emoji_recents_save (void)
{
GString *contents = g_string_new (NULL);
char *filename;
gsize i;
for (i = 0; i < recent_emoji->len; i++)
{
g_string_append (contents, g_ptr_array_index (recent_emoji, i));
g_string_append_c (contents, '\n');
}
filename = emoji_recents_filename ();
g_file_set_contents (filename, contents->str, contents->len, NULL);
g_free (filename);
g_string_free (contents, TRUE);
}
static void
emoji_recents_add (const char *sequence)
{
GPtrArray *updated;
gsize i;
emoji_recents_load ();
updated = g_ptr_array_new_with_free_func (g_free);
g_ptr_array_add (updated, g_strdup (sequence));
for (i = 0; i < recent_emoji->len && updated->len < EMOJI_RECENTS_MAX; i++)
{
const char *old = g_ptr_array_index (recent_emoji, i);
if (strcmp (old, sequence) != 0)
g_ptr_array_add (updated, g_strdup (old));
}
g_ptr_array_free (recent_emoji, TRUE);
recent_emoji = updated;
emoji_recents_save ();
}
/* ------------------------------------------------------------------------ *
* Appearance helpers
* ------------------------------------------------------------------------ */
/* One application-wide provider so every emoji cell prefers a color emoji
* font without changing the fonts of surrounding widgets. Emoji families
* that are not installed are simply skipped by fontconfig/Pango. */
static void
emoji_picker_ensure_css (void)
{
static gboolean done = FALSE;
GtkCssProvider *provider;
if (done)
return;
done = TRUE;
provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider,
".zc-emoji-cell {"
" font-family: \"Noto Color Emoji\", \"Segoe UI Emoji\","
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
" font-size: 15pt;"
" padding: 2px;"
"}"
".zc-emoji-cell-small {"
" font-family: \"Noto Color Emoji\", \"Segoe UI Emoji\","
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
"}",
-1, NULL);
gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
g_object_unref (provider);
}
static GtkWidget *
emoji_label_new (const char *sequence, gboolean small)
{
GtkWidget *label = gtk_label_new (sequence);
gtk_style_context_add_class (gtk_widget_get_style_context (label),
small ? "zc-emoji-cell-small" : "zc-emoji-cell");
return label;
}
static void
emoji_picker_set_a11y_name (GtkWidget *widget, const char *name)
{
AtkObject *accessible = gtk_widget_get_accessible (widget);
if (accessible)
atk_object_set_name (accessible, name);
}
static EmojiTone
emoji_picker_current_tone (void)
{
int tone = prefs.hex_gui_emoji_skin_tone;
if (tone < EMOJI_TONE_NONE || tone >= EMOJI_TONE_COUNT)
return EMOJI_TONE_NONE;
return (EmojiTone) tone;
}
/* "waving hand" / "waving hand (Dark skin tone)", with :shortcodes: on a
* second line - used for both tooltips and accessible names. */
static char *
emoji_picker_describe (const EmojiEntry *entry, EmojiTone tone)
{
GString *desc = g_string_new (NULL);
char **aliases;
gsize i;
if (tone != EMOJI_TONE_NONE && entry->tone_set >= 0)
g_string_append_printf (desc, "%s (%s)", entry->name,
emoji_data_tone_display_name (tone));
else
g_string_append (desc, entry->name);
if (*entry->aliases)
{
g_string_append_c (desc, '\n');
aliases = g_strsplit (entry->aliases, " ", -1);
for (i = 0; aliases[i]; i++)
{
if (i > 0)
g_string_append_c (desc, ' ');
g_string_append_printf (desc, ":%s:", aliases[i]);
}
g_strfreev (aliases);
}
return g_string_free (desc, FALSE);
}
/* ------------------------------------------------------------------------ *
* Insertion
* ------------------------------------------------------------------------ */
static void
emoji_picker_insert (EmojiPicker *picker, const char *sequence)
{
GtkEditable *editable;
int pos;
if (!picker->target || !sequence || !*sequence)
return;
gtk_popover_popdown (GTK_POPOVER (picker->popover));
editable = GTK_EDITABLE (picker->target);
gtk_editable_delete_selection (editable);
pos = gtk_editable_get_position (editable);
gtk_editable_insert_text (editable, sequence, strlen (sequence), &pos);
/* focus the entry without letting GtkEntry select everything */
gtk_editable_set_editable (editable, FALSE);
gtk_widget_grab_focus (picker->target);
gtk_editable_set_editable (editable, TRUE);
gtk_editable_set_position (editable, pos);
emoji_recents_add (sequence);
}
static void
emoji_picker_activate_child (EmojiPicker *picker, GtkFlowBoxChild *child)
{
emoji_picker_insert (picker,
g_object_get_data (G_OBJECT (child), "emoji-sequence"));
}
static void
emoji_grid_child_activated_cb (GtkFlowBox *grid, GtkFlowBoxChild *child,
gpointer userdata)
{
emoji_picker_activate_child (userdata, child);
}
/* ------------------------------------------------------------------------ *
* Grid contents
* ------------------------------------------------------------------------ */
static void
emoji_grid_add (EmojiPicker *picker, const EmojiEntry *entry,
const char *sequence, EmojiTone tone)
{
GtkWidget *child, *label;
char *desc;
label = emoji_label_new (sequence, FALSE);
child = gtk_flow_box_child_new ();
gtk_container_add (GTK_CONTAINER (child), label);
g_object_set_data_full (G_OBJECT (child), "emoji-sequence",
g_strdup (sequence), g_free);
desc = emoji_picker_describe (entry, tone);
gtk_widget_set_tooltip_text (child, desc);
emoji_picker_set_a11y_name (child, desc);
g_free (desc);
gtk_container_add (GTK_CONTAINER (picker->grid), child);
}
static void
emoji_picker_refresh (EmojiPicker *picker)
{
const char *text = gtk_entry_get_text (GTK_ENTRY (picker->search_entry));
EmojiTone tone = emoji_picker_current_tone ();
GtkAdjustment *vadj;
gboolean any = FALSE;
gsize i, results = 0;
gtk_container_foreach (GTK_CONTAINER (picker->grid),
(GtkCallback) gtk_widget_destroy, NULL);
if (text && *text)
{
char **tokens = emoji_search_tokenize (text);
if (tokens)
{
for (i = 0; i < emoji_data_count () &&
results < EMOJI_SEARCH_MAX_RESULTS; i++)
{
const EmojiEntry *entry = emoji_data_entry (i);
if (!emoji_entry_matches (entry, tokens))
continue;
emoji_grid_add (picker, entry,
emoji_entry_sequence_for_tone (entry, tone), tone);
results++;
}
g_strfreev (tokens);
any = results > 0;
}
gtk_label_set_text (GTK_LABEL (picker->empty_label),
_("No emoji found"));
}
else if (picker->category == EMOJI_CATEGORY_RECENT)
{
emoji_recents_load ();
for (i = 0; i < recent_emoji->len; i++)
{
const char *sequence = g_ptr_array_index (recent_emoji, i);
EmojiTone seq_tone;
const EmojiEntry *entry =
emoji_data_find_by_sequence (sequence, &seq_tone);
if (!entry)
continue;
emoji_grid_add (picker, entry, sequence, seq_tone);
any = TRUE;
}
gtk_label_set_text (GTK_LABEL (picker->empty_label),
_("No recently used emoji"));
}
else
{
for (i = 0; i < emoji_data_count (); i++)
{
const EmojiEntry *entry = emoji_data_entry (i);
if (entry->group != picker->category)
continue;
emoji_grid_add (picker, entry,
emoji_entry_sequence_for_tone (entry, tone), tone);
any = TRUE;
}
}
gtk_widget_show_all (picker->grid);
gtk_stack_set_visible_child_name (GTK_STACK (picker->stack),
any ? "grid" : "empty");
vadj = gtk_scrolled_window_get_vadjustment (
GTK_SCROLLED_WINDOW (picker->scroller));
gtk_adjustment_set_value (vadj, gtk_adjustment_get_lower (vadj));
}
/* ------------------------------------------------------------------------ *
* Categories
* ------------------------------------------------------------------------ */
static void
emoji_picker_sync_category_buttons (EmojiPicker *picker)
{
gsize i;
picker->updating_buttons = TRUE;
for (i = 0; i < G_N_ELEMENTS (picker->category_buttons); i++)
{
int category = (int) i - 1;
gtk_toggle_button_set_active (
GTK_TOGGLE_BUTTON (picker->category_buttons[i]),
category == picker->category);
}
picker->updating_buttons = FALSE;
}
static void
emoji_category_toggled_cb (GtkToggleButton *button, gpointer userdata)
{
EmojiPicker *picker = userdata;
int category = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
"emoji-category"));
if (picker->updating_buttons)
return;
if (!gtk_toggle_button_get_active (button))
{
/* the active category cannot be untoggled */
if (picker->category == category)
emoji_picker_sync_category_buttons (picker);
return;
}
picker->category = category;
emoji_picker_sync_category_buttons (picker);
/* leave search mode; clearing the entry triggers the refresh */
if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry)))
gtk_entry_set_text (GTK_ENTRY (picker->search_entry), "");
else
emoji_picker_refresh (picker);
}
static GtkWidget *
emoji_category_button_new (EmojiPicker *picker, int category,
const char *icon_sequence, const char *name)
{
GtkWidget *button = gtk_toggle_button_new ();
gtk_container_add (GTK_CONTAINER (button),
emoji_label_new (icon_sequence, TRUE));
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
gtk_widget_set_tooltip_text (button, name);
emoji_picker_set_a11y_name (button, name);
g_object_set_data (G_OBJECT (button), "emoji-category",
GINT_TO_POINTER (category));
g_signal_connect (G_OBJECT (button), "toggled",
G_CALLBACK (emoji_category_toggled_cb), picker);
return button;
}
/* ------------------------------------------------------------------------ *
* Skin tones
* ------------------------------------------------------------------------ */
#define EMOJI_TONE_SAMPLE "\342\234\213" /* ✋ raised hand */
static void
emoji_tone_toggled_cb (GtkToggleButton *button, gpointer userdata)
{
EmojiPicker *picker = userdata;
int tone = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
"emoji-tone"));
if (!gtk_toggle_button_get_active (button))
return;
if (prefs.hex_gui_emoji_skin_tone == tone)
return;
prefs.hex_gui_emoji_skin_tone = tone;
preferences_persistence_save_all ();
emoji_picker_refresh (picker);
}
static GtkWidget *
emoji_tone_row_new (EmojiPicker *picker)
{
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *button;
GSList *group = NULL;
const EmojiEntry *sample = emoji_data_find_by_sequence (EMOJI_TONE_SAMPLE,
NULL);
int tone;
gtk_widget_set_halign (box, GTK_ALIGN_CENTER);
for (tone = EMOJI_TONE_NONE; tone < EMOJI_TONE_COUNT; tone++)
{
const char *sequence = EMOJI_TONE_SAMPLE;
if (sample)
sequence = emoji_entry_sequence_for_tone (sample, tone);
button = gtk_radio_button_new (group);
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (button), FALSE);
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
gtk_container_add (GTK_CONTAINER (button),
emoji_label_new (sequence, TRUE));
gtk_widget_set_tooltip_text (button,
emoji_data_tone_display_name (tone));
emoji_picker_set_a11y_name (button,
emoji_data_tone_display_name (tone));
g_object_set_data (G_OBJECT (button), "emoji-tone",
GINT_TO_POINTER (tone));
picker->tone_buttons[tone] = button;
if (emoji_picker_current_tone () == tone)
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
g_signal_connect (G_OBJECT (button), "toggled",
G_CALLBACK (emoji_tone_toggled_cb), picker);
gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
}
return box;
}
/* the pref can also change externally (/set gui_emoji_skin_tone) */
static void
emoji_picker_sync_tone_buttons (EmojiPicker *picker)
{
EmojiTone tone = emoji_picker_current_tone ();
gtk_toggle_button_set_active (
GTK_TOGGLE_BUTTON (picker->tone_buttons[tone]), TRUE);
}
/* ------------------------------------------------------------------------ *
* Search
* ------------------------------------------------------------------------ */
static void
emoji_search_changed_cb (GtkSearchEntry *entry, gpointer userdata)
{
emoji_picker_refresh (userdata);
}
static void
emoji_search_activate_cb (GtkEntry *entry, gpointer userdata)
{
EmojiPicker *picker = userdata;
GtkFlowBoxChild *child =
gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0);
/* Enter inserts the first hit */
if (child)
emoji_picker_activate_child (picker, child);
}
static gboolean
emoji_search_key_press_cb (GtkWidget *entry, GdkEventKey *event,
gpointer userdata)
{
EmojiPicker *picker = userdata;
GtkFlowBoxChild *child;
if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down)
{
child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0);
if (child)
{
gtk_widget_grab_focus (GTK_WIDGET (child));
return TRUE;
}
}
return FALSE;
}
/* ------------------------------------------------------------------------ *
* Popover assembly
* ------------------------------------------------------------------------ */
static void
emoji_picker_build_contents (EmojiPicker *picker)
{
GtkWidget *vbox, *categories, *button;
int group;
emoji_picker_ensure_css ();
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
g_object_set (vbox, "margin", 6, NULL);
picker->search_entry = gtk_search_entry_new ();
gtk_entry_set_placeholder_text (GTK_ENTRY (picker->search_entry),
_("Search emoji"));
emoji_picker_set_a11y_name (picker->search_entry, _("Search emoji"));
g_signal_connect (G_OBJECT (picker->search_entry), "search-changed",
G_CALLBACK (emoji_search_changed_cb), picker);
g_signal_connect (G_OBJECT (picker->search_entry), "activate",
G_CALLBACK (emoji_search_activate_cb), picker);
g_signal_connect (G_OBJECT (picker->search_entry), "key-press-event",
G_CALLBACK (emoji_search_key_press_cb), picker);
gtk_box_pack_start (GTK_BOX (vbox), picker->search_entry, FALSE, FALSE, 0);
categories = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_set_homogeneous (GTK_BOX (categories), TRUE);
picker->category_buttons[0] = emoji_category_button_new (picker,
EMOJI_CATEGORY_RECENT, "\360\237\225\230" /* 🕘 */,
_("Recently Used"));
gtk_box_pack_start (GTK_BOX (categories), picker->category_buttons[0],
TRUE, TRUE, 0);
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
{
button = emoji_category_button_new (picker, group,
emoji_data_group_icon_sequence (group),
emoji_data_group_display_name (group));
picker->category_buttons[group + 1] = button;
gtk_box_pack_start (GTK_BOX (categories), button, TRUE, TRUE, 0);
}
gtk_box_pack_start (GTK_BOX (vbox), categories, FALSE, FALSE, 0);
picker->grid = gtk_flow_box_new ();
gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (picker->grid),
GTK_SELECTION_NONE);
gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (picker->grid),
TRUE);
gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (picker->grid),
EMOJI_GRID_COLUMNS);
gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (picker->grid),
EMOJI_GRID_COLUMNS);
gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (picker->grid), TRUE);
gtk_widget_set_valign (picker->grid, GTK_ALIGN_START);
g_signal_connect (G_OBJECT (picker->grid), "child-activated",
G_CALLBACK (emoji_grid_child_activated_cb), picker);
picker->scroller = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (picker->scroller),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_height (
GTK_SCROLLED_WINDOW (picker->scroller), 300);
gtk_container_add (GTK_CONTAINER (picker->scroller), picker->grid);
picker->empty_label = gtk_label_new (_("No emoji found"));
gtk_style_context_add_class (
gtk_widget_get_style_context (picker->empty_label), "dim-label");
picker->stack = gtk_stack_new ();
gtk_widget_set_size_request (picker->stack, 380, -1);
gtk_stack_add_named (GTK_STACK (picker->stack), picker->scroller, "grid");
gtk_stack_add_named (GTK_STACK (picker->stack), picker->empty_label,
"empty");
gtk_box_pack_start (GTK_BOX (vbox), picker->stack, TRUE, TRUE, 0);
gtk_box_pack_start (GTK_BOX (vbox), emoji_tone_row_new (picker),
FALSE, FALSE, 0);
gtk_container_add (GTK_CONTAINER (picker->popover), vbox);
gtk_widget_show_all (vbox);
emoji_picker_sync_category_buttons (picker);
}
static void
emoji_popover_show_cb (GtkWidget *popover, gpointer userdata)
{
EmojiPicker *picker = userdata;
if (!picker->populated)
{
picker->populated = TRUE;
emoji_picker_build_contents (picker);
}
else if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry)))
{
gtk_entry_set_text (GTK_ENTRY (picker->search_entry), "");
}
emoji_picker_sync_tone_buttons (picker);
emoji_picker_refresh (picker);
gtk_widget_grab_focus (picker->search_entry);
}
static void
emoji_popover_closed_cb (GtkWidget *popover, gpointer userdata)
{
EmojiPicker *picker = userdata;
GtkEditable *editable;
if (!picker->target)
return;
/* hand focus back to the input box without selecting its contents */
editable = GTK_EDITABLE (picker->target);
gtk_editable_set_editable (editable, FALSE);
gtk_widget_grab_focus (picker->target);
gtk_editable_set_editable (editable, TRUE);
}
static const char *
emoji_picker_button_icon_name (void)
{
static const char *icon_names[] = {
"face-smile-symbolic",
"face-smile",
"insert-emoticon-symbolic",
"insert-emoticon",
"zc-menu-emoji",
};
GtkIconTheme *theme = gtk_icon_theme_get_default ();
gsize i;
if (!theme)
return NULL;
for (i = 0; i < G_N_ELEMENTS (icon_names); i++)
{
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
return icon_names[i];
}
return NULL;
}
static void
emoji_picker_free (gpointer data)
{
EmojiPicker *picker = data;
if (picker->target)
g_object_remove_weak_pointer (G_OBJECT (picker->target),
(gpointer *) &picker->target);
g_free (picker);
}
GtkWidget *
emoji_picker_button_new (GtkWidget *target_entry)
{
EmojiPicker *picker;
const char *icon_name;
g_return_val_if_fail (GTK_IS_ENTRY (target_entry), NULL);
picker = g_new0 (EmojiPicker, 1);
picker->target = target_entry;
picker->category = EMOJI_GROUP_SMILEYS_EMOTION;
g_object_add_weak_pointer (G_OBJECT (target_entry),
(gpointer *) &picker->target);
picker->button = gtk_menu_button_new ();
gtk_button_set_relief (GTK_BUTTON (picker->button), GTK_RELIEF_NONE);
gtk_widget_set_can_focus (picker->button, FALSE);
gtk_widget_set_tooltip_text (picker->button, _("Insert Emoji"));
emoji_picker_set_a11y_name (picker->button, _("Insert Emoji"));
g_object_set_data_full (G_OBJECT (picker->button), "emoji-picker",
picker, emoji_picker_free);
icon_name = emoji_picker_button_icon_name ();
if (icon_name)
{
gtk_button_set_image (GTK_BUTTON (picker->button),
gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU));
}
else
{
/* no themed icon anywhere: fall back to an emoji label so the
* button never turns into an unusable placeholder */
emoji_picker_ensure_css ();
gtk_container_add (GTK_CONTAINER (picker->button),
emoji_label_new ("\360\237\231\202" /* 🙂 */, TRUE));
}
/* the popover shell is cheap; its contents are built on first open so
* startup cost stays unchanged */
picker->popover = gtk_popover_new (picker->button);
gtk_popover_set_position (GTK_POPOVER (picker->popover), GTK_POS_TOP);
g_signal_connect (G_OBJECT (picker->popover), "show",
G_CALLBACK (emoji_popover_show_cb), picker);
g_signal_connect (G_OBJECT (picker->popover), "closed",
G_CALLBACK (emoji_popover_closed_cb), picker);
gtk_menu_button_set_popover (GTK_MENU_BUTTON (picker->button),
picker->popover);
return picker->button;
}

View File

@@ -1,31 +0,0 @@
/* ZoiteChat
* Copyright (C) 2026 deepend-tildeclub.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#ifndef ZOITECHAT_EMOJI_PICKER_H
#define ZOITECHAT_EMOJI_PICKER_H
#include <gtk/gtk.h>
/* Creates the emoji picker button for an input entry. The returned
* GtkMenuButton opens a popover with search, categories, recently used
* emoji and skin-tone selection; activating an emoji inserts its Unicode
* sequence into target_entry at the cursor. The popover contents are
* built lazily on first open. */
GtkWidget *emoji_picker_button_new (GtkWidget *target_entry);
#endif

View File

@@ -34,10 +34,6 @@
#include <unistd.h>
#endif
#ifdef HAVE_FONTCONFIG
#include <fontconfig/fontconfig.h>
#endif
#include "../common/zoitechat.h"
#include "../common/fe.h"
#include "../common/util.h"
@@ -106,95 +102,6 @@ static const GOptionEntry gopt_entries[] =
{NULL}
};
#ifdef WIN32
static gunichar2 *win32_private_emoji_font;
#endif
static char *
fe_bundled_emoji_font_path (void)
{
const char *override_path;
char *font_path = NULL;
override_path = g_getenv ("ZOITECHAT_EMOJI_FONT");
if (override_path && *override_path)
font_path = g_strdup (override_path);
#ifdef WIN32
else
{
char *base_path;
base_path = g_win32_get_package_installation_directory_of_module (NULL);
if (base_path)
{
font_path = g_build_filename (base_path, "share", "fonts",
"zoitechat", "NotoColorEmoji.ttf", NULL);
g_free (base_path);
}
}
#elif defined(HAVE_FONTCONFIG)
else
{
const char *appdir;
appdir = g_getenv ("APPDIR");
if (appdir && *appdir)
font_path = g_build_filename (appdir, "usr", "share", "fonts",
"zoitechat", "NotoColorEmoji.ttf", NULL);
else
font_path = g_strdup (ZOITECHAT_EMOJI_FONT_FILE);
}
#endif
if (font_path && !g_file_test (font_path, G_FILE_TEST_IS_REGULAR))
{
g_free (font_path);
font_path = NULL;
}
return font_path;
}
static void
fe_register_bundled_emoji_font (void)
{
char *font_path;
font_path = fe_bundled_emoji_font_path ();
if (!font_path)
{
g_debug ("Bundled Noto Color Emoji font was not found; using platform font fallback");
return;
}
#ifdef WIN32
win32_private_emoji_font = g_utf8_to_utf16 (font_path, -1, NULL, NULL, NULL);
if (!win32_private_emoji_font ||
AddFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL) == 0)
{
g_warning ("Unable to register bundled emoji font: %s", font_path);
g_clear_pointer (&win32_private_emoji_font, g_free);
}
#elif defined(HAVE_FONTCONFIG)
if (!FcConfigAppFontAddFile (NULL, (const FcChar8 *) font_path))
g_warning ("Unable to register bundled emoji font: %s", font_path);
#endif
g_free (font_path);
}
static void
fe_unregister_bundled_emoji_font (void)
{
#ifdef WIN32
if (win32_private_emoji_font)
{
RemoveFontResourceExW ((LPCWSTR) win32_private_emoji_font, FR_PRIVATE, NULL);
g_clear_pointer (&win32_private_emoji_font, g_free);
}
#endif
}
#ifdef WIN32
static void
create_msg_dialog (gchar *title, gchar *message)
@@ -532,8 +439,6 @@ fe_args (int argc, char *argv[])
return 0;
}
fe_register_bundled_emoji_font ();
#ifdef WIN32
win32_set_gsettings_schema_dir ();
win32_set_appusermodelid ();
@@ -709,7 +614,6 @@ fe_main (void)
#endif
gtk_main ();
fe_unregister_bundled_emoji_font ();
/* sleep for 2 seconds so any QUIT messages are not lost. The */
/* GUI is closed at this point, so the user doesn't even know! */

View File

@@ -31,7 +31,7 @@
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
</Link>
</ItemDefinitionGroup>
@@ -97,7 +97,6 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClCompile Include="custom-list.c" />
<ClCompile Include="dccgui.c" />
<ClCompile Include="editlist.c" />
<ClCompile Include="emoji-picker.c" />
<ClCompile Include="fe-gtk.c" />
<ClCompile Include="fkeys.c" />
<ClCompile Include="gtkutil.c" />

View File

@@ -527,43 +527,6 @@ gtkutil_destroy (GtkWidget * igad, GtkWidget * 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
gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry)
{
@@ -577,15 +540,15 @@ gtkutil_get_str_response (GtkDialog *dialog, gint arg1, gpointer entry)
switch (arg1)
{
case GTK_RESPONSE_REJECT:
callback (TRUE, text, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
case GTK_RESPONSE_ACCEPT:
callback (FALSE, text, user_data);
break;
default:
callback (TRUE, text, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
}
gtk_widget_destroy (GTK_WIDGET (dialog));
}
static void
@@ -594,8 +557,8 @@ gtkutil_str_enter (GtkWidget *entry, GtkWidget *dialog)
gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
}
static void
gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean visible)
void
fe_get_str (char *msg, char *def, void *callback, void *userdata)
{
GtkWidget *dialog;
GtkWidget *entry;
@@ -609,11 +572,18 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
NULL);
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));
gtkutil_place_dialog (dialog);
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);
g_object_set_data (G_OBJECT (dialog), "cb", callback);
@@ -622,8 +592,7 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
entry = gtk_entry_new ();
g_signal_connect (G_OBJECT (entry), "activate",
G_CALLBACK (gtkutil_str_enter), dialog);
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
gtk_entry_set_text (GTK_ENTRY (entry), def);
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
label = gtk_label_new (msg);
@@ -637,18 +606,6 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
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
gtkutil_get_number_response (GtkDialog *dialog, gint arg1, gpointer spin)
{
@@ -684,15 +641,15 @@ gtkutil_get_bool_response (GtkDialog *dialog, gint arg1, gpointer spin)
switch (arg1)
{
case GTK_RESPONSE_REJECT:
callback (0, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
case GTK_RESPONSE_ACCEPT:
callback (1, user_data);
break;
default:
callback (0, user_data);
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
}
gtk_widget_destroy (GTK_WIDGET (dialog));
}
void
@@ -710,10 +667,9 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
_("_OK"), GTK_RESPONSE_ACCEPT,
NULL);
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));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
@@ -751,10 +707,10 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
_("_Yes"), GTK_RESPONSE_ACCEPT,
NULL);
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));
gtkutil_place_dialog (dialog);
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
g_object_set_data (G_OBJECT (dialog), "cb", callback);
g_object_set_data (G_OBJECT (dialog), "ud", userdata);

View File

@@ -45,7 +45,6 @@
#include "theme/theme-manager.h"
#include "theme/theme-css.h"
#include "banlist.h"
#include "emoji-picker.h"
#include "gtkutil.h"
#include "icon-resolver.h"
#include "joind.h"
@@ -4370,6 +4369,38 @@ static const char *mg_emoji_family_fallback =
"Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
#endif
static const char *
mg_find_available_icon_name (const char *const *icon_names)
{
GtkIconTheme *theme;
int i;
theme = gtk_icon_theme_get_default ();
if (!theme || !icon_names)
return NULL;
for (i = 0; icon_names[i] != NULL; i++)
{
int action;
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
return icon_names[i];
if (icon_resolver_menu_action_from_name (icon_names[i], &action))
{
char *resource_path = icon_resolver_resolve_path (ICON_RESOLVER_ROLE_MENU_ACTION, action, GTK_ICON_SIZE_MENU, "menu", ICON_RESOLVER_THEME_SYSTEM, NULL);
if (resource_path)
{
g_free (resource_path);
return icon_names[i];
}
}
}
return NULL;
}
static gboolean
mg_family_already_has_emoji (const gchar *family)
{
@@ -4696,6 +4727,15 @@ mg_create_entry (session *sess, GtkWidget *box)
{
GtkWidget *hbox, *but, *entry;
session_gui *gui = sess->gui;
const char *emoji_fallback_icon_names[] = {
"face-smile-symbolic",
"face-smile",
"insert-emoticon-symbolic",
"insert-emoticon",
"zc-menu-emoji",
NULL
};
const char *emoji_fallback_icon_name;
gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
@@ -4750,11 +4790,14 @@ mg_create_entry (session *sess, GtkWidget *box)
mg_apply_entry_style (entry);
mg_apply_entry_scroll_artifact_fix (entry);
/* ZoiteChat's own emoji picker; deliberately not GTK's
* "show-emoji-icon" machinery, so the catalog is the same on every
* GTK runtime and the button always has a working callback */
but = emoji_picker_button_new (entry);
gtk_box_pack_start (GTK_BOX (hbox), but, FALSE, FALSE, 0);
g_object_set (G_OBJECT (entry), "show-emoji-icon", TRUE, NULL);
if (gtk_entry_get_icon_storage_type (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY) == GTK_IMAGE_EMPTY)
{
emoji_fallback_icon_name = mg_find_available_icon_name (emoji_fallback_icon_names);
if (emoji_fallback_icon_name)
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY, emoji_fallback_icon_name);
}
}
static void

View File

@@ -18,7 +18,6 @@ zoitechat_gtk_sources = [
'custom-list.c',
'dccgui.c',
'editlist.c',
'emoji-picker.c',
'fe-gtk.c',
'fkeys.c',
'gtkutil.c',
@@ -49,10 +48,6 @@ zoitechat_gtk_deps = [
libgmodule_dep, # used by libsexy
]
if fontconfig_dep.found()
zoitechat_gtk_deps += fontconfig_dep
endif
gtk_dep = dependency('gtk+-3.0', version: '>= 3.22')
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep]

View File

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

View File

@@ -88,7 +88,6 @@ static GtkWidget *edit_entry_real;
static GtkWidget *edit_entry_pass;
static GtkWidget *edit_check_show_pass;
static GtkWidget *edit_check_use_keyring;
static GtkWidget *edit_check_ask_pass;
static GtkWidget *edit_button_encrypt_pass;
static GtkWidget *edit_button_import_pass;
static int edit_pass_changed;
@@ -123,11 +122,7 @@ servlist_update_password_tools (ircnet *net)
return;
use_keyring = net && (net->flags & FLAG_USE_KEYRING);
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)));
has_local = net && net->pass && *net->pass && !use_keyring && !edit_pass_changed;
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);
}
@@ -140,17 +135,6 @@ 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);
}
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 *
servlist_display_password (ircnet *net)
{
@@ -1277,7 +1261,6 @@ static void
servlist_edit_update (ircnet *net)
{
gboolean use_keyring;
gboolean ask_pass;
gboolean keyring_changed;
char *password = NULL;
servlist_update_from_entry (&net->nick, edit_entry_nick);
@@ -1286,23 +1269,8 @@ servlist_edit_update (ircnet *net)
servlist_update_from_entry (&net->real, edit_entry_real);
if (net && net->name)
{
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));
use_keyring = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_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)
return;
if (edit_pass_changed)
@@ -1370,7 +1338,6 @@ servlist_edit_close_cb (GtkWidget *button, gpointer userdata)
edit_win = NULL;
edit_entry_pass = NULL;
edit_check_show_pass = NULL;
edit_check_ask_pass = NULL;
edit_button_encrypt_pass = NULL;
edit_button_import_pass = NULL;
}
@@ -2598,7 +2565,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
/* Checkboxes and entries */
table3 = gtkutil_grid_new (19, 2, FALSE);
table3 = gtkutil_grid_new (17, 2, FALSE);
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
@@ -2617,43 +2584,34 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
#endif
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"));
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, 7, 8,
servlist_table_attach (table3, edit_check_use_keyring, 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_use_keyring), "toggled",
G_CALLBACK (servlist_toggle_keyring_cb), NULL);
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:"), 9, net->nick2, &edit_label_nick2, 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:"), 11, net->user, &edit_label_user, 0);
edit_entry_nick = servlist_create_entry (table3, _("_Nick name:"), 7, net->nick, &edit_label_nick, 0);
edit_entry_nick2 = servlist_create_entry (table3, _("Second choice:"), 8, 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_user = servlist_create_entry (table3, _("_User name:"), 10, net->user, &edit_label_user, 0);
label_logintype = gtk_label_new (_("Login method:"));
servlist_table_attach (table3, label_logintype, 0, 1, 12, 13,
servlist_table_attach (table3, label_logintype, 0, 1, 11, 12,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label_logintype, GTK_ALIGN_START);
gtk_widget_set_valign (label_logintype, GTK_ALIGN_CENTER);
combobox_logintypes = servlist_create_logintypecombo (notebook);
servlist_table_attach (table3, combobox_logintypes, 1, 2, 12, 13,
servlist_table_attach (table3, combobox_logintypes, 1, 2, 11, 12,
FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
4, 2);
edit_entry_pass = servlist_create_entry (table3, _("Password:"), 13, NULL, 0, _("Password used for login. If in doubt, leave blank."));
edit_entry_pass = servlist_create_entry (table3, _("Password:"), 12, NULL, 0, _("Password used for login. If in doubt, leave blank."));
if (edit_loaded_password)
{
memset (edit_loaded_password, 0, strlen (edit_loaded_password));
@@ -2686,7 +2644,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
if (selected_net && selected_net->logintype == LOGIN_SASLEXTERNAL)
gtk_widget_set_sensitive (edit_entry_pass, FALSE);
edit_check_show_pass = gtk_check_button_new_with_mnemonic (_("Show password"));
servlist_table_attach (table3, edit_check_show_pass, 0, 2, 14, 15,
servlist_table_attach (table3, edit_check_show_pass, 0, 2, 13, 14,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
4, 2);
@@ -2694,14 +2652,14 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
G_CALLBACK (servlist_toggle_show_password_cb), edit_entry_pass);
edit_button_encrypt_pass = gtk_button_new_with_mnemonic (_("Encrypt saved password"));
servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 15, 16,
servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 14, 15,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_button_encrypt_pass), "clicked",
G_CALLBACK (servlist_encrypt_password_cb), net);
edit_button_import_pass = gtk_button_new_with_mnemonic (_("Move password to keyring"));
servlist_table_attach (table3, edit_button_import_pass, 1, 2, 15, 16,
servlist_table_attach (table3, edit_button_import_pass, 1, 2, 14, 15,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
4, 2);
@@ -2709,14 +2667,14 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
G_CALLBACK (servlist_import_password_cb), net);
label34 = gtk_label_new (_("Character set:"));
servlist_table_attach (table3, label34, 0, 1, 16, 17,
servlist_table_attach (table3, label34, 0, 1, 15, 16,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label34, GTK_ALIGN_START);
gtk_widget_set_valign (label34, GTK_ALIGN_CENTER);
comboboxentry_charset = servlist_create_charsetcombo ();
servlist_table_attach (table3, comboboxentry_charset, 1, 2, 16, 17,
servlist_table_attach (table3, comboboxentry_charset, 1, 2, 15, 16,
FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
4, 2);
@@ -2742,7 +2700,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *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);
servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 17, 18,
servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 16, 17,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);

View File

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

View File

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

View File

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

View File

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

View File

@@ -309,7 +309,7 @@ theme_gtk3_is_active (void)
#include "../theme-preferences.c"
static void
test_removed_selected_theme_commits_fallback_and_applies (void)
test_removed_selected_theme_falls_back_to_system_default (void)
{
GtkWidget *page;
theme_preferences_ui *ui;
@@ -326,23 +326,26 @@ test_removed_selected_theme_commits_fallback_and_applies (void)
g_strlcpy (prefs.hex_gui_gtk3_theme, "removed-theme", sizeof (prefs.hex_gui_gtk3_theme));
prefs.hex_gui_gtk3_variant = THEME_GTK3_VARIANT_PREFER_DARK;
removed_selected = FALSE;
apply_current_calls = 0;
applied_theme_id[0] = '\0';
page = theme_preferences_create_page (NULL, &setup_prefs, NULL);
ui = g_object_get_data (G_OBJECT (page), "theme-preferences-ui");
g_assert_nonnull (ui);
apply_current_calls = 0;
applied_theme_id[0] = '\0';
g_assert_nonnull (ui->gtk3_remove);
gtk_button_clicked (GTK_BUTTON (ui->gtk3_remove));
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "fallback-theme");
g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
/* Removing the active in-app theme returns to the system GTK3
* theme ("None") instead of forcing another theme. */
g_assert_cmpstr (prefs.hex_gui_gtk3_theme, ==, "");
g_assert_cmpstr (setup_prefs.hex_gui_gtk3_theme, ==, "");
g_assert_cmpint (prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
g_assert_cmpint (setup_prefs.hex_gui_gtk3_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
g_assert_cmpint (apply_current_calls, ==, 1);
g_assert_cmpstr (applied_theme_id, ==, "fallback-theme");
g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_PREFER_LIGHT);
g_assert_cmpstr (applied_theme_id, ==, "");
g_assert_cmpint (applied_variant, ==, THEME_GTK3_VARIANT_FOLLOW_SYSTEM);
gtk_widget_destroy (page);
}
@@ -421,8 +424,8 @@ main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
gtk_available = gtk_init_check (&argc, &argv);
g_test_add_func ("/theme/preferences/gtk3_removed_selection_applies_fallback",
test_removed_selected_theme_commits_fallback_and_applies);
g_test_add_func ("/theme/preferences/gtk3_removed_selection_falls_back_to_system",
test_removed_selected_theme_falls_back_to_system_default);
g_test_add_func ("/theme/preferences/gtk3_unset_keeps_system_default",
test_unset_theme_keeps_system_default_without_apply);
g_test_add_func ("/theme/preferences/gtk3_select_none_resets_theme",

View File

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

View File

@@ -21,7 +21,6 @@
#include "theme-access.h"
#include "theme-runtime.h"
#include "theme-gtk3.h"
@@ -32,7 +31,8 @@ enum
THEME_XTEXT_MARK_BG_INDEX = 100,
THEME_XTEXT_FG_INDEX = 101,
THEME_XTEXT_BG_INDEX = 102,
THEME_XTEXT_MARKER_INDEX = 103
THEME_XTEXT_MARKER_INDEX = 103,
THEME_XTEXT_MARKER_LEGACY_INDEX = 36
};
static const guint8 theme_default_99_mirc_colors[THEME_XTEXT_MIRC_COLS][3] = {
@@ -111,14 +111,49 @@ theme_access_context_get_background_color (GtkStyleContext *context, GtkStateFla
gtk_style_context_restore (context);
}
static gboolean
theme_access_lookup_named_color (GtkStyleContext *context, const char *const *names, GdkRGBA *out_color)
{
size_t i;
for (i = 0; names[i] != NULL; i++)
{
if (gtk_style_context_lookup_color (context, names[i], out_color))
return TRUE;
}
return FALSE;
}
static void
theme_access_resolve_map_background (GtkStyleContext *context, GtkStateFlags state,
const char *const *named_fallbacks, GdkRGBA *out_color)
{
GdkRGBA named;
theme_access_context_get_background_color (context, state, out_color);
if (out_color->alpha > 0.0)
return;
/* Unstyled CSS nodes (e.g. the custom chat text widget) report a fully
* transparent background; fall back to the theme's named colors so the
* mapped palette still follows the active GTK3 theme. */
if (theme_access_lookup_named_color (context, named_fallbacks, &named))
*out_color = named;
}
static gboolean
theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map)
{
static const char *const base_fallbacks[] = { "theme_base_color", "theme_bg_color", NULL };
static const char *const selected_bg_fallbacks[] = { "theme_selected_bg_color", NULL };
static const char *const selected_fg_fallbacks[] = { "theme_selected_fg_color", NULL };
GtkStyleContext *context;
GdkRGBA named;
GdkRGBA accent;
g_return_val_if_fail (out_map != NULL, FALSE);
if (!theme_gtk3_is_active () || widget == NULL || !GTK_IS_WIDGET (widget))
if (widget == NULL || !GTK_IS_WIDGET (widget))
return FALSE;
context = gtk_widget_get_style_context (widget);
@@ -126,9 +161,14 @@ theme_access_get_gtk_palette_map (GtkWidget *widget, ThemeGtkPaletteMap *out_map
return FALSE;
theme_access_context_get_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_foreground);
theme_access_context_get_background_color (context, GTK_STATE_FLAG_NORMAL, &out_map->text_background);
theme_access_resolve_map_background (context, GTK_STATE_FLAG_NORMAL, base_fallbacks,
&out_map->text_background);
theme_access_context_get_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_foreground);
theme_access_context_get_background_color (context, GTK_STATE_FLAG_SELECTED, &out_map->selection_background);
theme_access_resolve_map_background (context, GTK_STATE_FLAG_SELECTED, selected_bg_fallbacks,
&out_map->selection_background);
if (gdk_rgba_equal (&out_map->selection_foreground, &out_map->text_foreground) &&
theme_access_lookup_named_color (context, selected_fg_fallbacks, &named))
out_map->selection_foreground = named;
theme_access_context_get_color (context, GTK_STATE_FLAG_LINK, &accent);
if (accent.alpha <= 0.0)
accent = out_map->selection_background;
@@ -223,14 +263,35 @@ theme_get_xtext_colors (XTextColor *palette, size_t palette_len)
void
theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_t palette_len)
{
ThemeGtkPaletteMap gtk_map = { 0 };
ThemeWidgetStyleValues style_values;
GdkRGBA marker_color;
gboolean have_marker = FALSE;
if (!palette)
return;
theme_get_widget_style_values_for_widget (widget, &style_values);
if (theme_access_get_gtk_palette_map (widget, &gtk_map))
{
theme_runtime_get_widget_style_values_mapped (&gtk_map, &style_values);
theme_runtime_get_xtext_colors_mapped (&gtk_map, palette, palette_len);
}
else
{
theme_runtime_get_widget_style_values (&style_values);
theme_runtime_get_xtext_colors (palette, palette_len);
}
if (palette_len > THEME_XTEXT_MARKER_LEGACY_INDEX)
{
/* The marker token lives at its legacy slot before the extended
* mIRC palette overwrites it; keep the (possibly theme-mapped)
* value for the dedicated marker index below. */
marker_color.red = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].red;
marker_color.green = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].green;
marker_color.blue = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].blue;
marker_color.alpha = palette[THEME_XTEXT_MARKER_LEGACY_INDEX].alpha;
have_marker = TRUE;
}
if (palette_len >= THEME_XTEXT_MIRC_COLS)
theme_access_apply_default_99_palette (palette, palette_len);
if (palette_len > THEME_XTEXT_MARK_FG_INDEX)
@@ -249,7 +310,7 @@ theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *palette, size_
}
if (palette_len > THEME_XTEXT_MARKER_INDEX)
{
if (!theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color))
if (!have_marker && !theme_runtime_get_color (THEME_TOKEN_MARKER, &marker_color))
marker_color = style_values.selection_background;
palette[THEME_XTEXT_MARKER_INDEX].red = marker_color.red;
palette[THEME_XTEXT_MARKER_INDEX].green = marker_color.green;

View File

@@ -25,6 +25,7 @@
#include "theme-css.h"
#include "theme-runtime.h"
#include "theme-gtk3.h"
#include "theme-policy.h"
#include "../maingui.h"
#include <gtk/gtk.h>
@@ -59,6 +60,16 @@ theme_application_apply_toplevel_theme (gboolean dark)
prefer_dark = TRUE;
else if (prefs.hex_gui_gtk3_variant == THEME_GTK3_VARIANT_PREFER_LIGHT)
prefer_dark = FALSE;
else
prefer_dark = theme_policy_system_prefers_dark ();
}
else
{
/* Following the system GTK3 theme: only request the dark
* variant when the dark-mode preference (or the system, in
* auto mode) asks for it, instead of clobbering the value
* that came from the user's GTK configuration. */
prefer_dark = theme_policy_is_dark_mode_active (prefs.hex_gui_dark_mode);
}
if (settings && g_object_class_find_property (G_OBJECT_GET_CLASS (settings),

View File

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

View File

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

View File

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

View File

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

View File

@@ -101,6 +101,9 @@ typedef struct
gboolean changed;
gboolean snapshot_valid[THEME_TOKEN_COUNT];
gboolean staged_valid[THEME_TOKEN_COUNT];
/* Only explicitly edited tokens are written back to the runtime
* palette; untouched tokens keep following the GTK3 theme. */
gboolean staged_dirty[THEME_TOKEN_COUNT];
GdkRGBA snapshot[THEME_TOKEN_COUNT];
GdkRGBA staged[THEME_TOKEN_COUNT];
} theme_preferences_stage_state;
@@ -147,7 +150,8 @@ theme_preferences_stage_sync_runtime_to_snapshot (void)
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{
if (theme_preferences_stage.snapshot_valid[token])
if (theme_preferences_stage.snapshot_valid[token] &&
theme_preferences_stage.staged_dirty[token])
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token,
&theme_preferences_stage.snapshot[token], NULL);
}
@@ -160,7 +164,8 @@ theme_preferences_stage_sync_runtime_to_staged (void)
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{
if (theme_preferences_stage.staged_valid[token])
if (theme_preferences_stage.staged_valid[token] &&
theme_preferences_stage.staged_dirty[token])
theme_manager_set_token_color (ZOITECHAT_DARK_MODE_LIGHT, token,
&theme_preferences_stage.staged[token], NULL);
}
@@ -179,6 +184,7 @@ theme_preferences_staged_set_color (ThemeSemanticToken token, const GdkRGBA *rgb
{
theme_preferences_stage.staged[token] = *rgba;
theme_preferences_stage.staged_valid[token] = TRUE;
theme_preferences_stage.staged_dirty[token] = TRUE;
theme_preferences_stage_recompute_changed ();
if (color_change_flag)
*color_change_flag = theme_preferences_stage.changed;
@@ -841,6 +847,10 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i
ThemeSemanticToken token;
ThemeWidgetStyleValues style_values;
/* The runtime palette was just reset; drop any
* pending edits so the reset is not re-pinned. */
memset (theme_preferences_stage.staged_dirty, 0,
sizeof (theme_preferences_stage.staged_dirty));
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{
GdkRGBA rgba;
@@ -855,7 +865,6 @@ theme_preferences_manager_dialog_response_cb (GtkDialog *dialog, gint response_i
theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_FOREGROUND] = TRUE;
theme_preferences_stage.staged[THEME_TOKEN_TEXT_BACKGROUND] = style_values.background;
theme_preferences_stage.staged_valid[THEME_TOKEN_TEXT_BACKGROUND] = TRUE;
theme_preferences_stage_sync_runtime_to_staged ();
theme_preferences_stage_recompute_changed ();
if (ui->color_change_flag)
*ui->color_change_flag = theme_preferences_stage.changed;
@@ -1534,11 +1543,31 @@ theme_preferences_gtk3_sync_remove_state (theme_preferences_ui *ui)
gtk_widget_set_sensitive (ui->gtk3_remove, source == ZOITECHAT_GTK3_THEME_SOURCE_USER);
}
static void
theme_preferences_stage_rebase_color (ThemeSemanticToken token, const GdkRGBA *rgba)
{
if (token < 0 || token >= THEME_TOKEN_COUNT || !rgba)
return;
if (!theme_preferences_stage.active)
return;
/* The theme changed underneath the dialog: shift the baseline so the
* new theme-provided value is neither treated as a user edit nor
* pinned into the runtime palette. */
theme_preferences_stage.snapshot[token] = *rgba;
theme_preferences_stage.staged[token] = *rgba;
theme_preferences_stage.snapshot_valid[token] = TRUE;
theme_preferences_stage.staged_valid[token] = TRUE;
theme_preferences_stage.staged_dirty[token] = FALSE;
theme_preferences_stage_recompute_changed ();
}
static void
theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
{
ThemeWidgetStyleValues style_values;
GtkWidget *style_source = NULL;
gboolean follows_system_theme = prefs.hex_gui_gtk3_theme[0] == '\0';
if (ui && ui->parent)
style_source = GTK_WIDGET (ui->parent);
@@ -1549,6 +1578,18 @@ theme_preferences_gtk3_sync_runtime_palette (theme_preferences_ui *ui)
theme_get_widget_style_values_for_widget (style_source, &style_values);
if (follows_system_theme)
{
/* Following the system GTK3 theme: leave the mapped tokens
* unpinned so they keep tracking the theme, and only refresh
* what the dialog shows. */
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_SELECTION_FOREGROUND, &style_values.selection_foreground);
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,

View File

@@ -857,10 +857,6 @@ 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)
{
}

View File

@@ -1,280 +0,0 @@
#!/usr/bin/env python3
#
# gen-emoji-data.py - generate ZoiteChat's bundled emoji catalog
#
# Consumes the pinned data files checked in under data/emoji/ and emits a
# C table that is compiled into the application, so every ZoiteChat package
# ships the same emoji, names, categories and search keywords regardless of
# which GTK runtime or /usr/share/gtk-3.0/emoji data a distribution provides.
#
# Inputs:
# emoji-test.txt Unicode emoji-test data file (catalog,
# CLDR ordering, groups, short names)
# cldr-annotations-en.xml CLDR annotations (search keywords)
# cldr-annotations-derived-en.xml CLDR derived annotations (sequences)
# gemoji.json gemoji database (:shortcode: aliases, tags)
#
# Usage:
# gen-emoji-data.py EMOJI_TEST ANNOTATIONS DERIVED_ANNOTATIONS GEMOJI \
# VERSIONS OUTPUT
#
# Only fully-qualified sequences are emitted, so everything the picker
# inserts is a well-formed emoji presentation sequence. Skin-tone variants
# are folded into per-base tone sets: the picker shows the neutral emoji and
# swaps in the modified sequence for the user's preferred tone. Sequences
# that mix two different skin tones are not emitted.
import json
import re
import sys
import xml.etree.ElementTree as ET
# Must match the EmojiGroup enum in src/common/emoji-data.h. "Component"
# entries (bare tone swatches, hair styles) are not meaningful to insert.
GROUPS = [
"Smileys & Emotion",
"People & Body",
"Animals & Nature",
"Food & Drink",
"Travel & Places",
"Activities",
"Objects",
"Symbols",
"Flags",
]
SKIPPED_GROUPS = {"Component"}
# Order must match the EmojiTone enum (minus EMOJI_TONE_NONE).
TONE_DESCRIPTORS = [
"light skin tone",
"medium-light skin tone",
"medium skin tone",
"medium-dark skin tone",
"dark skin tone",
]
TONE_MODIFIERS = set(range(0x1F3FB, 0x1F3FF + 1))
VS16 = 0xFE0F
DATA_LINE = re.compile(
r"^(?P<cps>[0-9A-F ]+?)\s*;\s*(?P<status>[\w-]+)\s*#\s*(?P<emoji>\S+)"
r"\s+E\d+\.\d+\s+(?P<name>.+)$"
)
def parse_emoji_test(path):
"""Yield (group, codepoints, string, name) for fully-qualified entries."""
group = None
unicode_version = None
with open(path, encoding="utf-8") as fp:
for line in fp:
line = line.rstrip("\n")
if line.startswith("# group:"):
group = line.split(":", 1)[1].strip()
continue
if line.startswith("# Version:"):
unicode_version = line.split(":", 1)[1].strip()
continue
if not line or line.startswith("#"):
continue
m = DATA_LINE.match(line)
if not m:
continue
if m.group("status") != "fully-qualified":
continue
if group in SKIPPED_GROUPS:
continue
if group not in GROUPS:
sys.exit("gen-emoji-data.py: unknown emoji group %r; "
"update GROUPS and the EmojiGroup enum" % group)
cps = [int(cp, 16) for cp in m.group("cps").split()]
yield group, cps, "".join(map(chr, cps)), m.group("name")
if unicode_version is None:
sys.exit("gen-emoji-data.py: no '# Version:' header in emoji-test.txt")
parse_emoji_test.version = unicode_version
def parse_annotations(path, keywords, names):
root = ET.parse(path).getroot()
for node in root.iter("annotation"):
cp = node.attrib["cp"]
if node.attrib.get("type") == "tts":
names[cp] = node.text.strip()
else:
keywords[cp] = [k.strip() for k in node.text.split("|")]
def parse_versions(path):
versions = {}
with open(path, encoding="utf-8") as fp:
for line in fp:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
versions[key.strip()] = value.strip()
for key in ("unicode-emoji", "cldr", "gemoji"):
if key not in versions:
sys.exit("gen-emoji-data.py: %r missing from %s" % (key, path))
return versions
def strip_vs16(text):
return text.replace(chr(VS16), "")
def split_name(name):
"""Split 'kiss: woman, man, light skin tone' into a base name and the
list of tone descriptors it contains."""
if ": " not in name:
return name, []
head, qualifier = name.split(": ", 1)
parts = [p.strip() for p in qualifier.split(",")]
tones = [p for p in parts if p in TONE_DESCRIPTORS]
rest = [p for p in parts if p not in TONE_DESCRIPTORS]
base = head if not rest else head + ": " + ", ".join(rest)
return base, tones
def c_escape(text):
"""Escape text as the body of a C string literal. Non-ASCII bytes use
three-digit octal escapes, which cannot swallow a following character
the way hex escapes can."""
out = []
for byte in text.encode("utf-8"):
if byte in (0x22, 0x5C): # '"' and '\\'
out.append("\\" + chr(byte))
elif 0x20 <= byte < 0x7F:
out.append(chr(byte))
else:
out.append("\\%03o" % byte)
return "".join(out)
def build_search(name, keyword_list, aliases, tags):
tokens = []
for source in ([name] + keyword_list + tags):
tokens.extend(source.casefold().split())
tokens.extend(alias.casefold() for alias in aliases)
seen = {}
for token in tokens:
token = token.strip('",')
if token and token not in seen:
seen[token] = True
return " ".join(seen)
def main():
if len(sys.argv) != 7:
sys.exit("usage: gen-emoji-data.py EMOJI_TEST ANNOTATIONS "
"DERIVED_ANNOTATIONS GEMOJI VERSIONS OUTPUT")
(emoji_test, ann_path, derived_path, gemoji_path,
versions_path, output) = sys.argv[1:]
versions = parse_versions(versions_path)
cldr_version = versions["cldr"]
keywords = {}
cldr_names = {}
parse_annotations(ann_path, keywords, cldr_names)
parse_annotations(derived_path, keywords, cldr_names)
aliases = {}
tags = {}
with open(gemoji_path, encoding="utf-8") as fp:
for item in json.load(fp):
if "emoji" not in item:
continue
key = strip_vs16(item["emoji"])
aliases[key] = item.get("aliases", [])
tags[key] = item.get("tags", [])
entries = [] # base emoji, in CLDR order
by_name = {} # base name -> index into entries
tone_sets = [] # list of [seq or None] * 5
skipped_mixed = 0
for group, cps, string, name in parse_emoji_test(emoji_test):
tones_in_seq = {cp for cp in cps if cp in TONE_MODIFIERS}
base_name, tone_descs = split_name(name)
if not tones_in_seq:
key = strip_vs16(string)
entry = {
"sequence": string,
"name": name,
"keywords": keywords.get(key, keywords.get(string, [])),
"aliases": aliases.get(key, []),
"tags": tags.get(key, []),
"group": GROUPS.index(group),
"tone_set": -1,
}
by_name[name] = len(entries)
entries.append(entry)
continue
# Tone-modified sequence: attach to its base if it uses one uniform
# tone; sequences mixing two tones are intentionally not bundled.
if len(tones_in_seq) != 1 or len(tone_descs) != 1:
skipped_mixed += 1
continue
if base_name not in by_name:
sys.exit("gen-emoji-data.py: no base emoji %r for %r"
% (base_name, name))
entry = entries[by_name[base_name]]
if entry["tone_set"] == -1:
entry["tone_set"] = len(tone_sets)
tone_sets.append([None] * len(TONE_DESCRIPTORS))
tone_sets[entry["tone_set"]][TONE_DESCRIPTORS.index(tone_descs[0])] = string
for i, tone_set in enumerate(tone_sets):
if None in tone_set:
sys.exit("gen-emoji-data.py: incomplete tone set %d" % i)
unicode_version = parse_emoji_test.version
if unicode_version != versions["unicode-emoji"]:
sys.exit("gen-emoji-data.py: emoji-test.txt is Emoji %s but VERSIONS "
"pins %s" % (unicode_version, versions["unicode-emoji"]))
with open(output, "w", encoding="utf-8", newline="\n") as out:
out.write("/*\n"
" * Generated by tools/gen-emoji-data.py -- do not edit.\n"
" *\n"
" * Sources (pinned copies in data/emoji/):\n"
" * Unicode emoji-test.txt, Emoji %s\n"
" * CLDR annotations (en), release %s\n"
" * gemoji %s shortcode aliases\n"
" */\n\n" % (unicode_version, cldr_version,
versions["gemoji"]))
out.write("static const EmojiToneSet emoji_tone_sets_table[] =\n{\n")
for tone_set in tone_sets:
out.write("\t{ { %s } },\n"
% ", ".join('"%s"' % c_escape(seq) for seq in tone_set))
out.write("};\n\n")
out.write("static const EmojiEntry emoji_entries_table[] =\n{\n")
for entry in entries:
search = build_search(entry["name"], entry["keywords"],
entry["aliases"], entry["tags"])
out.write('\t{ "%s", "%s", "%s", "%s", %d, %d },\n' % (
c_escape(entry["sequence"]),
c_escape(entry["name"]),
c_escape(search),
c_escape(" ".join(entry["aliases"])),
entry["group"],
entry["tone_set"],
))
out.write("};\n\n")
out.write('static const char emoji_data_unicode_version_str[] = "%s";\n'
% c_escape(unicode_version))
out.write('static const char emoji_data_cldr_version_str[] = "%s";\n'
% c_escape(cldr_version))
sys.stderr.write("gen-emoji-data.py: %d emoji, %d tone sets, "
"%d mixed-tone sequences skipped (Emoji %s, CLDR %s)\n"
% (len(entries), len(tone_sets), skipped_mixed,
unicode_version, cldr_version))
if __name__ == "__main__":
main()

View File

@@ -82,8 +82,6 @@
<GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" />
<FontConfig Include="$(DepsRoot)\etc\fonts\*" />
<EmojiFont Include="..\..\data\fonts\NotoColorEmoji.ttf" />
<EmojiFontLicense Include="..\..\data\fonts\OFL.txt" />
<Share Include="share\**\*" />
<Locale Include="$(ZoiteChatBin)locale\**\*;$(DepsRoot)\share\locale\**\*" />
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
@@ -93,8 +91,6 @@
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
<Copy SourceFiles="@(FontConfig)" DestinationFolder="$(ZoiteChatRel)\etc\fonts" />
<Copy SourceFiles="@(EmojiFont)" DestinationFolder="$(ZoiteChatRel)\share\fonts\zoitechat" />
<Copy SourceFiles="@(EmojiFontLicense)" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat\fonts" />
<Copy SourceFiles="@(Gtk3Immodules)" DestinationFiles="@(Gtk3Immodules->'$(ZoiteChatRel)\lib\gtk-3.0\3.0.0\immodules\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Gtk3PrintBackends)" DestinationFiles="@(Gtk3PrintBackends->'$(ZoiteChatRel)\lib\gtk-3.0\3.0.0\printbackends\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GdkPixbufLoaders)" DestinationFiles="@(GdkPixbufLoaders->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />

View File

@@ -104,7 +104,6 @@ Source: "readme.url"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "cert.pem"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "share\xml\*"; DestDir: "{app}\share\xml"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
Source: "share\doc\zoitechat\*"; DestDir: "{app}\share\doc\zoitechat"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
Source: "share\fonts\zoitechat\*"; DestDir: "{app}\share\fonts\zoitechat"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
Source: "share\doc\WinSparkle\*"; DestDir: "{app}\share\doc\WinSparkle"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
Source: "share\themes\MS-Windows\*"; DestDir: "{app}\share\themes\MS-Windows"; Flags: ignoreversion createallsubdirs recursesubdirs skipifsourcedoesntexist; Components: libs
Source: "share\glib-2.0\schemas\*"; DestDir: "{app}\share\glib-2.0\schemas"; Flags: ignoreversion createallsubdirs recursesubdirs skipifsourcedoesntexist; Components: libs