2 Commits

44 changed files with 40016 additions and 2477 deletions

View File

@@ -36,7 +36,7 @@ jobs:
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \ libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \ 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 \ perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
fonts-noto-color-emoji breeze-gtk-theme \ breeze-gtk-theme \
patchelf file curl patchelf file curl
- name: Configure - name: Configure
@@ -119,15 +119,9 @@ jobs:
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/ cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
fi fi
if [ -d "/usr/share/gtk-3.0/emoji" ]; then # The emoji font is not bundled on Linux; the AppImage uses the
install -d AppDir/usr/share/gtk-3.0 # host system's emoji font (e.g. fonts-noto-color-emoji).
cp -a /usr/share/gtk-3.0/emoji AppDir/usr/share/gtk-3.0/ test ! -e AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
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 if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then
install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0 install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0
@@ -143,7 +137,6 @@ jobs:
run: | run: |
set -eux 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 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 for plugin in checksum fishlim lua perl python sysinfo; do
@@ -192,14 +185,6 @@ jobs:
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}" export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
fi fi
if [ -d "$APPDIR/etc/fonts" ]; then
export FONTCONFIG_SYSROOT="$APPDIR"
export FONTCONFIG_PATH="$APPDIR/etc/fonts${FONTCONFIG_PATH:+:$FONTCONFIG_PATH}"
if [ -f "$APPDIR/etc/fonts/fonts.conf" ]; then
export FONTCONFIG_FILE="$APPDIR/etc/fonts/fonts.conf"
fi
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
@@ -258,13 +243,10 @@ jobs:
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}" export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
fi fi
# OpenSSL trust store override
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}" 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 SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
export CURL_CA_BUNDLE="${CURL_CA_BUNDLE:-$SSL_CERT_FILE}" 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 [ -z "${GDK_BACKEND:-}" ]; then
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
export GDK_BACKEND="wayland,x11" export GDK_BACKEND="wayland,x11"

23
COPYING
View File

@@ -336,3 +336,26 @@ proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the 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 library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. 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

@@ -0,0 +1,39 @@
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.

30
data/emoji/README.md Normal file
View File

@@ -0,0 +1,30 @@
# 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.

6
data/emoji/VERSIONS Normal file
View File

@@ -0,0 +1,6 @@
# 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

5518
data/emoji/emoji-test.txt Normal file

File diff suppressed because it is too large Load Diff

23479
data/emoji/gemoji.json Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

91
data/fonts/OFL.txt Normal file
View File

@@ -0,0 +1,91 @@
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.

23
data/fonts/README.md Normal file
View File

@@ -0,0 +1,23 @@
# 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.
The font is bundled only with Windows builds, where it is installed
inside the application and registered only for the ZoiteChat process.
It is not installed as a global operating-system font.
Linux (and other Unix) builds do not install this file. They rely on
the emoji font provided by the operating system instead — typically the
distribution's Noto Color Emoji package (`fonts-noto-color-emoji` on
Debian/Ubuntu, `google-noto-color-emoji-fonts` on Fedora).
Upstream release: https://github.com/googlefonts/noto-emoji/releases/tag/v2.051
Vendored file SHA-256:
`dac5c27651082d6c53dab5081f50d2022ddf6877d730fa919cf6a4fc2af22de0`
The font is distributed under the SIL Open Font License 1.1. See
`OFL.txt`.

13
data/fonts/meson.build Normal file
View File

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

View File

@@ -65,7 +65,8 @@
"/share/man" "/share/man"
], ],
"post-install": [ "post-install": [
"install -d /app/extensions" "install -d /app/extensions",
"test ! -e /app/share/fonts/zoitechat/NotoColorEmoji.ttf"
], ],
"sources": [ "sources": [
{ {

View File

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

View File

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

View File

@@ -40,18 +40,18 @@
#define DEF_FONT "Monospace 9" #define DEF_FONT "Monospace 9"
#ifdef WIN32 #ifdef WIN32
#define DEF_FONT_ALTER "Segoe UI Emoji,Arial Unicode MS,Lucida Sans Unicode,Meiryo,Symbola,Unifont" #define DEF_FONT_ALTER "Segoe UI Emoji,Segoe UI Symbol,Arial Unicode MS,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
#else #else
#define DEF_FONT_ALTER "Arial Unicode MS,Segoe UI Emoji,Lucida Sans Unicode,Meiryo,Symbola,Unifont" #define DEF_FONT_ALTER "Arial Unicode MS,Segoe UI Emoji,Lucida Sans Unicode,Meiryo,Symbola,Unifont"
#endif #endif
const char * const languages[LANGUAGES_LENGTH] = { const char * const languages[LANGUAGES_LENGTH] = {
"af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN", /* 0 .. 9 */ "af", "sq", "am", "ast", "az", "eu", "be", "bg", "ca", "zh_CN",
"zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl", /* 10 .. 19 */ "zh_TW", "cs", "da", "nl", "en_GB", "en", "et", "fi", "fr", "gl",
"de", "el", "gu", "hi", "hu", "id", "it", "ja_JP", "kn", "rw", /* 20 .. 29 */ "de", "el", "gu", "hi", "hu", "id", "it", "ja_JP", "kn", "rw",
"ko", "lv", "lt", "mk", "ml", "ms", "nb", "no", "pl", "pt", /* 30 .. 39 */ "ko", "lv", "lt", "mk", "ml", "ms", "nb", "no", "pl", "pt",
"pt_BR", "pa", "ru", "sr", "sk", "sl", "es", "sv", "th", "tr", /* 40 .. 49 */ "pt_BR", "pa", "ru", "sr", "sk", "sl", "es", "sv", "th", "tr",
"uk", "vi", "wa" /* 50 .. */ "uk", "vi", "wa"
}; };
void void
@@ -78,8 +78,6 @@ list_addentry (GSList ** list, char *cmd, char *name)
*list = g_slist_append (*list, pop); *list = g_slist_append (*list, pop);
} }
/* read it in from a buffer to our linked list */
static void static void
list_load_from_data (GSList ** list, char *ibuf, int size) list_load_from_data (GSList ** list, char *ibuf, int size)
{ {
@@ -181,9 +179,9 @@ list_delentry (GSList ** list, char *name)
char * char *
cfg_get_str (char *cfg, const char *var, char *dest, int dest_len) cfg_get_str (char *cfg, const char *var, char *dest, int dest_len)
{ {
char buffer[128]; /* should be plenty for a variable name */ char buffer[128];
sprintf (buffer, "%s ", var); /* add one space, this way it works against var - var2 checks too */ sprintf (buffer, "%s ", var);
while (1) while (1)
{ {
@@ -197,8 +195,6 @@ cfg_get_str (char *cfg, const char *var, char *dest, int dest_len)
cfg++; cfg++;
while (*cfg == ' ') while (*cfg == ' ')
cfg++; cfg++;
/*while (*cfg == ' ' || *cfg == '=')
cfg++; */
value = cfg; value = cfg;
while (*cfg != 0 && *cfg != '\n') while (*cfg != 0 && *cfg != '\n')
cfg++; cfg++;
@@ -293,7 +289,7 @@ cfg_get_int (char *cfg, char *var)
return atoi (str); return atoi (str);
} }
char *xdir = NULL; /* utf-8 encoding */ char *xdir = NULL;
#ifdef WIN32 #ifdef WIN32
#include <windows.h> #include <windows.h>
@@ -416,6 +412,7 @@ const struct prefs vars[] =
{"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT}, {"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT},
{"gui_dialog_top", P_OFFINT (hex_gui_dialog_top), 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_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_filesize_iec", P_OFFINT (hex_gui_filesize_iec), TYPE_BOOL},
{"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL}, {"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL},
{"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL}, {"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL},
@@ -435,7 +432,6 @@ const struct prefs vars[] =
{"gui_pane_right_size_min", P_OFFINT (hex_gui_pane_right_size_min), TYPE_INT}, {"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_quit_dialog", P_OFFINT (hex_gui_quit_dialog), TYPE_BOOL},
{"gui_search_pos", P_OFFINT (hex_gui_search_pos), TYPE_INT}, {"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_fav", P_OFFINT (hex_gui_slist_fav), TYPE_BOOL},
{"gui_slist_select", P_OFFINT (hex_gui_slist_select), TYPE_INT}, {"gui_slist_select", P_OFFINT (hex_gui_slist_select), TYPE_INT},
{"gui_slist_skip", P_OFFINT (hex_gui_slist_skip), TYPE_BOOL}, {"gui_slist_skip", P_OFFINT (hex_gui_slist_skip), TYPE_BOOL},
@@ -564,7 +560,6 @@ const struct prefs vars[] =
{"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT}, {"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT},
{"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR}, {"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR},
{"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT}, {"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT},
{"net_remote_media", P_OFFINT (hex_net_remote_media), TYPE_BOOL},
{"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL}, {"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL},
{"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT}, {"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT},
@@ -777,8 +772,6 @@ load_default_config(void)
prefs.hex_gui_hide_menu = 1; prefs.hex_gui_hide_menu = 1;
#endif #endif
prefs.hex_gui_ctrlq_quit = 1; prefs.hex_gui_ctrlq_quit = 1;
/* hex_net_remote_media stays 0: loading remote media can reveal the
user's IP address, so it is strictly opt-in */
prefs.hex_gui_input_attr = 1; prefs.hex_gui_input_attr = 1;
prefs.hex_gui_input_icon = 1; prefs.hex_gui_input_icon = 1;
prefs.hex_gui_input_nick = 1; prefs.hex_gui_input_nick = 1;

View File

@@ -55,6 +55,7 @@
<ClCompile Include="chanopt.c" /> <ClCompile Include="chanopt.c" />
<ClCompile Include="ctcp.c" /> <ClCompile Include="ctcp.c" />
<ClCompile Include="dcc.c" /> <ClCompile Include="dcc.c" />
<ClCompile Include="emoji-data.c" />
<ClCompile Include="history.c" /> <ClCompile Include="history.c" />
<ClCompile Include="plugin-identd.c" /> <ClCompile Include="plugin-identd.c" />
<ClCompile Include="ignore.c" /> <ClCompile Include="ignore.c" />
@@ -113,6 +114,7 @@
SET SOLUTIONDIR=$(SolutionDir)..\ SET SOLUTIONDIR=$(SolutionDir)..\
"$(Python3Path)\python.exe" $(ProjectDir)make-te.py "$(ProjectDir)textevents.in" "$(ZoiteChatLib)textevents.h" "$(ZoiteChatLib)textenums.h" "$(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" $(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" 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 --header "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.h"
$(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c" $(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c"

239
src/common/emoji-data.c Normal file
View File

@@ -0,0 +1,239 @@
/* 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;
}

91
src/common/emoji-data.h Normal file
View File

@@ -0,0 +1,91 @@
/* 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

@@ -3,6 +3,7 @@ common_sources = [
'chanopt.c', 'chanopt.c',
'ctcp.c', 'ctcp.c',
'dcc.c', 'dcc.c',
'emoji-data.c',
'gtk3-theme-service.c', 'gtk3-theme-service.c',
'zoitechat.c', 'zoitechat.c',
'history.c', 'history.c',
@@ -109,6 +110,20 @@ public_suffix_data = custom_target('public_suffix_data_h',
command: [python3, files('gen-public-suffix.py'), '@OUTPUT@'] 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', textevents = custom_target('textevents',
input: 'textevents.in', input: 'textevents.in',
output: ['textevents.h', 'textenums.h'], output: ['textevents.h', 'textenums.h'],
@@ -132,7 +147,7 @@ if get_option('plugin')
endif endif
zoitechat_common = static_library('zoitechatcommon', zoitechat_common = static_library('zoitechatcommon',
sources: [textevents, public_suffix_data] + marshal + common_sources + secretstore_sources, sources: [textevents, public_suffix_data, emoji_data_table] + marshal + common_sources + secretstore_sources,
include_directories: config_h_include, include_directories: config_h_include,
dependencies: common_deps + common_sysinfo_deps, dependencies: common_deps + common_sysinfo_deps,
c_args: common_cflags, c_args: common_cflags,
@@ -167,3 +182,18 @@ test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
protocol: 'tap', protocol: 'tap',
timeout: 120, 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

@@ -0,0 +1,240 @@
/* 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

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

View File

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

View File

@@ -204,7 +204,6 @@ struct zoitechatprefs
unsigned int hex_net_auto_reconnect; unsigned int hex_net_auto_reconnect;
unsigned int hex_net_auto_reconnectonfail; unsigned int hex_net_auto_reconnectonfail;
unsigned int hex_net_proxy_auth; unsigned int hex_net_proxy_auth;
unsigned int hex_net_remote_media;
unsigned int hex_net_throttle; unsigned int hex_net_throttle;
unsigned int hex_notify_whois_online; unsigned int hex_notify_whois_online;
unsigned int hex_perl_warnings; unsigned int hex_perl_warnings;
@@ -260,6 +259,7 @@ struct zoitechatprefs
int hex_gui_dialog_left; int hex_gui_dialog_left;
int hex_gui_dialog_top; int hex_gui_dialog_top;
int hex_gui_dialog_width; int hex_gui_dialog_width;
int hex_gui_emoji_skin_tone;
int hex_gui_lagometer; int hex_gui_lagometer;
int hex_gui_lang; int hex_gui_lang;
int hex_gui_pane_divider_position; int hex_gui_pane_divider_position;

779
src/fe-gtk/emoji-picker.c Normal file
View File

@@ -0,0 +1,779 @@
/* 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_cell)
{
GtkWidget *label = gtk_label_new (sequence);
gtk_style_context_add_class (gtk_widget_get_style_context (label),
small_cell ? "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;
}

31
src/fe-gtk/emoji-picker.h Normal file
View File

@@ -0,0 +1,31 @@
/* 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

@@ -49,7 +49,6 @@
#include "chanlist.h" #include "chanlist.h"
#include "joind.h" #include "joind.h"
#include "xtext.h" #include "xtext.h"
#include "inline-image.h"
#include "theme/theme-gtk.h" #include "theme/theme-gtk.h"
#include "menu.h" #include "menu.h"
#include "notifygui.h" #include "notifygui.h"
@@ -103,6 +102,86 @@ static const GOptionEntry gopt_entries[] =
{NULL} {NULL}
}; };
/* The emoji font is only bundled with Windows builds; other platforms
* rely on the operating system's emoji font (e.g. the distribution's
* Noto Color Emoji package on Linux). */
#ifdef WIN32
static gunichar2 *win32_private_emoji_font;
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);
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);
}
}
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;
/* Windows 7 cannot render color emoji fonts, so the bundled Noto
* Color Emoji font is not registered there; emoji rendering falls
* back to the monochrome Segoe UI Symbol glyphs instead. */
if (!win32_is_windows_8_or_newer ())
{
g_debug ("Windows 7 detected; using Segoe UI Symbol for emoji");
return;
}
font_path = fe_bundled_emoji_font_path ();
if (!font_path)
{
g_debug ("Bundled Noto Color Emoji font was not found; using platform font fallback");
return;
}
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);
}
g_free (font_path);
}
static void
fe_unregister_bundled_emoji_font (void)
{
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 #ifdef WIN32
static void static void
create_msg_dialog (gchar *title, gchar *message) create_msg_dialog (gchar *title, gchar *message)
@@ -441,6 +520,8 @@ fe_args (int argc, char *argv[])
} }
#ifdef WIN32 #ifdef WIN32
fe_register_bundled_emoji_font ();
win32_set_gsettings_schema_dir (); win32_set_gsettings_schema_dir ();
win32_set_appusermodelid (); win32_set_appusermodelid ();
win32_configure_pixbuf_loaders (); win32_configure_pixbuf_loaders ();
@@ -615,6 +696,9 @@ fe_main (void)
#endif #endif
gtk_main (); gtk_main ();
#ifdef WIN32
fe_unregister_bundled_emoji_font ();
#endif
/* sleep for 2 seconds so any QUIT messages are not lost. The */ /* sleep for 2 seconds so any QUIT messages are not lost. The */
/* GUI is closed at this point, so the user doesn't even know! */ /* GUI is closed at this point, so the user doesn't even know! */
@@ -944,13 +1028,7 @@ void
fe_print_text (struct session *sess, char *text, time_t stamp, fe_print_text (struct session *sess, char *text, time_t stamp,
gboolean no_activity) gboolean no_activity)
{ {
/* image links become a "[view image]" placeholder with a hidden URL */ PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp);
char *filtered = inline_image_filter_text (text);
PrintTextRaw (sess->res->buffer,
(unsigned char *)(filtered ? filtered : text),
prefs.hex_text_indent, stamp);
g_free (filtered);
if (no_activity || !sess->gui->is_tab) if (no_activity || !sess->gui->is_tab)
return; return;

View File

@@ -31,7 +31,7 @@
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol> <EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
@@ -59,7 +59,6 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="fkeys.h" /> <ClInclude Include="fkeys.h" />
<ClInclude Include="gtkutil.h" /> <ClInclude Include="gtkutil.h" />
<ClInclude Include="icon-resolver.h" /> <ClInclude Include="icon-resolver.h" />
<ClInclude Include="inline-image.h" />
<ClInclude Include="joind.h" /> <ClInclude Include="joind.h" />
<ClInclude Include="maingui.h" /> <ClInclude Include="maingui.h" />
<ClInclude Include="preferences-persistence.h" /> <ClInclude Include="preferences-persistence.h" />
@@ -98,12 +97,12 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClCompile Include="custom-list.c" /> <ClCompile Include="custom-list.c" />
<ClCompile Include="dccgui.c" /> <ClCompile Include="dccgui.c" />
<ClCompile Include="editlist.c" /> <ClCompile Include="editlist.c" />
<ClCompile Include="emoji-picker.c" />
<ClCompile Include="fe-gtk.c" /> <ClCompile Include="fe-gtk.c" />
<ClCompile Include="fkeys.c" /> <ClCompile Include="fkeys.c" />
<ClCompile Include="gtkutil.c" /> <ClCompile Include="gtkutil.c" />
<ClCompile Include="icon-resolver.c" /> <ClCompile Include="icon-resolver.c" />
<ClCompile Include="ignoregui.c" /> <ClCompile Include="ignoregui.c" />
<ClCompile Include="inline-image.c" />
<ClCompile Include="joind.c" /> <ClCompile Include="joind.c" />
<ClCompile Include="maingui.c" /> <ClCompile Include="maingui.c" />
<ClCompile Include="menu.c" /> <ClCompile Include="menu.c" />

View File

@@ -45,9 +45,6 @@
<ClInclude Include="icon-resolver.h"> <ClInclude Include="icon-resolver.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="inline-image.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="joind.h"> <ClInclude Include="joind.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -173,9 +170,6 @@
<ClCompile Include="ignoregui.c"> <ClCompile Include="ignoregui.c">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="inline-image.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="joind.c"> <ClCompile Include="joind.c">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +0,0 @@
/* ZoiteChat
* Copyright (C) 2026 ZoiteChat contributors.
*
* 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_INLINE_IMAGE_H
#define ZOITECHAT_INLINE_IMAGE_H
#include "xtext.h"
/* does this look like a direct http(s) link to an image file? */
gboolean inline_image_is_image_url (const char *url);
/* show the image behind 'url' inline below 'ent', or hide it again if it is
* already shown. Errors are reported to 'sess'. */
void inline_image_toggle (session *sess, GtkXText *xtext, textentry *ent,
const char *url);
/* replace image links in a message with a clickable placeholder label that
* hides the URL. Returns a newly allocated string, or NULL if the text has
* no image links (or the feature is off). */
char *inline_image_filter_text (const char *text);
/* pop up a window with a larger version of the entry's inline image */
void inline_image_show_viewer (GtkXText *xtext, textentry *ent);
/* can downloads go through the proxy configured for ZoiteChat (or is no
* proxy configured)? FALSE means loading media would have to bypass the
* user's proxy, which is never done. */
gboolean inline_image_proxy_usable (void);
#endif

View File

@@ -45,6 +45,7 @@
#include "theme/theme-manager.h" #include "theme/theme-manager.h"
#include "theme/theme-css.h" #include "theme/theme-css.h"
#include "banlist.h" #include "banlist.h"
#include "emoji-picker.h"
#include "gtkutil.h" #include "gtkutil.h"
#include "icon-resolver.h" #include "icon-resolver.h"
#include "joind.h" #include "joind.h"
@@ -59,7 +60,6 @@
#include "pixmaps.h" #include "pixmaps.h"
#include "plugin-tray.h" #include "plugin-tray.h"
#include "xtext.h" #include "xtext.h"
#include "inline-image.h"
#include "sexy-spell-entry.h" #include "sexy-spell-entry.h"
#include "gtkutil.h" #include "gtkutil.h"
@@ -201,7 +201,7 @@ enum
#define TAG_IRC 0 /* server, channel, dialog */ #define TAG_IRC 0 /* server, channel, dialog */
#define TAG_UTIL 1 /* dcc, notify, chanlist */ #define TAG_UTIL 1 /* dcc, notify, chanlist */
static void mg_apply_emoji_fallback_widget (GtkWidget *widget); static void mg_apply_emoji_fallback_widget (GtkWidget *widget, const PangoFontDescription *font);
static void mg_reply_show_child (GtkWidget *widget, gpointer data); static void mg_reply_show_child (GtkWidget *widget, gpointer data);
#define MG_CONFIG_SAVE_DEBOUNCE_MS 250 #define MG_CONFIG_SAVE_DEBOUNCE_MS 250
@@ -3247,7 +3247,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23); gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23);
gtk_widget_set_size_request (gui->key_entry, 58, 11); gtk_widget_set_size_request (gui->key_entry, 58, 11);
gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0); gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0);
mg_apply_emoji_fallback_widget (gui->key_entry); mg_apply_emoji_fallback_widget (gui->key_entry, NULL);
mg_apply_compact_mode_css (gui->key_entry); mg_apply_compact_mode_css (gui->key_entry);
g_signal_connect (G_OBJECT (gui->key_entry), "activate", g_signal_connect (G_OBJECT (gui->key_entry), "activate",
G_CALLBACK (mg_key_entry_cb), NULL); G_CALLBACK (mg_key_entry_cb), NULL);
@@ -3265,7 +3265,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5); gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5);
gtk_widget_set_size_request (gui->limit_entry, 45, 11); gtk_widget_set_size_request (gui->limit_entry, 45, 11);
gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0); gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0);
mg_apply_emoji_fallback_widget (gui->limit_entry); mg_apply_emoji_fallback_widget (gui->limit_entry, NULL);
mg_apply_compact_mode_css (gui->limit_entry); mg_apply_compact_mode_css (gui->limit_entry);
g_signal_connect (G_OBJECT (gui->limit_entry), "activate", g_signal_connect (G_OBJECT (gui->limit_entry), "activate",
G_CALLBACK (mg_limit_entry_cb), NULL); G_CALLBACK (mg_limit_entry_cb), NULL);
@@ -3530,8 +3530,13 @@ mg_apply_session_font_prefs (session_gui *gui)
mg_topicbar_update_height (gui->topic_entry); mg_topicbar_update_height (gui->topic_entry);
} }
if (gui->input_box && prefs.hex_gui_input_style) if (gui->input_box)
{
if (prefs.hex_gui_input_style)
theme_manager_apply_entry_palette (gui->input_box, font); theme_manager_apply_entry_palette (gui->input_box, font);
mg_apply_emoji_fallback_widget (gui->input_box,
prefs.hex_gui_input_style ? font : NULL);
}
if (gui->chanview) if (gui->chanview)
chanview_apply_theme (gui->chanview); chanview_apply_theme (gui->chanview);
@@ -3688,12 +3693,6 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
{ {
if (word == NULL) if (word == NULL)
{ {
/* a click on a shown inline image opens the viewer */
textentry *img_ent = gtk_xtext_get_clicked_image_entry (GTK_XTEXT (xtext));
if (img_ent)
inline_image_show_viewer (GTK_XTEXT (xtext), img_ent);
else
mg_focus (sess); mg_focus (sess);
return; return;
} }
@@ -3706,20 +3705,6 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
case WORD_HOST6: case WORD_HOST6:
case WORD_HOST: case WORD_HOST:
word[end] = 0; word[end] = 0;
/* image links toggle an inline preview instead
of opening the browser */
if (word_type == WORD_URL &&
prefs.hex_net_remote_media &&
inline_image_is_image_url (word + start))
{
textentry *ent = gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext));
if (ent)
{
inline_image_toggle (sess, GTK_XTEXT (xtext), ent, word + start);
break;
}
}
fe_open_url (word + start); fe_open_url (word + start);
} }
} }
@@ -3748,8 +3733,7 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
case WORD_HOST: case WORD_HOST:
word[end] = 0; word[end] = 0;
word += start; word += start;
menu_urlmenu_entry (even, word, xtext, menu_urlmenu (even, word);
gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext)));
break; break;
case WORD_NICK: case WORD_NICK:
word[end] = 0; word[end] = 0;
@@ -4443,15 +4427,34 @@ mg_inputbox_rightclick (GtkEntry *entry, GtkWidget *menu)
* *
* Goal: prefer color emoji fonts when available, without changing existing * Goal: prefer color emoji fonts when available, without changing existing
* font size/style/weight, and without breaking user-configured fonts. * font size/style/weight, and without breaking user-configured fonts.
*
* Pango already routes emoji presentation runs to a color emoji font on its
* own, so the base font must NOT list an emoji family explicitly: doing so
* hands the emoji font every character its cmap covers, including '#', '*',
* digits and adjacent spaces (keycap components), which then render with
* wide emoji metrics ("/join #" grows a huge gap in the input box). Only
* Windows 7, which has no color emoji font for Pango to find, gets an
* explicit fallback family appended.
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
static const char *mg_emoji_family_fallback =
#ifdef G_OS_WIN32 #ifdef G_OS_WIN32
"Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color"; static const char *
#else mg_emoji_family_fallback (void)
"Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color"; {
static char family_list[sizeof prefs.hex_text_font_alternative + 32];
if (prefs.hex_text_font_alternative[0])
{
g_snprintf (family_list, sizeof family_list, "%s, Segoe UI Symbol", prefs.hex_text_font_alternative);
return family_list;
}
return "Segoe UI Symbol";
}
#endif #endif
#ifdef G_OS_WIN32
/* Only used by the Windows 7 fallback to GTK's built-in emoji chooser */
static const char * static const char *
mg_find_available_icon_name (const char *const *icon_names) mg_find_available_icon_name (const char *const *icon_names)
{ {
@@ -4483,7 +4486,9 @@ mg_find_available_icon_name (const char *const *icon_names)
return NULL; return NULL;
} }
#endif
#ifdef G_OS_WIN32
static gboolean static gboolean
mg_family_already_has_emoji (const gchar *family) mg_family_already_has_emoji (const gchar *family)
{ {
@@ -4497,44 +4502,40 @@ mg_family_already_has_emoji (const gchar *family)
(strstr (family, "Twemoji") != NULL) || (strstr (family, "Twemoji") != NULL) ||
(strstr (family, "EmojiOne") != NULL); (strstr (family, "EmojiOne") != NULL);
} }
#endif
static PangoFontDescription * static PangoFontDescription *
mg_fontdesc_with_fallback (const PangoFontDescription *base_desc, gboolean emoji_first) mg_fontdesc_with_fallback (const PangoFontDescription *base_desc)
{ {
PangoFontDescription *desc; PangoFontDescription *desc;
const gchar *base_family;
gchar *family_list;
if (!base_desc) if (!base_desc)
return NULL; return NULL;
desc = pango_font_description_copy (base_desc); desc = pango_font_description_copy (base_desc);
base_family = pango_font_description_get_family (desc);
#ifdef G_OS_WIN32
if (!win32_is_windows_8_or_newer ())
{
const gchar *base_family = pango_font_description_get_family (desc);
gchar *family_list;
if (mg_family_already_has_emoji (base_family)) if (mg_family_already_has_emoji (base_family))
return desc; return desc;
if (emoji_first)
{
family_list = g_strdup_printf ("%s, %s",
mg_emoji_family_fallback,
(base_family && *base_family) ? base_family : "Sans");
}
else
{
family_list = g_strdup_printf ("%s, %s", family_list = g_strdup_printf ("%s, %s",
(base_family && *base_family) ? base_family : "Sans", (base_family && *base_family) ? base_family : "Sans",
mg_emoji_family_fallback); mg_emoji_family_fallback ());
}
pango_font_description_set_family (desc, family_list); pango_font_description_set_family (desc, family_list);
g_free (family_list); g_free (family_list);
}
#endif
return desc; return desc;
} }
static void static void
mg_apply_emoji_fallback_widget (GtkWidget *widget) mg_apply_emoji_fallback_widget (GtkWidget *widget, const PangoFontDescription *font)
{ {
PangoFontDescription *desc; PangoFontDescription *desc;
GtkStyleContext *context; GtkStyleContext *context;
@@ -4547,13 +4548,16 @@ mg_apply_emoji_fallback_widget (GtkWidget *widget)
if (!context) if (!context)
return; return;
if (font)
base_desc = pango_font_description_copy (font);
else
gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL, gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL,
"font", &base_desc, "font", &base_desc,
NULL); NULL);
if (!base_desc) if (!base_desc)
return; return;
desc = mg_fontdesc_with_fallback (base_desc, FALSE); desc = mg_fontdesc_with_fallback (base_desc);
pango_font_description_free (base_desc); pango_font_description_free (base_desc);
if (!desc) if (!desc)
return; return;
@@ -4754,7 +4758,7 @@ mg_create_search(session *sess, GtkWidget *box)
gui->shentry = entry = gtk_entry_new(); gui->shentry = entry = gtk_entry_new();
gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0);
gtk_widget_set_size_request (gui->shentry, 180, -1); gtk_widget_set_size_request (gui->shentry, 180, -1);
mg_apply_emoji_fallback_widget (entry); mg_apply_emoji_fallback_widget (entry, NULL);
mg_apply_entry_scroll_artifact_fix (entry); mg_apply_entry_scroll_artifact_fix (entry);
gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess); gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess);
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_esc), sess); g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_esc), sess);
@@ -4810,15 +4814,6 @@ mg_create_entry (session *sess, GtkWidget *box)
{ {
GtkWidget *hbox, *but, *entry; GtkWidget *hbox, *but, *entry;
session_gui *gui = sess->gui; 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); gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar"); gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
@@ -4871,16 +4866,45 @@ mg_create_entry (session *sess, GtkWidget *box)
if (prefs.hex_gui_input_style) if (prefs.hex_gui_input_style)
mg_apply_entry_style (entry); mg_apply_entry_style (entry);
mg_apply_emoji_fallback_widget (entry,
prefs.hex_gui_input_style && input_style ? input_style->font_desc : NULL);
mg_apply_entry_scroll_artifact_fix (entry); mg_apply_entry_scroll_artifact_fix (entry);
#ifdef G_OS_WIN32
/* Windows 7 cannot render the color emoji font that ZoiteChat's own
* picker is designed around, so it keeps the GTK built-in emoji
* chooser that was used before the picker existed (the HexChat-era
* behavior, which works on Windows 7). */
if (!win32_is_windows_8_or_newer ())
{
const char *emoji_fallback_icon_names[] = {
"face-smile-symbolic",
"face-smile",
"insert-emoticon-symbolic",
"insert-emoticon",
"zc-menu-emoji",
NULL
};
g_object_set (G_OBJECT (entry), "show-emoji-icon", TRUE, NULL); 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) 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); const char *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); if (icon_name)
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY, icon_name);
} }
return;
}
#endif
/* ZoiteChat's own emoji picker; deliberately not GTK's
* "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);
} }
static void static void

View File

@@ -45,7 +45,6 @@
#include "../common/util.h" #include "../common/util.h"
#include "../common/text.h" #include "../common/text.h"
#include "xtext.h" #include "xtext.h"
#include "inline-image.h"
#include "ascii.h" #include "ascii.h"
#include "banlist.h" #include "banlist.h"
#include "chanlist.h" #include "chanlist.h"
@@ -1057,40 +1056,8 @@ open_url_cb (GtkWidget *item, char *url)
handle_command (current_sess, buf, FALSE); handle_command (current_sess, buf, FALSE);
} }
/* the text entry the open URL menu was created for; the widget pointer is
nulled automatically if the widget dies while the menu is up */
static GtkXText *urlmenu_xtext = NULL;
static textentry *urlmenu_ent = NULL;
static void
menu_urlmenu_set_entry (GtkWidget *xtext, textentry *ent)
{
if (urlmenu_xtext)
g_object_remove_weak_pointer (G_OBJECT (urlmenu_xtext),
(gpointer *) &urlmenu_xtext);
urlmenu_xtext = xtext ? GTK_XTEXT (xtext) : NULL;
urlmenu_ent = ent;
if (urlmenu_xtext)
g_object_add_weak_pointer (G_OBJECT (urlmenu_xtext),
(gpointer *) &urlmenu_xtext);
}
static void
inline_image_menu_cb (GtkWidget *item, char *url)
{
if (urlmenu_xtext == NULL || urlmenu_ent == NULL)
return;
/* the line may have scrolled out of the buffer since the menu opened */
if (!gtk_xtext_buffer_contains (urlmenu_xtext, urlmenu_ent))
return;
inline_image_toggle (current_sess, urlmenu_xtext, urlmenu_ent, url);
}
void void
menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext, menu_urlmenu (GdkEventButton *event, char *url)
textentry *ent)
{ {
GtkWidget *menu; GtkWidget *menu;
char *tmp, *chop; char *tmp, *chop;
@@ -1098,14 +1065,6 @@ menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
g_free (str_copy); g_free (str_copy);
str_copy = g_strdup (url); str_copy = g_strdup (url);
if (xtext == NULL || ent == NULL || !prefs.hex_net_remote_media ||
!inline_image_is_image_url (str_copy))
{
xtext = NULL;
ent = NULL;
}
menu_urlmenu_set_entry (xtext, ent);
menu = menu_new (); menu = menu_new ();
/* more than 51 chars? Chop it */ /* more than 51 chars? Chop it */
if (g_utf8_strlen (str_copy, -1) >= 52) if (g_utf8_strlen (str_copy, -1) >= 52)
@@ -1128,15 +1087,6 @@ menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy); menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy);
else else
menu_quick_item_with_callback (open_url_cb, _("Open Link in Browser"), menu, str_copy); menu_quick_item_with_callback (open_url_cb, _("Open Link in Browser"), menu, str_copy);
if (ent)
{
const char *shown = gtk_xtext_entry_get_image_url (GTK_XTEXT (xtext), ent);
menu_quick_item_with_callback (inline_image_menu_cb,
shown && strcmp (shown, str_copy) == 0 ?
_("Hide Inline Image") : _("Show Image Inline"),
menu, str_copy);
}
menu_quick_item_with_callback (copy_to_clipboard_cb, _("Copy Selected Link"), menu, str_copy); menu_quick_item_with_callback (copy_to_clipboard_cb, _("Copy Selected Link"), menu, str_copy);
/* custom ones from urlhandlers.conf */ /* custom ones from urlhandlers.conf */
menu_create (menu, urlhandler_list, str_copy, TRUE); menu_create (menu, urlhandler_list, str_copy, TRUE);
@@ -1144,12 +1094,6 @@ menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
menu_popup (menu, event, NULL); menu_popup (menu, event, NULL);
} }
void
menu_urlmenu (GdkEventButton *event, char *url)
{
menu_urlmenu_entry (event, url, NULL, NULL);
}
static void static void
menu_chan_cycle (GtkWidget * menu, char *chan) menu_chan_cycle (GtkWidget * menu, char *chan)
{ {

View File

@@ -20,11 +20,8 @@
#ifndef ZOITECHAT_MENU_H #ifndef ZOITECHAT_MENU_H
#define ZOITECHAT_MENU_H #define ZOITECHAT_MENU_H
#include "xtext.h"
GtkWidget *menu_create_main (void *accel_group, int bar, int away, int toplevel, GtkWidget **menu_widgets); GtkWidget *menu_create_main (void *accel_group, int bar, int away, int toplevel, GtkWidget **menu_widgets);
void menu_urlmenu (GdkEventButton * event, char *url); void menu_urlmenu (GdkEventButton * event, char *url);
void menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext, textentry *ent);
void menu_chanmenu (session *sess, GdkEventButton * event, char *chan); void menu_chanmenu (session *sess, GdkEventButton * event, char *chan);
void menu_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree); void menu_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree);
void menu_addconnectmenu (server *serv, GtkWidget *menu); void menu_addconnectmenu (server *serv, GtkWidget *menu);

View File

@@ -18,12 +18,12 @@ zoitechat_gtk_sources = [
'custom-list.c', 'custom-list.c',
'dccgui.c', 'dccgui.c',
'editlist.c', 'editlist.c',
'emoji-picker.c',
'fe-gtk.c', 'fe-gtk.c',
'fkeys.c', 'fkeys.c',
'gtkutil.c', 'gtkutil.c',
'icon-resolver.c', 'icon-resolver.c',
'ignoregui.c', 'ignoregui.c',
'inline-image.c',
'joind.c', 'joind.c',
'menu.c', 'menu.c',
'maingui.c', 'maingui.c',

View File

@@ -673,11 +673,6 @@ static const setting network_settings[] =
{ST_ENTRY, N_("Username:"), P_OFFSETNL(hex_net_proxy_user), 0, 0, sizeof prefs.hex_net_proxy_user}, {ST_ENTRY, N_("Username:"), P_OFFSETNL(hex_net_proxy_user), 0, 0, sizeof prefs.hex_net_proxy_user},
{ST_ENTRY, N_("Password:"), P_OFFSETNL(hex_net_proxy_pass), 0, GINT_TO_POINTER(1), sizeof prefs.hex_net_proxy_pass}, {ST_ENTRY, N_("Password:"), P_OFFSETNL(hex_net_proxy_pass), 0, GINT_TO_POINTER(1), sizeof prefs.hex_net_proxy_pass},
{ST_HEADER, N_("Remote Media"), 0, 0, 0, 0},
{ST_TOGGLE, N_("Allow loading remote media (inline images) when clicked"),
P_OFFINTNL(hex_net_remote_media),
N_("Loading remote media contacts the site hosting it, which can reveal your IP address and that you viewed the message. You are asked for confirmation before each new domain is contacted."), 0, 0},
{ST_END, 0, 0, 0, 0, 0} {ST_END, 0, 0, 0, 0, 0}
}; };

View File

@@ -47,6 +47,7 @@
#include "fe-gtk.h" #include "fe-gtk.h"
#include "xtext.h" #include "xtext.h"
#include "fkeys.h" #include "fkeys.h"
#include "gtkutil.h"
#include "theme/theme-access.h" #include "theme/theme-access.h"
#define charlen(str) g_utf8_skip[*(guchar *)(str)] #define charlen(str) g_utf8_skip[*(guchar *)(str)]
@@ -92,104 +93,8 @@ struct textentry
guchar pad1; guchar pad1;
guchar pad2; guchar pad2;
GList *marks; /* List of found strings */ GList *marks; /* List of found strings */
GdkPixbuf *image; /* inline image shown below the text, or NULL */
GdkPixbuf *image_full; /* larger version for the image viewer */
char *image_url; /* the URL the image was loaded from */
}; };
/* inline image display */
#define XTEXT_IMAGE_PADDING 4
#define XTEXT_IMAGE_MAX_WIDTH 480
#define XTEXT_IMAGE_MAX_HEIGHT 360
#define XTEXT_IMAGE_FULL_MAX_WIDTH 1600
#define XTEXT_IMAGE_FULL_MAX_HEIGHT 1200
/* an inline image load that has been started but not yet completed;
ent/buf/xtext are invalidated (entry removed) when any of them dies */
typedef struct
{
guint id;
GtkXText *xtext;
xtext_buffer *buf;
textentry *ent;
char *url;
} xtext_image_pending;
static GSList *image_pending_list = NULL;
static guint image_pending_next_id = 1;
static void
gtk_xtext_image_pending_forget (gboolean (*match) (xtext_image_pending *, gconstpointer), gconstpointer data)
{
GSList *list = image_pending_list;
while (list)
{
GSList *next = list->next;
xtext_image_pending *pending = list->data;
if (match (pending, data))
{
image_pending_list = g_slist_delete_link (image_pending_list, list);
g_free (pending->url);
g_free (pending);
}
list = next;
}
}
static gboolean
gtk_xtext_image_pending_match_ent (xtext_image_pending *pending, gconstpointer ent)
{
return pending->ent == ent;
}
static gboolean
gtk_xtext_image_pending_match_buf (xtext_image_pending *pending, gconstpointer buf)
{
return pending->buf == buf;
}
static gboolean
gtk_xtext_image_pending_match_xtext (xtext_image_pending *pending, gconstpointer xtext)
{
return pending->xtext == xtext;
}
/* number of extra screen lines an entry needs for its inline image */
static int
gtk_xtext_image_rows (GtkXText *xtext, textentry *ent)
{
if (ent->image == NULL || xtext->fontsize <= 0)
return 0;
return (gdk_pixbuf_get_height (ent->image) + XTEXT_IMAGE_PADDING * 2 +
xtext->fontsize - 1) / xtext->fontsize;
}
/* free an entry's inline image and drop any pending load aimed at it;
must be called whenever a textentry is freed */
static void
gtk_xtext_ent_free_image (textentry *ent)
{
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_ent, ent);
if (ent->image)
{
g_object_unref (ent->image);
ent->image = NULL;
}
if (ent->image_full)
{
g_object_unref (ent->image_full);
ent->image_full = NULL;
}
g_free (ent->image_url);
ent->image_url = NULL;
}
enum enum
{ {
WORD_CLICK, WORD_CLICK,
@@ -309,7 +214,6 @@ static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk
static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp); static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp);
static gboolean gtk_xtext_word_select_char (const unsigned char *ch); static gboolean gtk_xtext_word_select_char (const unsigned char *ch);
static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len); static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len);
static textentry * gtk_xtext_image_hit (GtkXText *xtext, int x, int y);
static inline void static inline void
gtk_xtext_cursor_unref (GdkCursor *cursor) gtk_xtext_cursor_unref (GdkCursor *cursor)
@@ -754,6 +658,93 @@ backend_font_open (GtkXText *xtext, char *name)
pango_font_metrics_unref (metrics); pango_font_metrics_unref (metrics);
} }
#ifdef WIN32
static gboolean
xtext_win7_is_emoji_char (gunichar ch)
{
return (ch >= 0x1f000 && ch <= 0x1faff) ||
(ch >= 0x2600 && ch <= 0x27bf) ||
(ch >= 0x2300 && ch <= 0x23ff) ||
(ch >= 0x2b00 && ch <= 0x2bff) ||
(ch >= 0xfe00 && ch <= 0xfe0f) ||
ch == 0x200d;
}
static const char *
xtext_win7_emoji_font_family (void)
{
static char family[FONTNAMELEN + 1];
static char source[sizeof prefs.hex_text_font_alternative];
char **families;
int i;
if (win32_is_windows_8_or_newer ())
return NULL;
if (strcmp (source, prefs.hex_text_font_alternative) == 0 && family[0])
return family;
family[0] = 0;
g_strlcpy (source, prefs.hex_text_font_alternative, sizeof source);
families = g_strsplit (prefs.hex_text_font_alternative, ",", -1);
for (i = 0; families[i]; i++)
{
char *name = g_strstrip (families[i]);
if (*name && gtkutil_find_font (name))
{
g_strlcpy (family, name, sizeof family);
break;
}
}
g_strfreev (families);
if (!family[0] && gtkutil_find_font ("Segoe UI Symbol"))
g_strlcpy (family, "Segoe UI Symbol", sizeof family);
return family[0] ? family : NULL;
}
static PangoAttrList *
xtext_win7_emoji_attrs (const char *str, int len, int emphasis)
{
const char *family;
const char *p;
const char *end;
PangoAttrList *attrs = NULL;
family = xtext_win7_emoji_font_family ();
if (!family)
return attr_lists[emphasis];
p = str;
end = str + len;
while (p < end)
{
const char *next = g_utf8_next_char (p);
gunichar ch = g_utf8_get_char_validated (p, end - p);
if (ch != (gunichar)-1 && ch != (gunichar)-2 && xtext_win7_is_emoji_char (ch))
{
PangoAttribute *attr;
if (!attrs)
attrs = pango_attr_list_copy (attr_lists[emphasis]);
attr = pango_attr_family_new (family);
attr->start_index = p - str;
attr->end_index = next - str;
pango_attr_list_insert (attrs, attr);
}
p = next;
}
return attrs ? attrs : attr_lists[emphasis];
}
#endif
static int static int
backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis) backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis)
{ {
@@ -777,8 +768,18 @@ backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis
deltaw = fontwidths[emphasis][*str]; deltaw = fontwidths[emphasis][*str];
else else
{ {
#ifdef WIN32
PangoAttrList *attrs = xtext_win7_emoji_attrs ((const char *)str, mbl, emphasis);
pango_layout_set_attributes (xtext->layout, attrs);
#endif
pango_layout_set_text (xtext->layout, str, mbl); pango_layout_set_text (xtext->layout, str, mbl);
pango_layout_get_pixel_size (xtext->layout, &deltaw, NULL); pango_layout_get_pixel_size (xtext->layout, &deltaw, NULL);
#ifdef WIN32
if (attrs != attr_lists[emphasis])
pango_attr_list_unref (attrs);
pango_layout_set_attributes (xtext->layout, attr_lists[emphasis]);
#endif
} }
width += deltaw; width += deltaw;
str += mbl; str += mbl;
@@ -812,10 +813,18 @@ backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y,
{ {
cairo_t *cr; cairo_t *cr;
PangoLayoutLine *line; PangoLayoutLine *line;
#ifdef WIN32
PangoAttrList *attrs;
#endif
cr = xtext_create_context (xtext); cr = xtext_create_context (xtext);
#ifdef WIN32
attrs = xtext_win7_emoji_attrs (str, len, emphasis);
pango_layout_set_attributes (xtext->layout, attrs);
#else
pango_layout_set_attributes (xtext->layout, attr_lists[emphasis]); pango_layout_set_attributes (xtext->layout, attr_lists[emphasis]);
#endif
pango_layout_set_text (xtext->layout, str, len); pango_layout_set_text (xtext->layout, str, len);
if (dofill) if (dofill)
@@ -831,7 +840,10 @@ backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y,
cairo_move_to (cr, x, y); cairo_move_to (cr, x, y);
pango_cairo_show_layout_line (cr, line); pango_cairo_show_layout_line (cr, line);
cairo_restore (cr); cairo_restore (cr);
#ifdef WIN32
if (attrs != attr_lists[emphasis])
pango_attr_list_unref (attrs);
#endif
cairo_destroy (cr); cairo_destroy (cr);
} }
@@ -1159,8 +1171,6 @@ gtk_xtext_dispose (GObject *object)
{ {
GtkXText *xtext = GTK_XTEXT (object); GtkXText *xtext = GTK_XTEXT (object);
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_xtext, xtext);
g_clear_pointer (&xtext->tooltip_url, g_free);
gtk_xtext_cleanup (xtext); gtk_xtext_cleanup (xtext);
if (G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose) if (G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose)
@@ -1542,7 +1552,7 @@ gtk_xtext_find_x (GtkXText * xtext, int x, textentry * ent, int subline,
str = ent->str + gtk_xtext_find_subline (xtext, ent, subline); str = ent->str + gtk_xtext_find_subline (xtext, ent, subline);
if (str >= ent->str + ent->str_len) if (str >= ent->str + ent->str_len)
return ent->str_len; /* e.g. the blank rows of an inline image */ return 0;
/* Let user select left a few pixels to grab hidden text e.g. '<' */ /* Let user select left a few pixels to grab hidden text e.g. '<' */
if (x < indent - xtext->space_width) if (x < indent - xtext->space_width)
@@ -2242,7 +2252,6 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
int len; int len;
int out_of_bounds = 0; int out_of_bounds = 0;
int len_to_offset = 0; int len_to_offset = 0;
gboolean has_hidden;
ent = gtk_xtext_find_char (xtext, x, y, &offset, &out_of_bounds); ent = gtk_xtext_find_char (xtext, x, y, &offset, &out_of_bounds);
if (ent == NULL || out_of_bounds || offset < 0 || offset >= ent->str_len) if (ent == NULL || out_of_bounds || offset < 0 || offset >= ent->str_len)
@@ -2287,15 +2296,10 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
if (ret_len) if (ret_len)
*ret_len = len; /* Length before stripping */ *ret_len = len; /* Length before stripping */
/* a word with hidden text is a placeholder (e.g. an inline image link
label); its whole visible part acts as the link */
has_hidden = memchr (word, ATTR_HIDDEN, len) != NULL;
word = gtk_xtext_strip_color (word, len, xtext->scratch_buffer, NULL, slp, FALSE); word = gtk_xtext_strip_color (word, len, xtext->scratch_buffer, NULL, slp, FALSE);
/* avoid turning the cursor into a hand for non-url part of the word */ /* avoid turning the cursor into a hand for non-url part of the word */
if (!has_hidden && if (xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
{ {
int start, end; int start, end;
url_last (&start, &end); url_last (&start, &end);
@@ -2379,43 +2383,12 @@ gtk_xtext_unrender_hilight (GtkXText *xtext)
xtext->un_hilight = FALSE; xtext->un_hilight = FALSE;
} }
/* show (or stop showing) the destination URL of a hovered placeholder
word as the widget's tooltip */
static void
gtk_xtext_set_url_tooltip (GtkXText *xtext, const char *url, int url_len)
{
if (url == NULL)
{
if (xtext->tooltip_url)
{
g_free (xtext->tooltip_url);
xtext->tooltip_url = NULL;
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), NULL);
xtext->tooltip_stamp_set = FALSE;
}
return;
}
if (xtext->tooltip_url &&
strlen (xtext->tooltip_url) == (gsize) url_len &&
memcmp (xtext->tooltip_url, url, url_len) == 0)
return;
g_free (xtext->tooltip_url);
xtext->tooltip_url = g_strndup (url, url_len);
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), xtext->tooltip_url);
xtext->tooltip_stamp_set = FALSE;
}
static gboolean static gboolean
gtk_xtext_leave_notify (GtkWidget * widget, GdkEventCrossing * event) gtk_xtext_leave_notify (GtkWidget * widget, GdkEventCrossing * event)
{ {
GtkXText *xtext = GTK_XTEXT (widget); GtkXText *xtext = GTK_XTEXT (widget);
GdkWindow *window = gtk_widget_get_window (widget); GdkWindow *window = gtk_widget_get_window (widget);
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
if (xtext->cursor_hand) if (xtext->cursor_hand)
{ {
gtk_xtext_unrender_hilight (xtext); gtk_xtext_unrender_hilight (xtext);
@@ -2479,7 +2452,6 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
GSList *slp = NULL; GSList *slp = NULL;
unsigned char *word; unsigned char *word;
int word_type = 0; int word_type = 0;
gboolean placeholder = FALSE;
word = gtk_xtext_get_word (xtext, x, y, word_ent, offset, len, &slp); word = gtk_xtext_get_word (xtext, x, y, word_ent, offset, len, &slp);
if (word) if (word)
@@ -2489,18 +2461,7 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
word_type = xtext->urlcheck_function (GTK_WIDGET (xtext), word); word_type = xtext->urlcheck_function (GTK_WIDGET (xtext), word);
if (word_type > 0) if (word_type > 0)
{ {
/* words with hidden text are placeholders (e.g. inline image if (url_last (&laststart, &lastend))
links): hilight the whole word, not just the URL inside it */
if (memchr ((*word_ent)->str + *offset, ATTR_HIDDEN, *len))
{
/* the label hides the real destination, so show the full
URL as a tooltip while hovering */
placeholder = TRUE;
if (url_last (&laststart, &lastend) && lastend > laststart)
gtk_xtext_set_url_tooltip (xtext,
(const char *) word + laststart, lastend - laststart);
}
else if (url_last (&laststart, &lastend))
{ {
int cumlen, startadj = 0, endadj = 0; int cumlen, startadj = 0, endadj = 0;
offlen_t *meta; offlen_t *meta;
@@ -2530,9 +2491,6 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
} }
g_slist_free_full (slp, g_free); g_slist_free_full (slp, g_free);
if (!placeholder)
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
return word_type; return word_type;
} }
@@ -2664,20 +2622,6 @@ gtk_xtext_motion_notify (GtkWidget * widget, GdkEventMotion * event)
} }
tooltip_check: tooltip_check:
/* hovering an inline image? it opens the viewer when clicked */
if (gtk_xtext_image_hit (xtext, x, y))
{
if (xtext->hilight_ent) /* moving here straight from a link */
gtk_xtext_leave_notify (widget, NULL);
if (!xtext->cursor_hand)
{
gdk_window_set_cursor (window, xtext->hand_cursor);
xtext->cursor_hand = TRUE;
xtext->cursor_resize = FALSE;
}
return FALSE;
}
if (xtext->buffer->time_stamp && xtext->buffer->indent > 0 && x >= 0 && x < xtext->stamp_width) if (xtext->buffer->time_stamp && xtext->buffer->indent > 0 && x >= 0 && x < xtext->stamp_width)
{ {
textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL); textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL);
@@ -2785,46 +2729,6 @@ gtk_xtext_unselect (GtkXText *xtext)
xtext->buffer->last_ent_end = NULL; xtext->buffer->last_ent_end = NULL;
} }
/* check whether the given widget coordinates hit an entry's inline image;
returns the entry if so */
static textentry *
gtk_xtext_image_hit (GtkXText *xtext, int x, int y)
{
textentry *ent;
int line, subline, image_rows, text_rows, rel_y, img_x;
if (xtext->fontsize <= 0)
return NULL;
/* adjust y value for negative rounding, double to int */
if (y < 0)
y -= xtext->fontsize;
line = (y + xtext->pixel_offset) / xtext->fontsize;
ent = gtk_xtext_nth (xtext, line + (int) xtext_adj_get_value (xtext->adj),
&subline);
if (ent == NULL || ent->image == NULL)
return NULL;
image_rows = gtk_xtext_image_rows (xtext, ent);
text_rows = ent->subline_count - image_rows;
if (subline < text_rows)
return NULL;
img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
if (x < img_x || x >= img_x + gdk_pixbuf_get_width (ent->image))
return NULL;
rel_y = (subline - text_rows) * xtext->fontsize +
(y + xtext->pixel_offset) - (line * xtext->fontsize) -
XTEXT_IMAGE_PADDING;
if (rel_y < 0 || rel_y >= gdk_pixbuf_get_height (ent->image))
return NULL;
return ent;
}
static gboolean static gboolean
gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event) gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
{ {
@@ -2897,15 +2801,8 @@ gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
if (!gtk_xtext_is_selecting (xtext)) if (!gtk_xtext_is_selecting (xtext))
{ {
textentry *word_ent = NULL; word = gtk_xtext_get_word (xtext, event_x, event_y, 0, 0, 0, 0);
word = gtk_xtext_get_word (xtext, event_x, event_y, &word_ent, 0, 0, 0);
xtext->clicked_ent = word ? word_ent : NULL;
xtext->clicked_image_ent = word ? NULL :
gtk_xtext_image_hit (xtext, event_x, event_y);
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, word ? word : NULL, event); g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, word ? word : NULL, event);
xtext->clicked_ent = NULL;
xtext->clicked_image_ent = NULL;
} }
} }
@@ -2932,16 +2829,14 @@ gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event)
if (event->button == 3 || event->button == 2) /* right/middle click */ if (event->button == 3 || event->button == 2) /* right/middle click */
{ {
word = gtk_xtext_get_word (xtext, x, y, &ent, 0, 0, 0); word = gtk_xtext_get_word (xtext, x, y, 0, 0, 0, 0);
if (word) if (word)
{ {
xtext->clicked_ent = ent;
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
word, event); word, event);
} else } else
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
"", event); "", event);
xtext->clicked_ent = NULL;
return FALSE; return FALSE;
} }
@@ -4319,7 +4214,6 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
unsigned char *str; unsigned char *str;
int indent, taken, entline, len, y, start_subline; int indent, taken, entline, len, y, start_subline;
int emphasis = 0; int emphasis = 0;
int image_rows;
entline = taken = 0; entline = taken = 0;
str = ent->str; str = ent->str;
@@ -4390,56 +4284,6 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
} }
while (str < ent->str + ent->str_len); while (str < ent->str + ent->str_len);
/* draw the inline image on its blank rows below the text. 'subline'
still holds the rows left to skip when the page starts inside the
image area (the text loop above only consumes text sublines). */
image_rows = gtk_xtext_image_rows (xtext, ent);
if (image_rows > 0)
{
int skip = MIN (subline, image_rows);
int rows = image_rows - skip;
int first_row_y = (xtext->fontsize * line) - xtext->pixel_offset;
int drawn = 0;
gboolean render_image = !xtext->dont_render && !xtext->skip_border_fills &&
!xtext->render_hilights_only;
while (rows > 0 && line < lines_max)
{
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
if (render_image)
{
int bg_x = MAX (0, xtext->clip_x);
int bg_w = MIN (win_width + MARGIN, xtext->clip_x2) - bg_x;
if (bg_w > 0)
xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
gtk_xtext_draw_sep (xtext, y - xtext->font->ascent);
}
line++;
taken++;
rows--;
drawn++;
}
if (drawn > 0 && render_image)
{
/* this may be the shared draw context, so the clip has to be
undone before anything else is rendered with it */
cairo_t *cr = xtext_create_context (xtext);
int img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
int img_y = first_row_y - (skip * xtext->fontsize) + XTEXT_IMAGE_PADDING;
cairo_save (cr);
cairo_rectangle (cr, 0, first_row_y, win_width + MARGIN,
drawn * xtext->fontsize);
cairo_clip (cr);
gdk_cairo_set_source_pixbuf (cr, ent->image, img_x, img_y);
cairo_paint (cr);
cairo_restore (cr);
cairo_destroy (cr);
}
}
gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline)); gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline));
return taken; return taken;
@@ -4598,7 +4442,6 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
unsigned char *str; unsigned char *str;
int indent, len; int indent, len;
int win_width; int win_width;
int image_rows, i;
g_slist_free (ent->sublines); g_slist_free (ent->sublines);
ent->sublines = NULL; ent->sublines = NULL;
@@ -4608,9 +4451,10 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
if (win_width >= ent->indent + ent->str_width) if (win_width >= ent->indent + ent->str_width)
{ {
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len)); ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len));
ent->subline_count = 1;
return ent->subline_count;
} }
else
{
indent = ent->indent; indent = ent->indent;
str = ent->str; str = ent->str;
@@ -4622,13 +4466,6 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
str += len; str += len;
} }
while (str < ent->str + ent->str_len); while (str < ent->str + ent->str_len);
}
/* extra blank rows that make room for an attached inline image;
their offset is str_len, i.e. past the end of the text */
image_rows = gtk_xtext_image_rows (buf->xtext, ent);
for (i = 0; i < image_rows; i++)
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER ((gint) ent->str_len));
ent->subline_count = g_slist_length (ent->sublines); ent->subline_count = g_slist_length (ent->sublines);
return ent->subline_count; return ent->subline_count;
@@ -5051,8 +4888,6 @@ gtk_xtext_kill_ent (xtext_buffer *buffer, textentry *ent)
ent->sublines = NULL; ent->sublines = NULL;
ent->subline_count = 0; ent->subline_count = 0;
gtk_xtext_ent_free_image (ent);
g_free (ent); g_free (ent);
return visible; return visible;
} }
@@ -5190,7 +5025,6 @@ gtk_xtext_clear (xtext_buffer *buf, int lines)
while (buf->text_first) while (buf->text_first)
{ {
next = buf->text_first->next; next = buf->text_first->next;
gtk_xtext_ent_free_image (buf->text_first);
g_free (buf->text_first); g_free (buf->text_first);
buf->text_first = next; buf->text_first = next;
} }
@@ -5710,9 +5544,6 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp)
ent->next = NULL; ent->next = NULL;
ent->marks = NULL; ent->marks = NULL;
ent->subline_count = 0; ent->subline_count = 0;
ent->image = NULL;
ent->image_full = NULL;
ent->image_url = NULL;
if (ent->indent < MARGIN) if (ent->indent < MARGIN)
ent->indent = MARGIN; /* 2 pixels is the left margin */ ent->indent = MARGIN; /* 2 pixels is the left margin */
@@ -6233,260 +6064,13 @@ gtk_xtext_buffer_free (xtext_buffer *buf)
gtk_xtext_search_fini (buf); gtk_xtext_search_fini (buf);
} }
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_buf, buf);
ent = buf->text_first; ent = buf->text_first;
while (ent) while (ent)
{ {
next = ent->next; next = ent->next;
gtk_xtext_ent_free_image (ent);
g_free (ent); g_free (ent);
ent = next; ent = next;
} }
g_free (buf); g_free (buf);
} }
/* ========================================= */
/* ========== INLINE IMAGE API ============= */
/* ========================================= */
/* the entry the last word_click signal was emitted for; only valid while
handling that signal */
textentry *
gtk_xtext_get_clicked_entry (GtkXText *xtext)
{
return xtext->clicked_ent;
}
/* the entry whose inline image the last word_click signal (with a NULL
word) landed on; only valid while handling that signal */
textentry *
gtk_xtext_get_clicked_image_entry (GtkXText *xtext)
{
return xtext->clicked_image_ent;
}
/* check that 'ent' is still an entry of the currently shown buffer; used to
validate entry pointers that were remembered across main loop iterations */
gboolean
gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent)
{
textentry *walk;
if (ent == NULL)
return FALSE;
for (walk = xtext->buffer->text_first; walk; walk = walk->next)
{
if (walk == ent)
return TRUE;
}
return FALSE;
}
gboolean
gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent)
{
return ent != NULL && ent->image != NULL;
}
/* the URL of the entry's attached image, or NULL if it has none */
const char *
gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent)
{
return ent && ent->image ? ent->image_url : NULL;
}
/* larger version of the entry's attached image, for the viewer */
GdkPixbuf *
gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent)
{
if (ent == NULL)
return NULL;
return ent->image_full ? ent->image_full : ent->image;
}
/* the entry's subline count changed (image attached or removed):
redo its layout, fix the scrollbar and redraw */
static void
gtk_xtext_image_relayout (GtkXText *xtext, xtext_buffer *buf, textentry *ent)
{
buf->num_lines -= ent->subline_count;
buf->num_lines += gtk_xtext_lines_taken (buf, ent);
buf->pagetop_ent = NULL;
dontscroll (buf);
if (xtext->buffer == buf)
{
gtk_xtext_adjustment_set (buf, TRUE);
if (buf->scrollbar_down)
xtext_adj_set_value (xtext->adj,
xtext_adj_get_upper (xtext->adj) -
xtext_adj_get_page_size (xtext->adj));
if (gtk_widget_get_realized (GTK_WIDGET (xtext)))
gtk_widget_queue_draw (GTK_WIDGET (xtext));
}
}
/* scale down (never up) to fit within max_width x max_height */
static GdkPixbuf *
gtk_xtext_image_scale_max (GdkPixbuf *pixbuf, int max_width, int max_height)
{
int width = gdk_pixbuf_get_width (pixbuf);
int height = gdk_pixbuf_get_height (pixbuf);
int new_width, new_height;
double scale;
if (width < 1 || height < 1)
return NULL;
if (width <= max_width && height <= max_height)
return g_object_ref (pixbuf);
scale = MIN ((double) max_width / width, (double) max_height / height);
new_width = MAX (1, (int) (width * scale));
new_height = MAX (1, (int) (height * scale));
return gdk_pixbuf_scale_simple (pixbuf, new_width, new_height,
GDK_INTERP_BILINEAR);
}
/* scale down (never up) to fit the usual text area */
static GdkPixbuf *
gtk_xtext_image_scale (xtext_buffer *buf, GdkPixbuf *pixbuf)
{
int max_width = XTEXT_IMAGE_MAX_WIDTH;
int avail;
avail = buf->window_width - buf->indent - MARGIN - XTEXT_IMAGE_PADDING * 2;
if (avail >= 64 && avail < max_width)
max_width = avail;
return gtk_xtext_image_scale_max (pixbuf, max_width, XTEXT_IMAGE_MAX_HEIGHT);
}
/* register an asynchronous image load for 'ent'. Returns a handle to pass
to gtk_xtext_image_load_finish(), or 0 if the entry already has an image
or a load underway. */
guint
gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url)
{
xtext_image_pending *pending;
GSList *list;
if (ent == NULL || ent->image != NULL)
return 0;
for (list = image_pending_list; list; list = list->next)
{
pending = list->data;
if (pending->ent == ent)
return 0;
}
pending = g_new0 (xtext_image_pending, 1);
pending->id = image_pending_next_id++;
if (image_pending_next_id == 0) /* skip 0 on wrap, it means failure */
image_pending_next_id = 1;
pending->xtext = xtext;
pending->buf = xtext->buffer;
pending->ent = ent;
pending->url = g_strdup (url);
image_pending_list = g_slist_prepend (image_pending_list, pending);
return pending->id;
}
/* complete a load started with gtk_xtext_image_load_begin(). pixbuf may be
NULL to abandon the load (e.g. download failed). Returns TRUE if the
image was attached, FALSE if it was dropped (failure, or the entry is
gone). The caller keeps ownership of the pixbuf. */
gboolean
gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf)
{
xtext_image_pending *pending = NULL;
GSList *list;
GdkPixbuf *scaled;
GtkXText *xtext;
xtext_buffer *buf;
textentry *ent;
if (handle == 0)
return FALSE;
for (list = image_pending_list; list; list = list->next)
{
xtext_image_pending *walk = list->data;
if (walk->id == handle)
{
pending = walk;
break;
}
}
if (pending == NULL) /* entry, buffer or widget died meanwhile */
return FALSE;
xtext = pending->xtext;
buf = pending->buf;
ent = pending->ent;
image_pending_list = g_slist_remove (image_pending_list, pending);
if (pixbuf == NULL || ent->image != NULL)
{
g_free (pending->url);
g_free (pending);
return FALSE;
}
scaled = gtk_xtext_image_scale (buf, pixbuf);
if (scaled == NULL)
{
g_free (pending->url);
g_free (pending);
return FALSE;
}
ent->image = scaled;
ent->image_full = gtk_xtext_image_scale_max (pixbuf,
XTEXT_IMAGE_FULL_MAX_WIDTH, XTEXT_IMAGE_FULL_MAX_HEIGHT);
ent->image_url = pending->url; /* transferred */
g_free (pending);
gtk_xtext_image_relayout (xtext, buf, ent);
return TRUE;
}
/* remove an entry's inline image again. Returns FALSE if it had none. */
gboolean
gtk_xtext_image_remove (GtkXText *xtext, textentry *ent)
{
if (ent == NULL || ent->image == NULL)
return FALSE;
g_object_unref (ent->image);
ent->image = NULL;
if (ent->image_full)
{
g_object_unref (ent->image_full);
ent->image_full = NULL;
}
g_free (ent->image_url);
ent->image_url = NULL;
gtk_xtext_image_relayout (xtext, xtext->buffer, ent);
return TRUE;
}

View File

@@ -192,7 +192,6 @@ struct _GtkXText
int hilight_end; int hilight_end;
time_t tooltip_stamp; time_t tooltip_stamp;
unsigned int tooltip_stamp_set:1; unsigned int tooltip_stamp_set:1;
char *tooltip_url; /* URL shown while hovering a placeholder */
guint16 fontwidth[128]; /* each char's width, only the ASCII ones */ guint16 fontwidth[128]; /* each char's width, only the ASCII ones */
@@ -255,9 +254,6 @@ struct _GtkXText
unsigned int force_render:1; unsigned int force_render:1;
unsigned int color_paste:1; /* CTRL was pressed when selection finished */ unsigned int color_paste:1; /* CTRL was pressed when selection finished */
textentry *clicked_ent; /* entry under the last word_click, or NULL */
textentry *clicked_image_ent; /* entry whose inline image was clicked */
/* settings/prefs */ /* settings/prefs */
unsigned int auto_indent:1; unsigned int auto_indent:1;
unsigned int thinline:1; unsigned int thinline:1;
@@ -313,19 +309,4 @@ void gtk_xtext_buffer_free (xtext_buffer *buf);
void gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render); void gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render);
void gtk_xtext_copy_selection (GtkXText *xtext); void gtk_xtext_copy_selection (GtkXText *xtext);
/* inline image support. A textentry can have one image attached, which is
* rendered below its text. Loading is asynchronous: begin() registers the
* target entry and returns a handle, finish() attaches the downloaded pixbuf
* if the entry still exists. The handle stays valid even if the entry, its
* buffer or the widget are destroyed in the meantime. */
textentry *gtk_xtext_get_clicked_entry (GtkXText *xtext);
textentry *gtk_xtext_get_clicked_image_entry (GtkXText *xtext);
gboolean gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent);
gboolean gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent);
const char *gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent);
GdkPixbuf *gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent);
guint gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url);
gboolean gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf);
gboolean gtk_xtext_image_remove (GtkXText *xtext, textentry *ent);
#endif #endif

280
tools/gen-emoji-data.py Executable file
View File

@@ -0,0 +1,280 @@
#!/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

@@ -78,11 +78,12 @@
<Gtk3Immodules Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\immodules\**\*" /> <Gtk3Immodules Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\immodules\**\*" />
<Gtk3PrintBackends Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\printbackends\**\*" /> <Gtk3PrintBackends Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\printbackends\**\*" />
<GSettingsSchemas Include="$(DepsRoot)\share\glib-2.0\schemas\*" /> <GSettingsSchemas Include="$(DepsRoot)\share\glib-2.0\schemas\*" />
<GioModules Include="$(DepsRoot)\lib\gio\modules\**\*" />
<GdkPixbufLoaders Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders\**\*" /> <GdkPixbufLoaders Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders\**\*" />
<GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" /> <GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" />
<FontConfig Include="$(DepsRoot)\etc\fonts\*" /> <FontConfig Include="$(DepsRoot)\etc\fonts\*" />
<EmojiFont Include="..\..\data\fonts\NotoColorEmoji.ttf" />
<EmojiFontLicense Include="..\..\data\fonts\OFL.txt" />
<Share Include="share\**\*" /> <Share Include="share\**\*" />
<Locale Include="$(ZoiteChatBin)locale\**\*;$(DepsRoot)\share\locale\**\*" /> <Locale Include="$(ZoiteChatBin)locale\**\*;$(DepsRoot)\share\locale\**\*" />
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" /> <MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
@@ -92,12 +93,13 @@
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" /> <Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
<Copy SourceFiles="@(FontConfig)" DestinationFolder="$(ZoiteChatRel)\etc\fonts" /> <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="@(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="@(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)')" /> <Copy SourceFiles="@(GdkPixbufLoaders)" DestinationFiles="@(GdkPixbufLoaders->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GdkPixbufLoaderCache)" DestinationFiles="@(GdkPixbufLoaderCache->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" /> <Copy SourceFiles="@(GdkPixbufLoaderCache)" DestinationFiles="@(GdkPixbufLoaderCache->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GSettingsSchemas)" DestinationFiles="@(GSettingsSchemas->'$(ZoiteChatRel)\share\glib-2.0\schemas\%(Filename)%(Extension)')" /> <Copy SourceFiles="@(GSettingsSchemas)" DestinationFiles="@(GSettingsSchemas->'$(ZoiteChatRel)\share\glib-2.0\schemas\%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GioModules)" DestinationFiles="@(GioModules->'$(ZoiteChatRel)\lib\gio\modules\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" /> <Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" /> <Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" />
<Copy SourceFiles="$(WinSparklePath)\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\WinSparkle" /> <Copy SourceFiles="$(WinSparklePath)\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\WinSparkle" />

View File

@@ -104,6 +104,7 @@ Source: "readme.url"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "cert.pem"; 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\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\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\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\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 Source: "share\glib-2.0\schemas\*"; DestDir: "{app}\share\glib-2.0\schemas"; Flags: ignoreversion createallsubdirs recursesubdirs skipifsourcedoesntexist; Components: libs
@@ -151,7 +152,6 @@ Source: "tiff.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "plugins\hcnotifications-winrt.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: libs Source: "plugins\hcnotifications-winrt.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: libs
Source: "lib\enchant\*"; DestDir: "{app}\lib\enchant"; Flags: ignoreversion; Components: libs Source: "lib\enchant\*"; DestDir: "{app}\lib\enchant"; Flags: ignoreversion; Components: libs
Source: "lib\gio\modules\*"; DestDir: "{app}\lib\gio\modules"; Flags: ignoreversion recursesubdirs; Components: libs
Source: "girepository-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua Source: "girepository-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua
Source: "lua51.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua Source: "lua51.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua