mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-09-17 13:19:57 +00:00
Compare commits
24 Commits
test-start
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61ff308c9f | ||
| 2672f9b087 | |||
| 4315a75a12 | |||
|
|
15905bebb1 | ||
|
|
fc4a7454ad | ||
| 87410aea50 | |||
|
|
de98d00a1c | ||
| e94ffd9d79 | |||
|
|
d7aebb784d | ||
|
|
20b8cb939d | ||
| ff4217c3d1 | |||
| 4fb72d30d8 | |||
|
|
d2b8fc0bda | ||
| 951181628a | |||
|
|
ccee274847 | ||
| 967f6728ef | |||
|
|
bf11ffb626 | ||
| c12cabbe4e | |||
|
|
e997594db9 | ||
| 6f5e3b395f | |||
| 8bf2caf20d | |||
|
|
040021ee0e | ||
|
|
5018a3a848 | ||
|
|
df1c74a91d |
45
.github/workflows/appimage-build.yml
vendored
45
.github/workflows/appimage-build.yml
vendored
@@ -30,13 +30,13 @@ jobs:
|
||||
gettext \
|
||||
libcanberra-dev libglib2.0-dev \
|
||||
libarchive-dev \
|
||||
libgtk-3-dev \
|
||||
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
|
||||
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
|
||||
libxkbcommon0 \
|
||||
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
|
||||
libgtk-3-bin libglib2.0-bin glib-networking shared-mime-info gsettings-desktop-schemas \
|
||||
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
|
||||
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
|
||||
fonts-noto-color-emoji breeze-gtk-theme \
|
||||
breeze-gtk-theme \
|
||||
patchelf file curl
|
||||
|
||||
- name: Configure
|
||||
@@ -56,6 +56,9 @@ jobs:
|
||||
set -eux
|
||||
ninja -C build
|
||||
|
||||
- name: Test emoji catalog, font coverage and picker
|
||||
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
|
||||
|
||||
- name: Install to AppDir
|
||||
run: |
|
||||
set -eux
|
||||
@@ -119,15 +122,8 @@ jobs:
|
||||
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
|
||||
fi
|
||||
|
||||
if [ -d "/usr/share/gtk-3.0/emoji" ]; then
|
||||
install -d AppDir/usr/share/gtk-3.0
|
||||
cp -a /usr/share/gtk-3.0/emoji AppDir/usr/share/gtk-3.0/
|
||||
fi
|
||||
|
||||
if [ -f "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf" ]; then
|
||||
install -d AppDir/usr/share/fonts/truetype/noto
|
||||
cp -a /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf AppDir/usr/share/fonts/truetype/noto/
|
||||
fi
|
||||
# Every package ships the pinned font, including portable builds.
|
||||
cmp data/fonts/NotoColorEmoji.ttf AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
|
||||
|
||||
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
|
||||
@@ -139,6 +135,20 @@ jobs:
|
||||
cp -a /usr/lib/gtk-3.0/modules AppDir/usr/lib/gtk-3.0/
|
||||
fi
|
||||
|
||||
# HTTPS inline-image downloads use GSocketClient TLS on Linux.
|
||||
# GLib provides the API, but TLS and system-proxy support live in
|
||||
# loadable GIO modules shipped by glib-networking. Bundle those
|
||||
# modules so the AppImage does not depend on the host copy.
|
||||
gio_module_dir="$(pkg-config --variable=giomoduledir gio-2.0)"
|
||||
|
||||
test -n "$gio_module_dir"
|
||||
test -f "${gio_module_dir}/libgiognutls.so"
|
||||
install -d "AppDir${gio_module_dir}"
|
||||
cp -a "${gio_module_dir}"/libgio*.so "AppDir${gio_module_dir}/"
|
||||
gio-querymodules "AppDir${gio_module_dir}"
|
||||
test -f "AppDir${gio_module_dir}/libgiognutls.so"
|
||||
test -s "AppDir${gio_module_dir}/giomodule.cache"
|
||||
|
||||
- name: Verify bundled plugins
|
||||
run: |
|
||||
set -eux
|
||||
@@ -192,14 +202,6 @@ jobs:
|
||||
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
|
||||
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
|
||||
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
|
||||
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
|
||||
@@ -258,6 +260,9 @@ jobs:
|
||||
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
|
||||
fi
|
||||
|
||||
# Register the private font without hiding the host's normal fonts.
|
||||
export ZOITECHAT_EMOJI_FONT="${ZOITECHAT_EMOJI_FONT:-$APPDIR/usr/share/fonts/zoitechat/NotoColorEmoji.ttf}"
|
||||
|
||||
# OpenSSL trust store override
|
||||
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
|
||||
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
|
||||
|
||||
40
.github/workflows/windows-build.yml
vendored
40
.github/workflows/windows-build.yml
vendored
@@ -69,6 +69,30 @@ jobs:
|
||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK3_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
|
||||
Expand-Archive -LiteralPath deps\gtk-${{ matrix.arch }}.zip -DestinationPath C:\gtk-build\gtk\x64\release -Force
|
||||
|
||||
# The GTK3 archive omits librsvg. Add only its renderer, loader and
|
||||
# license from the companion release, keeping the GTK3 DLLs intact.
|
||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK4_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\svg-runtime.zip
|
||||
if ((Get-FileHash deps\svg-runtime.zip -Algorithm SHA256).Hash -ne 'fb657f9ebae3a6807defb15e66091d643a703ee66cb37488e3e11e5646f45adb') {
|
||||
throw 'Unexpected SVG runtime archive checksum'
|
||||
}
|
||||
$svgArchive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path deps\svg-runtime.zip))
|
||||
try {
|
||||
foreach ($name in @('bin/rsvg-2-2.dll', 'lib/gdk-pixbuf-2.0/2.10.0/loaders/pixbufloader_svg.dll', 'share/doc/librsvg/COPYING.LIB')) {
|
||||
$entry = $svgArchive.GetEntry($name)
|
||||
if ($null -eq $entry) { throw "Missing SVG runtime file: $name" }
|
||||
$destination = Join-Path 'C:\gtk-build\gtk\x64\release' $name
|
||||
New-Item -ItemType Directory -Path (Split-Path $destination) -Force | Out-Null
|
||||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destination, $true)
|
||||
}
|
||||
}
|
||||
finally { $svgArchive.Dispose() }
|
||||
# This loads the module now, catching missing DLLs before the build.
|
||||
& C:\gtk-build\gtk\x64\release\bin\gdk-pixbuf-query-loaders.exe --update-cache
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not generate the pixbuf loader cache' }
|
||||
if (-not (Select-String -LiteralPath 'C:\gtk-build\gtk\x64\release\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache' -SimpleMatch '"svg"')) {
|
||||
throw 'SVG loader could not be loaded with the GTK3 runtime'
|
||||
}
|
||||
|
||||
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
|
||||
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\hicolor-icon-theme.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith('mingw64/share/icons/hicolor/')];tf.close();reader.close();f.close()"
|
||||
|
||||
@@ -174,6 +198,22 @@ jobs:
|
||||
)
|
||||
shell: cmd
|
||||
|
||||
- name: Test emoji catalog, font coverage and picker
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
|
||||
set "PATH=%CD%\..\zoitechat-build\x64\rel;%PATH%"
|
||||
set "GDK_PIXBUF_MODULE_FILE=%CD%\..\zoitechat-build\x64\rel\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache"
|
||||
set "ZOITECHAT_EMOJI_FONT=%CD%\..\zoitechat-build\x64\rel\share\fonts\zoitechat\NotoColorEmoji.ttf"
|
||||
fc /b data\fonts\NotoColorEmoji.ttf "%ZOITECHAT_EMOJI_FONT%"
|
||||
if errorlevel 1 exit /b 1
|
||||
for %%T in (data font ui) do (
|
||||
msbuild win32\emoji-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64 /p:EmojiTestKind=%%T
|
||||
if errorlevel 1 exit /b 1
|
||||
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
|
||||
if errorlevel 1 exit /b 1
|
||||
)
|
||||
|
||||
- name: Preparing Artifacts
|
||||
run: |
|
||||
move ..\zoitechat-build\${{ matrix.platform }}\ZoiteChat-*.exe .\
|
||||
|
||||
23
COPYING
23
COPYING
@@ -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
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
|
||||
|
||||
======================================================================
|
||||
ADDITIONAL LICENSE NOTICE
|
||||
======================================================================
|
||||
|
||||
ZoiteChat source code:
|
||||
GPL version 2
|
||||
See Above.
|
||||
|
||||
Unicode Emoji and CLDR data:
|
||||
Unicode License v3
|
||||
data/emoji/LICENSE-UNICODE.txt
|
||||
|
||||
gemoji shortcode data:
|
||||
MIT License
|
||||
Copyright (c) 2019 GitHub, Inc.
|
||||
data/emoji/LICENSE-GEMOJI.txt
|
||||
|
||||
Noto Color Emoji:
|
||||
SIL Open Font License 1.1
|
||||
data/fonts/OFL.txt
|
||||
|
||||
39
data/emoji/LICENSE-UNICODE.txt
Normal file
39
data/emoji/LICENSE-UNICODE.txt
Normal 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
30
data/emoji/README.md
Normal 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
6
data/emoji/VERSIONS
Normal 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
|
||||
4749
data/emoji/cldr-annotations-derived-en.xml
Normal file
4749
data/emoji/cldr-annotations-derived-en.xml
Normal file
File diff suppressed because it is too large
Load Diff
3957
data/emoji/cldr-annotations-en.xml
Normal file
3957
data/emoji/cldr-annotations-en.xml
Normal file
File diff suppressed because it is too large
Load Diff
5518
data/emoji/emoji-test.txt
Normal file
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
23479
data/emoji/gemoji.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
data/fonts/NotoColorEmoji.ttf
Normal file
BIN
data/fonts/NotoColorEmoji.ttf
Normal file
Binary file not shown.
91
data/fonts/OFL.txt
Normal file
91
data/fonts/OFL.txt
Normal 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.
|
||||
43
data/fonts/README.md
Normal file
43
data/fonts/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 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.
|
||||
|
||||
Every GTK build installs this exact file: Windows (installer and portable
|
||||
build files), AppImage, Flatpak and native Meson/RPM builds. It is loaded
|
||||
into a private Pango/FreeType font map as `ZoiteChat Emoji`; an older system
|
||||
Noto font cannot replace it. No font is installed globally on Windows.
|
||||
|
||||
Chat windows and the emoji picker share this map. Fontconfig still loads
|
||||
system fonts and configuration for ordinary text, and the configured text
|
||||
font, size and style remain in effect. In particular, plain digits, `#`
|
||||
and `*` are not assigned emoji metrics.
|
||||
|
||||
Windows uses the same FreeType backend rather than GDI registration or an
|
||||
OS-version-specific picker. The dependency bundle already ships FreeType,
|
||||
Fontconfig and PangoFT2. This rendering path does not require Windows' native
|
||||
color-font support. Actual Windows 7 execution still needs testing on that OS.
|
||||
|
||||
Meson requires PangoFT2 >= 1.44 and Fontconfig >= 2.13. Normal installations
|
||||
load `share/fonts/zoitechat/NotoColorEmoji.ttf` beneath the configured data
|
||||
directory; Windows resolves it relative to the executable. AppImage sets
|
||||
`ZOITECHAT_EMOJI_FONT` to its own copy. For an uninstalled development build:
|
||||
|
||||
```sh
|
||||
ZOITECHAT_EMOJI_FONT="$PWD/data/fonts/NotoColorEmoji.ttf" build/src/fe-gtk/zoitechat
|
||||
```
|
||||
|
||||
A missing/unloadable file logs a warning and retains system-font fallback.
|
||||
CI verifies the installed copy and tests catalog/font coverage; AppImage and
|
||||
Windows also run the interactive picker regression tests. Native themes,
|
||||
DPI and different Pango/Cairo versions can still affect pixel-level output.
|
||||
|
||||
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
13
data/fonts/meson.build
Normal 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,
|
||||
)
|
||||
@@ -3,6 +3,7 @@ if get_option('plugin')
|
||||
endif
|
||||
|
||||
if get_option('gtk-frontend')
|
||||
subdir('fonts')
|
||||
subdir('icons')
|
||||
subdir('misc')
|
||||
subdir('man')
|
||||
|
||||
@@ -2,6 +2,7 @@ appdir = join_paths(get_option('datadir'), 'applications')
|
||||
metainfodir = join_paths(get_option('datadir'), 'metainfo')
|
||||
mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
|
||||
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||
appstreamcli = find_program('appstreamcli', required: false)
|
||||
|
||||
if get_option('gtk-frontend')
|
||||
if get_option('install-appdata')
|
||||
@@ -13,7 +14,6 @@ if get_option('gtk-frontend')
|
||||
install_dir: metainfodir
|
||||
)
|
||||
|
||||
appstreamcli = find_program('appstreamcli', required: false)
|
||||
if appstreamcli.found()
|
||||
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
|
||||
args: ['validate', zoitechat_appdata]
|
||||
|
||||
@@ -29,6 +29,22 @@
|
||||
<id>zoitechat.desktop</id>
|
||||
</provides>
|
||||
<releases>
|
||||
<release date="2026-09-15" version="2.19.1">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed /msg messages showing twice on servers using IRCv3 <code>echo-message</code>.</li>
|
||||
<li>Fixed escaping a leading command slash so <code>//text</code> sends <code>/text</code> as normal chat.</li>
|
||||
<li>Fixed automatic away clearing when sending messages on servers using <code>echo-message</code>.</li>
|
||||
<li>Fixed other users' away status not updating until rejoining a channel when <code>away-notify</code> updates are missed.</li>
|
||||
<li>Fixed private messages and <code>/me</code> actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.</li>
|
||||
<li>Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.</li>
|
||||
<li>Fixed optional Windows 10 theme downloads in the Windows installer.</li>
|
||||
<li>Fixed the GTK warning when switching from channel tabs to the channel tree.</li>
|
||||
<li>Fixed GTK3 theme archive root detection so themes with an <code>index.theme</code> file are preferred correctly.</li>
|
||||
<li>Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2026-07-28" version="2.19.0">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
ZoiteChat ChangeLog
|
||||
=================
|
||||
|
||||
2.19.1 (2026-09-15)
|
||||
-------------------
|
||||
|
||||
- Fixed ``/msg`` messages showing twice on servers using IRCv3 ``echo-message``.
|
||||
- Fixed escaping a leading command slash so ``//text`` sends ``/text`` as normal chat.
|
||||
- Fixed automatic away clearing when sending messages on servers using ``echo-message``.
|
||||
- Fixed other users' away status not updating until rejoining a channel when ``away-notify`` updates are missed.
|
||||
- Fixed private messages and ``/me`` actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.
|
||||
- Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.
|
||||
- Fixed optional Windows 10 theme downloads in the Windows installer.
|
||||
- Fixed the GTK warning when switching from channel tabs to the channel tree.
|
||||
- Fixed GTK3 theme archive root detection so themes with an ``index.theme`` file are preferred correctly.
|
||||
- Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.
|
||||
|
||||
2.19.0 (2026-07-28)
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
{
|
||||
"name": "zoitechat",
|
||||
"buildsystem": "meson",
|
||||
"run-tests": true,
|
||||
"test-rule": "",
|
||||
"test-commands": [
|
||||
"meson test --print-errorlogs 'Emoji Data Tests' 'Emoji Font Tests'"
|
||||
],
|
||||
"config-opts": [
|
||||
"-Ddbus-service-use-appid=true",
|
||||
"-Dwith-perl=perl",
|
||||
@@ -65,7 +70,8 @@
|
||||
"/share/man"
|
||||
],
|
||||
"post-install": [
|
||||
"install -d /app/extensions"
|
||||
"install -d /app/extensions",
|
||||
"cmp ../data/fonts/NotoColorEmoji.ttf /app/share/fonts/zoitechat/NotoColorEmoji.ttf"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
project('zoitechat', 'c',
|
||||
version: run_command('cat', 'VERSION', check: true).stdout().strip(),
|
||||
meson_version: '>= 0.55.0',
|
||||
meson_version: '>= 0.59.0',
|
||||
default_options: [
|
||||
'c_std=c17',
|
||||
'buildtype=debugoptimized',
|
||||
@@ -36,6 +36,7 @@ config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat')
|
||||
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
|
||||
get_option('datadir'), 'locale'))
|
||||
config_h.set10('ENABLE_NLS', true)
|
||||
config_h.set_quoted('ZOITECHAT_DATADIR', join_paths(get_option('prefix'), get_option('datadir')))
|
||||
|
||||
# Optional features
|
||||
config_h.set('USE_OPENSSL', libssl_dep.found())
|
||||
|
||||
@@ -23,6 +23,8 @@ BuildRequires: pkgconfig(dbus-glib-1)
|
||||
BuildRequires: pkgconfig(gio-2.0) >= 2.36.0
|
||||
BuildRequires: pkgconfig(gmodule-2.0)
|
||||
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
|
||||
BuildRequires: pkgconfig(fontconfig) >= 2.13
|
||||
BuildRequires: pkgconfig(pangoft2) >= 1.44
|
||||
BuildRequires: pkgconfig(iso-codes)
|
||||
BuildRequires: pkgconfig(libarchive)
|
||||
BuildRequires: pkgconfig(libcanberra) >= 0.22
|
||||
@@ -72,6 +74,9 @@ appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite
|
||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
||||
|
||||
xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_build_ncpus} --print-errorlogs \
|
||||
"Emoji Data Tests" \
|
||||
"Emoji Font Tests" \
|
||||
"Emoji UI Tests" \
|
||||
"Theme Manager Dispatch Routing Tests" \
|
||||
"Validate net.zoite.Zoitechat.desktop" \
|
||||
"Validate translations" \
|
||||
@@ -80,6 +85,8 @@ xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_bui
|
||||
%files -f %{name}.lang
|
||||
%license COPYING
|
||||
%doc readme.md troubleshooting.md
|
||||
%{_datadir}/fonts/zoitechat/
|
||||
%{_datadir}/doc/zoitechat/fonts/
|
||||
%{_bindir}/zoitechat
|
||||
%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
||||
%{_datadir}/dbus-1/services/org.zoitechat.service.service
|
||||
|
||||
@@ -26,7 +26,7 @@ if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system =
|
||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||
sysinfo_sources += 'unix/pci.c'
|
||||
|
||||
picidsdir = libpci.get_pkgconfig_variable('idsdir')
|
||||
picidsdir = libpci.get_variable(pkgconfig : 'idsdir')
|
||||
pciids = join_paths(picidsdir, 'pci.ids')
|
||||
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
||||
endif
|
||||
|
||||
@@ -4,6 +4,7 @@ data/misc/net.zoite.Zoitechat.ThemeManager.desktop.in
|
||||
src/common/cfgfiles.c
|
||||
src/common/chanopt.c
|
||||
src/common/dcc.c
|
||||
src/common/emoji-data.c
|
||||
src/common/zoitechat.c
|
||||
src/common/ignore.c
|
||||
src/common/inbound.c
|
||||
@@ -24,10 +25,12 @@ src/fe-gtk/banlist.c
|
||||
src/fe-gtk/chanlist.c
|
||||
src/fe-gtk/dccgui.c
|
||||
src/fe-gtk/editlist.c
|
||||
src/fe-gtk/emoji-picker.c
|
||||
src/fe-gtk/fe-gtk.c
|
||||
src/fe-gtk/fkeys.c
|
||||
src/fe-gtk/gtkutil.c
|
||||
src/fe-gtk/ignoregui.c
|
||||
src/fe-gtk/inline-image.c
|
||||
src/fe-gtk/joind.c
|
||||
src/fe-gtk/maingui.c
|
||||
src/fe-gtk/menu.c
|
||||
|
||||
@@ -416,6 +416,7 @@ const struct prefs vars[] =
|
||||
{"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT},
|
||||
{"gui_dialog_top", P_OFFINT (hex_gui_dialog_top), TYPE_INT},
|
||||
{"gui_dialog_width", P_OFFINT (hex_gui_dialog_width), TYPE_INT},
|
||||
{"gui_emoji_skin_tone", P_OFFINT (hex_gui_emoji_skin_tone), TYPE_INT},
|
||||
{"gui_filesize_iec", P_OFFINT (hex_gui_filesize_iec), TYPE_BOOL},
|
||||
{"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL},
|
||||
{"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL},
|
||||
@@ -442,6 +443,7 @@ const struct prefs vars[] =
|
||||
{"gui_tab_chans", P_OFFINT (hex_gui_tab_chans), TYPE_BOOL},
|
||||
{"gui_tab_dialogs", P_OFFINT (hex_gui_tab_dialogs), TYPE_BOOL},
|
||||
{"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL},
|
||||
{"gui_tab_font", P_OFFSET (hex_gui_tab_font), TYPE_STR},
|
||||
{"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL},
|
||||
{"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_INT},
|
||||
{"gui_gtk3_variant", P_OFFINT (hex_gui_gtk3_variant), TYPE_INT},
|
||||
@@ -564,6 +566,7 @@ const struct prefs vars[] =
|
||||
{"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT},
|
||||
{"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR},
|
||||
{"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},
|
||||
|
||||
{"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT},
|
||||
@@ -777,6 +780,8 @@ load_default_config(void)
|
||||
prefs.hex_gui_hide_menu = 1;
|
||||
#endif
|
||||
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_icon = 1;
|
||||
prefs.hex_gui_input_nick = 1;
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
<ClCompile Include="chanopt.c" />
|
||||
<ClCompile Include="ctcp.c" />
|
||||
<ClCompile Include="dcc.c" />
|
||||
<ClCompile Include="emoji-data.c" />
|
||||
<ClCompile Include="history.c" />
|
||||
<ClCompile Include="plugin-identd.c" />
|
||||
<ClCompile Include="ignore.c" />
|
||||
@@ -113,6 +114,8 @@
|
||||
SET SOLUTIONDIR=$(SolutionDir)..\
|
||||
"$(Python3Path)\python.exe" $(ProjectDir)make-te.py "$(ProjectDir)textevents.in" "$(ZoiteChatLib)textevents.h" "$(ZoiteChatLib)textenums.h"
|
||||
"$(Python3Path)\python.exe" $(ProjectDir)gen-public-suffix.py "$(ZoiteChatLib)public_suffix_data.h"
|
||||
"$(Python3Path)\python.exe" "$(SolutionDir)..\tools\gen-emoji-data.py" "$(SolutionDir)..\data\emoji\emoji-test.txt" "$(SolutionDir)..\data\emoji\cldr-annotations-en.xml" "$(SolutionDir)..\data\emoji\cldr-annotations-derived-en.xml" "$(SolutionDir)..\data\emoji\gemoji.json" "$(SolutionDir)..\data\emoji\VERSIONS" "$(ZoiteChatLib)emoji-data-table.h"
|
||||
if errorlevel 1 exit /b 1
|
||||
powershell -File "$(SolutionDir)..\win32\version-template.ps1" "$(SolutionDir)..\win32\config.h.tt" "$(ZoiteChatLib)config.h"
|
||||
$(GlibGenMarshal) --prefix=_zoitechat_marshal --header "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.h"
|
||||
$(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c"
|
||||
|
||||
239
src/common/emoji-data.c
Normal file
239
src/common/emoji-data.c
Normal 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 ((guint) 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 ((guint) 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 ((guint) 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 || (guint) 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 || !g_utf8_validate (text, -1, NULL))
|
||||
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
91
src/common/emoji-data.h
Normal 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
|
||||
@@ -858,8 +858,10 @@ path_depth_from_root (const char *base, const char *path)
|
||||
static gint
|
||||
theme_root_candidate_compare (gconstpointer a, gconstpointer b)
|
||||
{
|
||||
const ThemeRootCandidate *ca = a;
|
||||
const ThemeRootCandidate *cb = b;
|
||||
/* g_ptr_array_sort() passes pointers to the array slots, not the values
|
||||
* stored in those slots. Dereference once before comparing candidates. */
|
||||
const ThemeRootCandidate *ca = *(ThemeRootCandidate * const *) a;
|
||||
const ThemeRootCandidate *cb = *(ThemeRootCandidate * const *) b;
|
||||
if (ca->has_index_theme != cb->has_index_theme)
|
||||
return ca->has_index_theme ? -1 : 1;
|
||||
if (ca->depth != cb->depth)
|
||||
|
||||
@@ -3,6 +3,7 @@ common_sources = [
|
||||
'chanopt.c',
|
||||
'ctcp.c',
|
||||
'dcc.c',
|
||||
'emoji-data.c',
|
||||
'gtk3-theme-service.c',
|
||||
'zoitechat.c',
|
||||
'history.c',
|
||||
@@ -109,6 +110,20 @@ public_suffix_data = custom_target('public_suffix_data_h',
|
||||
command: [python3, files('gen-public-suffix.py'), '@OUTPUT@']
|
||||
)
|
||||
|
||||
emoji_data_table = custom_target('emoji_data_table_h',
|
||||
input: [
|
||||
'../../data/emoji/emoji-test.txt',
|
||||
'../../data/emoji/cldr-annotations-en.xml',
|
||||
'../../data/emoji/cldr-annotations-derived-en.xml',
|
||||
'../../data/emoji/gemoji.json',
|
||||
'../../data/emoji/VERSIONS',
|
||||
],
|
||||
output: 'emoji-data-table.h',
|
||||
command: [python3, files('../../tools/gen-emoji-data.py'),
|
||||
'@INPUT0@', '@INPUT1@', '@INPUT2@', '@INPUT3@', '@INPUT4@',
|
||||
'@OUTPUT@']
|
||||
)
|
||||
|
||||
textevents = custom_target('textevents',
|
||||
input: 'textevents.in',
|
||||
output: ['textevents.h', 'textenums.h'],
|
||||
@@ -132,7 +147,7 @@ if get_option('plugin')
|
||||
endif
|
||||
|
||||
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,
|
||||
dependencies: common_deps + common_sysinfo_deps,
|
||||
c_args: common_cflags,
|
||||
@@ -167,3 +182,18 @@ test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
emoji_data_tests = executable('emoji_data_tests',
|
||||
[
|
||||
'tests/test-emoji-data.c',
|
||||
'emoji-data.c',
|
||||
emoji_data_table,
|
||||
],
|
||||
include_directories: [config_h_include, include_directories('.')],
|
||||
dependencies: [libgio_dep],
|
||||
)
|
||||
|
||||
test('Emoji Data Tests', emoji_data_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
243
src/common/tests/test-emoji-data.c
Normal file
243
src/common/tests/test-emoji-data.c
Normal file
@@ -0,0 +1,243 @@
|
||||
/* 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_cmpstr (emoji_entry_sequence_for_tone (thumbs, (EmojiTone) -1), ==, thumbs->sequence);
|
||||
g_assert_cmpstr (emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_COUNT), ==, 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 (":::"));
|
||||
g_assert_null (emoji_search_tokenize ("\377"));
|
||||
}
|
||||
|
||||
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 ();
|
||||
}
|
||||
@@ -463,8 +463,7 @@ doover:
|
||||
while (list)
|
||||
{
|
||||
sess = list->data;
|
||||
if (!sess->server->have_awaynotify)
|
||||
sess->done_away_check = FALSE;
|
||||
sess->done_away_check = FALSE;
|
||||
list = list->next;
|
||||
}
|
||||
loop++;
|
||||
|
||||
@@ -204,6 +204,7 @@ struct zoitechatprefs
|
||||
unsigned int hex_net_auto_reconnect;
|
||||
unsigned int hex_net_auto_reconnectonfail;
|
||||
unsigned int hex_net_proxy_auth;
|
||||
unsigned int hex_net_remote_media;
|
||||
unsigned int hex_net_throttle;
|
||||
unsigned int hex_notify_whois_online;
|
||||
unsigned int hex_perl_warnings;
|
||||
@@ -260,6 +261,7 @@ struct zoitechatprefs
|
||||
int hex_gui_dialog_left;
|
||||
int hex_gui_dialog_top;
|
||||
int hex_gui_dialog_width;
|
||||
int hex_gui_emoji_skin_tone;
|
||||
int hex_gui_lagometer;
|
||||
int hex_gui_lang;
|
||||
int hex_gui_pane_divider_position;
|
||||
@@ -313,6 +315,7 @@ struct zoitechatprefs
|
||||
char hex_dcc_completed_dir[PATHLEN + 1];
|
||||
char hex_dcc_dir[PATHLEN + 1];
|
||||
char hex_dcc_ip[DOMAINLEN + 1];
|
||||
char hex_gui_tab_font[FONTNAMELEN + 1];
|
||||
char hex_gui_ulist_doubleclick[256];
|
||||
char hex_input_command_char[4];
|
||||
char hex_irc_extra_hilight[300];
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
/* file included in chanview.c */
|
||||
|
||||
#include "theme/theme-css.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkWidget *outer; /* outer box */
|
||||
@@ -690,6 +692,7 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
||||
GtkWidget *label;
|
||||
GtkWidget *close_button;
|
||||
GtkWidget *close_icon;
|
||||
GtkCssProvider *close_css;
|
||||
|
||||
but = gtk_toggle_button_new ();
|
||||
gtk_widget_set_name (but, "zoitechat-tab");
|
||||
@@ -698,9 +701,27 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
||||
cv_add_scroll_events (but);
|
||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
|
||||
label = gtk_label_new (name);
|
||||
if (prefs.hex_gui_tab_font[0])
|
||||
{
|
||||
PangoFontDescription *font_desc;
|
||||
|
||||
/* Keep activity colours and the Smaller text scale in the label attributes. */
|
||||
font_desc = pango_font_description_from_string (prefs.hex_gui_tab_font);
|
||||
gtkutil_apply_palette (label, NULL, NULL, font_desc);
|
||||
pango_font_description_free (font_desc);
|
||||
}
|
||||
close_button = gtk_button_new ();
|
||||
cv_add_scroll_events (close_button);
|
||||
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "flat");
|
||||
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "zoitechat-tab-close");
|
||||
/* Theme button minimums and padding must not determine the tab height. */
|
||||
close_css = gtk_css_provider_new ();
|
||||
gtk_css_provider_load_from_data (close_css,
|
||||
".zoitechat-tab-close { min-width: 0; min-height: 0; padding: 1px; margin: 0; }",
|
||||
-1, NULL);
|
||||
theme_css_apply_widget_provider (close_button, GTK_STYLE_PROVIDER (close_css));
|
||||
g_object_unref (close_css);
|
||||
gtk_widget_set_valign (close_button, GTK_ALIGN_CENTER);
|
||||
close_icon = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
|
||||
gtk_image_set_pixel_size (GTK_IMAGE (close_icon), 8);
|
||||
gtk_button_set_always_show_image (GTK_BUTTON (close_button), TRUE);
|
||||
|
||||
@@ -301,26 +301,35 @@ cv_tree_focus (chan *ch)
|
||||
path = gtk_tree_model_get_path (model, &ch->iter);
|
||||
if (path)
|
||||
{
|
||||
/* This full section does what
|
||||
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||
* does, except it only scrolls the window if the provided cell is
|
||||
* not visible. Basic algorithm taken from gtktreeview.c */
|
||||
|
||||
/* obtain information to see if the cell is visible */
|
||||
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
|
||||
gtk_tree_view_get_visible_rect (tree, &vis_rect);
|
||||
|
||||
/* The cordinates aren't offset correctly */
|
||||
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
|
||||
|
||||
/* only need to scroll if out of bounds */
|
||||
if (cell_rect.y < vis_rect.y ||
|
||||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
|
||||
if (!gtk_widget_get_realized (GTK_WIDGET (tree)))
|
||||
{
|
||||
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
|
||||
if (dest_y < 0)
|
||||
dest_y = 0;
|
||||
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
|
||||
/* Layout changes can restore focus before the new tree is realized.
|
||||
* Let GTK track the row and scroll once its geometry is available. */
|
||||
gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* This full section does what
|
||||
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||
* does, except it only scrolls the window if the provided cell is
|
||||
* not visible. Basic algorithm taken from gtktreeview.c */
|
||||
|
||||
/* obtain information to see if the cell is visible */
|
||||
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
|
||||
gtk_tree_view_get_visible_rect (tree, &vis_rect);
|
||||
|
||||
/* The cordinates aren't offset correctly */
|
||||
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
|
||||
|
||||
/* only need to scroll if out of bounds */
|
||||
if (cell_rect.y < vis_rect.y ||
|
||||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
|
||||
{
|
||||
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
|
||||
if (dest_y < 0)
|
||||
dest_y = 0;
|
||||
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
|
||||
}
|
||||
}
|
||||
/* theft done, now make it focused like */
|
||||
gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
|
||||
|
||||
117
src/fe-gtk/emoji-font.c
Normal file
117
src/fe-gtk/emoji-font.c
Normal file
@@ -0,0 +1,117 @@
|
||||
/* ZoiteChat -- shared, process-private emoji font rendering.
|
||||
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||
|
||||
#include "fe-gtk.h"
|
||||
#include "emoji-font.h"
|
||||
|
||||
#include <fontconfig/fontconfig.h>
|
||||
#include <pango/pangocairo.h>
|
||||
#include <pango/pangofc-fontmap.h>
|
||||
#ifdef G_OS_WIN32
|
||||
#include <glib/gwin32.h>
|
||||
#endif
|
||||
|
||||
static char *
|
||||
emoji_font_path (void)
|
||||
{
|
||||
const char *override = g_getenv ("ZOITECHAT_EMOJI_FONT");
|
||||
|
||||
if (override && *override)
|
||||
return g_strdup (override);
|
||||
|
||||
#ifdef G_OS_WIN32
|
||||
{
|
||||
char *base = g_win32_get_package_installation_directory_of_module (NULL);
|
||||
char *path;
|
||||
|
||||
if (!base)
|
||||
return NULL;
|
||||
path = g_build_filename (base, "share", "fonts", "zoitechat",
|
||||
"NotoColorEmoji.ttf", NULL);
|
||||
g_free (base);
|
||||
return path;
|
||||
}
|
||||
#else
|
||||
return g_build_filename (ZOITECHAT_DATADIR, "fonts", "zoitechat",
|
||||
"NotoColorEmoji.ttf", NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
PangoFontMap *
|
||||
emoji_font_get_map (void)
|
||||
{
|
||||
static PangoFontMap *map;
|
||||
static gboolean attempted;
|
||||
FcConfig *config;
|
||||
char *path, *escaped, *rules;
|
||||
gboolean registered;
|
||||
|
||||
/* GTK widgets are used on the main thread. Keep the map alive for every
|
||||
* layout's lifetime, including detached chat windows and shutdown. */
|
||||
if (attempted)
|
||||
return map;
|
||||
attempted = TRUE;
|
||||
|
||||
path = emoji_font_path ();
|
||||
if (!path || !g_file_test (path, G_FILE_TEST_IS_REGULAR))
|
||||
{
|
||||
g_warning ("Bundled emoji font is missing: %s", path ? path : "(unknown path)");
|
||||
g_free (path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Keep the system configuration for normal fonts and user preferences.
|
||||
* Give ONLY our application font a private family so an older system
|
||||
* Noto cannot win the match. Never install fonts globally or override
|
||||
* digits, '#' or '*' in the widget's ordinary text font. */
|
||||
config = FcInitLoadConfigAndFonts ();
|
||||
if (!config)
|
||||
{
|
||||
g_warning ("Unable to load font configuration for emoji rendering");
|
||||
g_free (path);
|
||||
return NULL;
|
||||
}
|
||||
escaped = g_markup_escape_text (path, -1);
|
||||
rules = g_strdup_printf (
|
||||
"<fontconfig>"
|
||||
"<match target='scan'><test name='file'><string>%s</string></test>"
|
||||
"<edit name='family' mode='assign'><string>" ZOITECHAT_EMOJI_FAMILY
|
||||
"</string></edit></match>"
|
||||
"<match target='pattern'><test name='family'><string>emoji</string></test>"
|
||||
/* System rules may already have prepended older emoji families.
|
||||
* Put our bundled family before those, not just before 'emoji'. */
|
||||
"<edit name='family' mode='prepend_first' binding='strong'><string>"
|
||||
ZOITECHAT_EMOJI_FAMILY "</string></edit></match>"
|
||||
"</fontconfig>", escaped);
|
||||
registered = FcConfigParseAndLoadFromMemory (config, (const FcChar8 *) rules, FcTrue) &&
|
||||
FcConfigAppFontAddFile (config, (const FcChar8 *) path);
|
||||
g_free (rules);
|
||||
g_free (escaped);
|
||||
g_free (path);
|
||||
|
||||
if (registered)
|
||||
{
|
||||
/* GDI cannot load Noto's CBDT/CBLC color glyphs. FreeType works on
|
||||
* Windows 7 too, and is shipped in all of our GTK dependency bundles. */
|
||||
map = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
|
||||
if (map && PANGO_IS_FC_FONT_MAP (map))
|
||||
pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (map), config);
|
||||
else
|
||||
g_clear_object (&map);
|
||||
}
|
||||
FcConfigDestroy (config);
|
||||
if (!map)
|
||||
g_warning ("Unable to initialize bundled emoji rendering; using system fonts");
|
||||
return map;
|
||||
}
|
||||
|
||||
void
|
||||
emoji_font_apply (GtkWidget *widget)
|
||||
{
|
||||
PangoFontMap *map;
|
||||
|
||||
g_return_if_fail (GTK_IS_WIDGET (widget));
|
||||
map = emoji_font_get_map ();
|
||||
if (map && gtk_widget_get_font_map (widget) != map)
|
||||
gtk_widget_set_font_map (widget, map);
|
||||
}
|
||||
16
src/fe-gtk/emoji-font.h
Normal file
16
src/fe-gtk/emoji-font.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef ZOITECHAT_EMOJI_FONT_H
|
||||
#define ZOITECHAT_EMOJI_FONT_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
/* Application-private family, distinct from any system Noto version. */
|
||||
#define ZOITECHAT_EMOJI_FAMILY "ZoiteChat Emoji"
|
||||
|
||||
/* Use the shared FreeType font map without changing the widget's font/style.
|
||||
* Children inherit it. Call before creating layouts for the widget. */
|
||||
void emoji_font_apply (GtkWidget *widget);
|
||||
|
||||
/* Borrowed reference, or NULL if registration failed. Also usable headlessly. */
|
||||
PangoFontMap *emoji_font_get_map (void);
|
||||
|
||||
#endif
|
||||
828
src/fe-gtk/emoji-picker.c
Normal file
828
src/fe-gtk/emoji-picker.c
Normal file
@@ -0,0 +1,828 @@
|
||||
/* 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 "emoji-font.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 void emoji_search_stop_cb (GtkSearchEntry *entry, gpointer userdata);
|
||||
|
||||
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. The private
|
||||
* font map resolves ZoiteChat Emoji to the same bundled file everywhere. */
|
||||
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: \"ZoiteChat Emoji\", \"Noto Color Emoji\","
|
||||
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
|
||||
" font-size: 15pt;"
|
||||
" padding: 2px;"
|
||||
"}"
|
||||
".zc-emoji-cell-small {"
|
||||
" font-family: \"ZoiteChat Emoji\", \"Noto Color 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
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/* GtkEntry can select all text when focused; preserve its selection and
|
||||
* editability instead of temporarily making a read-only entry writable. */
|
||||
static void
|
||||
emoji_picker_focus_target (EmojiPicker *picker)
|
||||
{
|
||||
GtkEditable *editable;
|
||||
int start, end, pos;
|
||||
gboolean selected;
|
||||
|
||||
if (!picker->target)
|
||||
return;
|
||||
/* Closing a window also closes its popovers. Moving focus during that
|
||||
* teardown can make GtkWindow reference an already destroyed widget. */
|
||||
if (gtk_widget_in_destruction (picker->target) ||
|
||||
gtk_widget_in_destruction (gtk_widget_get_toplevel (picker->target)))
|
||||
return;
|
||||
editable = GTK_EDITABLE (picker->target);
|
||||
pos = gtk_editable_get_position (editable);
|
||||
selected = gtk_editable_get_selection_bounds (editable, &start, &end);
|
||||
gtk_widget_grab_focus (picker->target);
|
||||
if (selected)
|
||||
gtk_editable_select_region (editable, pos == start ? end : start, pos);
|
||||
else
|
||||
gtk_editable_set_position (editable, pos);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_picker_insert (EmojiPicker *picker, const char *sequence)
|
||||
{
|
||||
GtkEditable *editable;
|
||||
int pos, start, end, max_length, length;
|
||||
|
||||
if (!picker->target || !sequence || !*sequence)
|
||||
return;
|
||||
editable = GTK_EDITABLE (picker->target);
|
||||
if (!gtk_editable_get_editable (editable))
|
||||
return;
|
||||
|
||||
/* GtkEntry otherwise truncates at a code point, possibly leaving half
|
||||
* a flag, a dangling joiner or a skin-tone modifier. Reject as a unit. */
|
||||
max_length = gtk_entry_get_max_length (GTK_ENTRY (editable));
|
||||
length = gtk_entry_get_text_length (GTK_ENTRY (editable));
|
||||
if (gtk_editable_get_selection_bounds (editable, &start, &end))
|
||||
length -= end - start;
|
||||
if (length + g_utf8_strlen (sequence, -1) > (max_length ? max_length : 65535))
|
||||
{
|
||||
gtk_widget_error_bell (picker->target);
|
||||
return;
|
||||
}
|
||||
|
||||
gtk_editable_delete_selection (editable);
|
||||
pos = gtk_editable_get_position (editable);
|
||||
gtk_editable_insert_text (editable, sequence, strlen (sequence), &pos);
|
||||
gtk_editable_set_position (editable, pos);
|
||||
emoji_recents_add (sequence);
|
||||
gtk_popover_popdown (GTK_POPOVER (picker->popover));
|
||||
emoji_picker_focus_target (picker);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/* search-changed is delayed by GTK; Enter must use the current query. */
|
||||
emoji_picker_refresh (picker);
|
||||
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)
|
||||
{
|
||||
emoji_picker_refresh (picker);
|
||||
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), "stop-search",
|
||||
G_CALLBACK (emoji_search_stop_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), "");
|
||||
}
|
||||
|
||||
gtk_widget_grab_focus (picker->search_entry);
|
||||
emoji_picker_sync_tone_buttons (picker);
|
||||
emoji_picker_refresh (picker);
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_search_stop_cb (GtkSearchEntry *entry, gpointer userdata)
|
||||
{
|
||||
EmojiPicker *picker = userdata;
|
||||
|
||||
gtk_popover_popdown (GTK_POPOVER (picker->popover));
|
||||
}
|
||||
|
||||
static void
|
||||
emoji_entry_insert_cb (GtkEntry *entry, GtkWidget *button)
|
||||
{
|
||||
/* Covers GtkEntry's context menu and Ctrl+period/Ctrl+semicolon. */
|
||||
g_signal_stop_emission_by_name (entry, "insert-emoji");
|
||||
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), 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 ();
|
||||
emoji_font_apply (picker->button);
|
||||
gtk_button_set_relief (GTK_BUTTON (picker->button), GTK_RELIEF_NONE);
|
||||
gtk_widget_set_can_focus (picker->button, TRUE);
|
||||
gtk_widget_set_focus_on_click (picker->button, FALSE);
|
||||
gtk_widget_set_tooltip_text (picker->button, _("Insert Emoji"));
|
||||
emoji_picker_set_a11y_name (picker->button, _("Insert Emoji"));
|
||||
|
||||
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);
|
||||
emoji_font_apply (picker->popover);
|
||||
g_object_set_data_full (G_OBJECT (picker->popover), "emoji-picker",
|
||||
picker, emoji_picker_free);
|
||||
/* GtkEntry added this action in 3.22.27; earlier supported GTK 3.22
|
||||
* runtimes still use the button without emitting an unknown signal. */
|
||||
if (g_signal_lookup ("insert-emoji", GTK_TYPE_ENTRY))
|
||||
g_signal_connect_object (target_entry, "insert-emoji",
|
||||
G_CALLBACK (emoji_entry_insert_cb), picker->button, 0);
|
||||
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);
|
||||
gtk_menu_button_set_popover (GTK_MENU_BUTTON (picker->button),
|
||||
picker->popover);
|
||||
|
||||
return picker->button;
|
||||
}
|
||||
31
src/fe-gtk/emoji-picker.h
Normal file
31
src/fe-gtk/emoji-picker.h
Normal 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
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "chanlist.h"
|
||||
#include "joind.h"
|
||||
#include "xtext.h"
|
||||
#include "inline-image.h"
|
||||
#include "theme/theme-gtk.h"
|
||||
#include "menu.h"
|
||||
#include "notifygui.h"
|
||||
@@ -943,7 +944,13 @@ void
|
||||
fe_print_text (struct session *sess, char *text, time_t stamp,
|
||||
gboolean no_activity)
|
||||
{
|
||||
PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp);
|
||||
/* image links become a "[view image]" placeholder with a hidden URL */
|
||||
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)
|
||||
return;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<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;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
@@ -59,6 +59,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
||||
<ClInclude Include="fkeys.h" />
|
||||
<ClInclude Include="gtkutil.h" />
|
||||
<ClInclude Include="icon-resolver.h" />
|
||||
<ClInclude Include="inline-image.h" />
|
||||
<ClInclude Include="joind.h" />
|
||||
<ClInclude Include="maingui.h" />
|
||||
<ClInclude Include="preferences-persistence.h" />
|
||||
@@ -97,11 +98,14 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
||||
<ClCompile Include="custom-list.c" />
|
||||
<ClCompile Include="dccgui.c" />
|
||||
<ClCompile Include="editlist.c" />
|
||||
<ClCompile Include="emoji-picker.c" />
|
||||
<ClCompile Include="emoji-font.c" />
|
||||
<ClCompile Include="fe-gtk.c" />
|
||||
<ClCompile Include="fkeys.c" />
|
||||
<ClCompile Include="gtkutil.c" />
|
||||
<ClCompile Include="icon-resolver.c" />
|
||||
<ClCompile Include="ignoregui.c" />
|
||||
<ClCompile Include="inline-image.c" />
|
||||
<ClCompile Include="joind.c" />
|
||||
<ClCompile Include="maingui.c" />
|
||||
<ClCompile Include="menu.c" />
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<ClInclude Include="icon-resolver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="inline-image.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="joind.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -170,6 +173,9 @@
|
||||
<ClCompile Include="ignoregui.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="inline-image.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="joind.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -168,7 +168,36 @@ gtkutil_append_font_css (GString *css, const PangoFontDescription *font_desc)
|
||||
const char *family = pango_font_description_get_family (font_desc);
|
||||
|
||||
if (family && *family)
|
||||
g_string_append_printf (css, " font-family: \"%s\";", family);
|
||||
{
|
||||
char **families = g_strsplit (family, ",", -1);
|
||||
gboolean appended = FALSE;
|
||||
int i;
|
||||
|
||||
for (i = 0; families[i]; i++)
|
||||
{
|
||||
char *name = g_strstrip (families[i]);
|
||||
|
||||
if (!*name)
|
||||
continue;
|
||||
|
||||
g_string_append (css, appended ? "," : " font-family:");
|
||||
g_string_append (css, " \"");
|
||||
for (; *name; name++)
|
||||
{
|
||||
if (*name == '\\' || *name == '\"')
|
||||
g_string_append_c (css, '\\');
|
||||
if (*name == '\n' || *name == '\r' || *name == '\f')
|
||||
g_string_append_printf (css, "\\%x ", (unsigned char) *name);
|
||||
else
|
||||
g_string_append_c (css, *name);
|
||||
}
|
||||
g_string_append_c (css, '\"');
|
||||
appended = TRUE;
|
||||
}
|
||||
if (appended)
|
||||
g_string_append (css, ";");
|
||||
g_strfreev (families);
|
||||
}
|
||||
}
|
||||
|
||||
if (mask & PANGO_FONT_MASK_STYLE)
|
||||
|
||||
1924
src/fe-gtk/inline-image.c
Normal file
1924
src/fe-gtk/inline-image.c
Normal file
File diff suppressed because it is too large
Load Diff
45
src/fe-gtk/inline-image.h
Normal file
45
src/fe-gtk/inline-image.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/* 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
|
||||
@@ -45,6 +45,8 @@
|
||||
#include "theme/theme-manager.h"
|
||||
#include "theme/theme-css.h"
|
||||
#include "banlist.h"
|
||||
#include "emoji-picker.h"
|
||||
#include "emoji-font.h"
|
||||
#include "gtkutil.h"
|
||||
#include "icon-resolver.h"
|
||||
#include "joind.h"
|
||||
@@ -59,6 +61,7 @@
|
||||
#include "pixmaps.h"
|
||||
#include "plugin-tray.h"
|
||||
#include "xtext.h"
|
||||
#include "inline-image.h"
|
||||
#include "sexy-spell-entry.h"
|
||||
#include "gtkutil.h"
|
||||
|
||||
@@ -200,7 +203,6 @@ enum
|
||||
#define TAG_IRC 0 /* server, channel, dialog */
|
||||
#define TAG_UTIL 1 /* dcc, notify, chanlist */
|
||||
|
||||
static void mg_apply_emoji_fallback_widget (GtkWidget *widget);
|
||||
static void mg_reply_show_child (GtkWidget *widget, gpointer data);
|
||||
|
||||
#define MG_CONFIG_SAVE_DEBOUNCE_MS 250
|
||||
@@ -925,10 +927,236 @@ mg_send_reply_or_text (session *sess, char *cmd)
|
||||
mg_reply_update (sess);
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
gboolean bold;
|
||||
gboolean italic;
|
||||
gboolean underline;
|
||||
gboolean strikethrough;
|
||||
gboolean reverse;
|
||||
gint foreground;
|
||||
gint background;
|
||||
gboolean pending_foreground;
|
||||
gboolean pending_background;
|
||||
} MgInputFormatState;
|
||||
|
||||
static void
|
||||
mg_input_format_state_at_cursor (GtkEntry *entry, MgInputFormatState *state)
|
||||
{
|
||||
const char *text;
|
||||
const char *cursor;
|
||||
gsize limit;
|
||||
gsize i;
|
||||
gint cursor_pos;
|
||||
|
||||
memset (state, 0, sizeof (*state));
|
||||
state->foreground = -1;
|
||||
state->background = -1;
|
||||
|
||||
text = gtk_entry_get_text (entry);
|
||||
cursor_pos = gtk_editable_get_position (GTK_EDITABLE (entry));
|
||||
if (!text || cursor_pos < 0)
|
||||
return;
|
||||
|
||||
cursor = g_utf8_offset_to_pointer (text, cursor_pos);
|
||||
limit = (gsize) (cursor - text);
|
||||
|
||||
for (i = 0; i < limit;)
|
||||
{
|
||||
guchar ch = (guchar) text[i++];
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case ATTR_BOLD:
|
||||
state->bold = !state->bold;
|
||||
break;
|
||||
case ATTR_ITALICS:
|
||||
state->italic = !state->italic;
|
||||
break;
|
||||
case ATTR_UNDERLINE:
|
||||
state->underline = !state->underline;
|
||||
break;
|
||||
case ATTR_STRIKETHROUGH:
|
||||
state->strikethrough = !state->strikethrough;
|
||||
break;
|
||||
case ATTR_REVERSE:
|
||||
state->reverse = !state->reverse;
|
||||
break;
|
||||
case ATTR_RESET:
|
||||
case '\n':
|
||||
state->bold = FALSE;
|
||||
state->italic = FALSE;
|
||||
state->underline = FALSE;
|
||||
state->strikethrough = FALSE;
|
||||
state->reverse = FALSE;
|
||||
state->foreground = -1;
|
||||
state->background = -1;
|
||||
state->pending_foreground = FALSE;
|
||||
state->pending_background = FALSE;
|
||||
break;
|
||||
case ATTR_COLOR:
|
||||
{
|
||||
gint value = 0;
|
||||
gint digits = 0;
|
||||
|
||||
state->pending_foreground = FALSE;
|
||||
state->pending_background = FALSE;
|
||||
|
||||
if (i >= limit)
|
||||
{
|
||||
state->pending_foreground = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!g_ascii_isdigit (text[i]))
|
||||
{
|
||||
state->foreground = -1;
|
||||
state->background = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
|
||||
{
|
||||
value = (value * 10) + (text[i] - '0');
|
||||
i++;
|
||||
digits++;
|
||||
}
|
||||
state->foreground = value;
|
||||
state->background = -1;
|
||||
|
||||
if (i < limit && text[i] == ',')
|
||||
{
|
||||
i++;
|
||||
if (i >= limit)
|
||||
{
|
||||
state->pending_background = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
digits = 0;
|
||||
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
|
||||
{
|
||||
value = (value * 10) + (text[i] - '0');
|
||||
i++;
|
||||
digits++;
|
||||
}
|
||||
if (digits > 0)
|
||||
state->background = value;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
mg_input_format_append (GString *summary, const char *text)
|
||||
{
|
||||
if (summary->len > 0)
|
||||
g_string_append (summary, " · ");
|
||||
g_string_append (summary, text);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_input_format_color_value (char *buffer, gsize buffer_len, const char *label, gint color_index)
|
||||
{
|
||||
guint16 red;
|
||||
guint16 green;
|
||||
guint16 blue;
|
||||
|
||||
if (theme_get_mirc_color_rgb16 ((unsigned int) color_index, &red, &green, &blue))
|
||||
{
|
||||
g_snprintf (buffer, buffer_len,
|
||||
"%s <b>%02d</b> <span foreground=\"#%02x%02x%02x\">■</span>",
|
||||
label, color_index, (guint) (red >> 8), (guint) (green >> 8),
|
||||
(guint) (blue >> 8));
|
||||
return;
|
||||
}
|
||||
|
||||
g_snprintf (buffer, buffer_len, "%s <b>%02d</b>", label, color_index);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_input_format_update (GtkEntry *entry)
|
||||
{
|
||||
GtkWidget *format_box;
|
||||
GtkWidget *format_label;
|
||||
MgInputFormatState state;
|
||||
GString *summary;
|
||||
char color[96];
|
||||
char *markup;
|
||||
|
||||
format_box = g_object_get_data (G_OBJECT (entry), "zoitechat-format-box");
|
||||
format_label = g_object_get_data (G_OBJECT (entry), "zoitechat-format-label");
|
||||
if (!format_box || !format_label)
|
||||
return;
|
||||
|
||||
if (!prefs.hex_gui_input_attr)
|
||||
{
|
||||
gtk_widget_hide (format_box);
|
||||
return;
|
||||
}
|
||||
|
||||
mg_input_format_state_at_cursor (entry, &state);
|
||||
summary = g_string_new (NULL);
|
||||
|
||||
if (state.bold)
|
||||
mg_input_format_append (summary, "<b>Bold</b>");
|
||||
if (state.italic)
|
||||
mg_input_format_append (summary, "<i>Italic</i>");
|
||||
if (state.underline)
|
||||
mg_input_format_append (summary, "<u>Underline</u>");
|
||||
if (state.strikethrough)
|
||||
mg_input_format_append (summary, "<s>Strikethrough</s>");
|
||||
if (state.reverse)
|
||||
mg_input_format_append (summary, "Reverse");
|
||||
|
||||
if (state.foreground >= 0)
|
||||
{
|
||||
mg_input_format_color_value (color, sizeof (color), "Text color", state.foreground);
|
||||
mg_input_format_append (summary, color);
|
||||
}
|
||||
if (state.background >= 0)
|
||||
{
|
||||
mg_input_format_color_value (color, sizeof (color), "Background", state.background);
|
||||
mg_input_format_append (summary, color);
|
||||
}
|
||||
if (state.pending_foreground)
|
||||
mg_input_format_append (summary, "Color: type foreground[,background]");
|
||||
else if (state.pending_background)
|
||||
mg_input_format_append (summary, "Background: type color");
|
||||
|
||||
if (summary->len == 0)
|
||||
{
|
||||
gtk_widget_hide (format_box);
|
||||
g_string_free (summary, TRUE);
|
||||
return;
|
||||
}
|
||||
|
||||
markup = g_strdup_printf ("<span foreground='#7d8790'>Formatting · %s</span>", summary->str);
|
||||
gtk_label_set_markup (GTK_LABEL (format_label), markup);
|
||||
gtk_widget_show (format_label);
|
||||
gtk_widget_show (format_box);
|
||||
g_free (markup);
|
||||
g_string_free (summary, TRUE);
|
||||
}
|
||||
|
||||
static void
|
||||
mg_inputbox_cursor_changed (GObject *object, GParamSpec *pspec, gpointer userdata)
|
||||
{
|
||||
(void) pspec;
|
||||
(void) userdata;
|
||||
mg_input_format_update (GTK_ENTRY (object));
|
||||
}
|
||||
|
||||
static void
|
||||
mg_inputbox_changed (GtkEditable *editable, session_gui *gui)
|
||||
{
|
||||
key_check_replace_on_change (editable, NULL);
|
||||
mg_input_format_update (GTK_ENTRY (editable));
|
||||
if (current_sess && current_sess->gui == gui)
|
||||
mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable)));
|
||||
}
|
||||
@@ -3246,7 +3474,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
|
||||
gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23);
|
||||
gtk_widget_set_size_request (gui->key_entry, 58, 11);
|
||||
gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0);
|
||||
mg_apply_emoji_fallback_widget (gui->key_entry);
|
||||
emoji_font_apply (gui->key_entry);
|
||||
mg_apply_compact_mode_css (gui->key_entry);
|
||||
g_signal_connect (G_OBJECT (gui->key_entry), "activate",
|
||||
G_CALLBACK (mg_key_entry_cb), NULL);
|
||||
@@ -3264,7 +3492,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
|
||||
gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5);
|
||||
gtk_widget_set_size_request (gui->limit_entry, 45, 11);
|
||||
gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0);
|
||||
mg_apply_emoji_fallback_widget (gui->limit_entry);
|
||||
emoji_font_apply (gui->limit_entry);
|
||||
mg_apply_compact_mode_css (gui->limit_entry);
|
||||
g_signal_connect (G_OBJECT (gui->limit_entry), "activate",
|
||||
G_CALLBACK (mg_limit_entry_cb), NULL);
|
||||
@@ -3529,8 +3757,12 @@ mg_apply_session_font_prefs (session_gui *gui)
|
||||
mg_topicbar_update_height (gui->topic_entry);
|
||||
}
|
||||
|
||||
if (gui->input_box && prefs.hex_gui_input_style)
|
||||
theme_manager_apply_entry_palette (gui->input_box, font);
|
||||
if (gui->input_box)
|
||||
{
|
||||
if (prefs.hex_gui_input_style)
|
||||
theme_manager_apply_entry_palette (gui->input_box, font);
|
||||
emoji_font_apply (gui->input_box);
|
||||
}
|
||||
|
||||
if (gui->chanview)
|
||||
chanview_apply_theme (gui->chanview);
|
||||
@@ -3687,7 +3919,13 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
||||
{
|
||||
if (word == NULL)
|
||||
{
|
||||
mg_focus (sess);
|
||||
/* 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);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3699,6 +3937,20 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
||||
case WORD_HOST6:
|
||||
case WORD_HOST:
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3727,7 +3979,8 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
||||
case WORD_HOST:
|
||||
word[end] = 0;
|
||||
word += start;
|
||||
menu_urlmenu (even, word);
|
||||
menu_urlmenu_entry (even, word, xtext,
|
||||
gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext)));
|
||||
break;
|
||||
case WORD_NICK:
|
||||
word[end] = 0;
|
||||
@@ -4416,131 +4669,6 @@ mg_inputbox_rightclick (GtkEntry *entry, GtkWidget *menu)
|
||||
mg_create_color_menu (menu, NULL);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Emoji font handling
|
||||
*
|
||||
* Goal: prefer color emoji fonts when available, without changing existing
|
||||
* font size/style/weight, and without breaking user-configured fonts.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static const char *mg_emoji_family_fallback =
|
||||
#ifdef G_OS_WIN32
|
||||
"Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
|
||||
#else
|
||||
"Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
|
||||
#endif
|
||||
|
||||
static const char *
|
||||
mg_find_available_icon_name (const char *const *icon_names)
|
||||
{
|
||||
GtkIconTheme *theme;
|
||||
int i;
|
||||
|
||||
theme = gtk_icon_theme_get_default ();
|
||||
if (!theme || !icon_names)
|
||||
return NULL;
|
||||
|
||||
for (i = 0; icon_names[i] != NULL; i++)
|
||||
{
|
||||
int action;
|
||||
|
||||
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
|
||||
return icon_names[i];
|
||||
|
||||
if (icon_resolver_menu_action_from_name (icon_names[i], &action))
|
||||
{
|
||||
char *resource_path = icon_resolver_resolve_path (ICON_RESOLVER_ROLE_MENU_ACTION, action, GTK_ICON_SIZE_MENU, "menu", ICON_RESOLVER_THEME_SYSTEM, NULL);
|
||||
|
||||
if (resource_path)
|
||||
{
|
||||
g_free (resource_path);
|
||||
return icon_names[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
mg_family_already_has_emoji (const gchar *family)
|
||||
{
|
||||
if (!family || !*family)
|
||||
return FALSE;
|
||||
|
||||
/* cheap but effective */
|
||||
return (strstr (family, "Noto Color Emoji") != NULL) ||
|
||||
(strstr (family, "Segoe UI Emoji") != NULL) ||
|
||||
(strstr (family, "Apple Color Emoji") != NULL) ||
|
||||
(strstr (family, "Twemoji") != NULL) ||
|
||||
(strstr (family, "EmojiOne") != NULL);
|
||||
}
|
||||
|
||||
static PangoFontDescription *
|
||||
mg_fontdesc_with_fallback (const PangoFontDescription *base_desc, gboolean emoji_first)
|
||||
{
|
||||
PangoFontDescription *desc;
|
||||
const gchar *base_family;
|
||||
gchar *family_list;
|
||||
|
||||
if (!base_desc)
|
||||
return NULL;
|
||||
|
||||
desc = pango_font_description_copy (base_desc);
|
||||
base_family = pango_font_description_get_family (desc);
|
||||
|
||||
if (mg_family_already_has_emoji (base_family))
|
||||
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",
|
||||
(base_family && *base_family) ? base_family : "Sans",
|
||||
mg_emoji_family_fallback);
|
||||
}
|
||||
|
||||
pango_font_description_set_family (desc, family_list);
|
||||
g_free (family_list);
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
static void
|
||||
mg_apply_emoji_fallback_widget (GtkWidget *widget)
|
||||
{
|
||||
PangoFontDescription *desc;
|
||||
GtkStyleContext *context;
|
||||
PangoFontDescription *base_desc = NULL;
|
||||
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
context = gtk_widget_get_style_context (widget);
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL,
|
||||
"font", &base_desc,
|
||||
NULL);
|
||||
if (!base_desc)
|
||||
return;
|
||||
|
||||
desc = mg_fontdesc_with_fallback (base_desc, FALSE);
|
||||
pango_font_description_free (base_desc);
|
||||
if (!desc)
|
||||
return;
|
||||
|
||||
mg_apply_font_css (widget, desc, "zoitechat-emoji-font",
|
||||
"zoitechat-emoji-font-provider");
|
||||
pango_font_description_free (desc);
|
||||
}
|
||||
|
||||
/* Search bar adapted from Conspire's by William Pitcock */
|
||||
|
||||
#define SEARCH_CHANGE 1
|
||||
@@ -4745,7 +4873,7 @@ mg_create_search(session *sess, GtkWidget *box)
|
||||
gui->shentry = entry = gtk_entry_new();
|
||||
gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0);
|
||||
gtk_widget_set_size_request (gui->shentry, 180, -1);
|
||||
mg_apply_emoji_fallback_widget (entry);
|
||||
emoji_font_apply (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);
|
||||
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_keypress), sess);
|
||||
@@ -4799,17 +4927,18 @@ mg_create_search(session *sess, GtkWidget *box)
|
||||
static void
|
||||
mg_create_entry (session *sess, GtkWidget *box)
|
||||
{
|
||||
GtkWidget *hbox, *but, *entry;
|
||||
GtkWidget *hbox, *but, *entry, *format_box, *format_label;
|
||||
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;
|
||||
|
||||
format_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
gtk_widget_set_name (format_box, "zoitechat-formatbar");
|
||||
gtk_widget_set_no_show_all (format_box, TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (box), format_box, 0, 0, 0);
|
||||
format_label = gtk_label_new ("");
|
||||
gtk_label_set_ellipsize (GTK_LABEL (format_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_pack_start (GTK_BOX (format_box), format_label, TRUE, TRUE, 8);
|
||||
gtk_widget_show (format_label);
|
||||
gtk_widget_hide (format_box);
|
||||
|
||||
gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
|
||||
@@ -4841,12 +4970,16 @@ mg_create_entry (session *sess, GtkWidget *box)
|
||||
gui->input_box = entry = sexy_spell_entry_new ();
|
||||
sexy_spell_entry_set_checked ((SexySpellEntry *)entry, prefs.hex_gui_input_spell);
|
||||
sexy_spell_entry_set_parse_attributes ((SexySpellEntry *)entry, prefs.hex_gui_input_attr);
|
||||
g_object_set_data (G_OBJECT (entry), "zoitechat-format-box", format_box);
|
||||
g_object_set_data (G_OBJECT (entry), "zoitechat-format-label", format_label);
|
||||
|
||||
gtk_entry_set_max_length (GTK_ENTRY (gui->input_box), 0);
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (mg_inputbox_cb), gui);
|
||||
g_signal_connect (G_OBJECT (entry), "changed",
|
||||
G_CALLBACK (mg_inputbox_changed), gui);
|
||||
g_signal_connect (G_OBJECT (entry), "notify::cursor-position",
|
||||
G_CALLBACK (mg_inputbox_cursor_changed), gui);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_set_name (entry, "zoitechat-inputbox");
|
||||
@@ -4862,16 +4995,14 @@ mg_create_entry (session *sess, GtkWidget *box)
|
||||
|
||||
if (prefs.hex_gui_input_style)
|
||||
mg_apply_entry_style (entry);
|
||||
emoji_font_apply (entry);
|
||||
mg_apply_entry_scroll_artifact_fix (entry);
|
||||
|
||||
g_object_set (G_OBJECT (entry), "show-emoji-icon", TRUE, NULL);
|
||||
|
||||
if (gtk_entry_get_icon_storage_type (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY) == GTK_IMAGE_EMPTY)
|
||||
{
|
||||
emoji_fallback_icon_name = mg_find_available_icon_name (emoji_fallback_icon_names);
|
||||
if (emoji_fallback_icon_name)
|
||||
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY, emoji_fallback_icon_name);
|
||||
}
|
||||
/* 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
|
||||
@@ -5019,6 +5150,7 @@ mg_create_topwindow (session *sess)
|
||||
prefs.hex_gui_win_width,
|
||||
prefs.hex_gui_win_height, 0);
|
||||
sess->gui->window = win;
|
||||
emoji_font_apply (win);
|
||||
gtk_container_set_border_width (GTK_CONTAINER (win), GUI_BORDER);
|
||||
gtk_widget_set_opacity (win, (prefs.hex_gui_transparency / 255.));
|
||||
|
||||
@@ -5223,6 +5355,7 @@ mg_create_tabwindow (session *sess)
|
||||
win = gtkutil_window_new ("ZoiteChat", NULL, prefs.hex_gui_win_width,
|
||||
prefs.hex_gui_win_height, 0);
|
||||
sess->gui->window = win;
|
||||
emoji_font_apply (win);
|
||||
gtk_window_move (GTK_WINDOW (win), prefs.hex_gui_win_left,
|
||||
prefs.hex_gui_win_top);
|
||||
if (prefs.hex_gui_win_state)
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "../common/util.h"
|
||||
#include "../common/text.h"
|
||||
#include "xtext.h"
|
||||
#include "inline-image.h"
|
||||
#include "ascii.h"
|
||||
#include "banlist.h"
|
||||
#include "chanlist.h"
|
||||
@@ -1056,8 +1057,40 @@ open_url_cb (GtkWidget *item, char *url)
|
||||
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
|
||||
menu_urlmenu (GdkEventButton *event, char *url)
|
||||
menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
|
||||
textentry *ent)
|
||||
{
|
||||
GtkWidget *menu;
|
||||
char *tmp, *chop;
|
||||
@@ -1065,6 +1098,14 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
||||
g_free (str_copy);
|
||||
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 ();
|
||||
/* more than 51 chars? Chop it */
|
||||
if (g_utf8_strlen (str_copy, -1) >= 52)
|
||||
@@ -1087,6 +1128,15 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
||||
menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy);
|
||||
else
|
||||
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);
|
||||
/* custom ones from urlhandlers.conf */
|
||||
menu_create (menu, urlhandler_list, str_copy, TRUE);
|
||||
@@ -1094,6 +1144,12 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
||||
menu_popup (menu, event, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
menu_urlmenu (GdkEventButton *event, char *url)
|
||||
{
|
||||
menu_urlmenu_entry (event, url, NULL, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
menu_chan_cycle (GtkWidget * menu, char *chan)
|
||||
{
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
#ifndef 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);
|
||||
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_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree);
|
||||
void menu_addconnectmenu (server *serv, GtkWidget *menu);
|
||||
|
||||
@@ -18,11 +18,14 @@ zoitechat_gtk_sources = [
|
||||
'custom-list.c',
|
||||
'dccgui.c',
|
||||
'editlist.c',
|
||||
'emoji-picker.c',
|
||||
'emoji-font.c',
|
||||
'fe-gtk.c',
|
||||
'fkeys.c',
|
||||
'gtkutil.c',
|
||||
'icon-resolver.c',
|
||||
'ignoregui.c',
|
||||
'inline-image.c',
|
||||
'joind.c',
|
||||
'menu.c',
|
||||
'maingui.c',
|
||||
@@ -72,7 +75,8 @@ if host_machine.system() != 'windows'
|
||||
endif
|
||||
endif
|
||||
|
||||
zoitechat_gtk_deps += gtk_dep
|
||||
emoji_font_deps = [gtk_dep, dependency('pangoft2', version: '>= 1.44'), dependency('fontconfig', version: '>= 2.13')]
|
||||
zoitechat_gtk_deps += emoji_font_deps
|
||||
|
||||
if host_machine.system() != 'windows'
|
||||
gdk_x11_dep = dependency('gdk-x11-3.0', required: false)
|
||||
@@ -107,7 +111,7 @@ zoitechat_theme_lib = static_library('zoitechat_theme',
|
||||
iso_codes = dependency('iso-codes', required: false)
|
||||
if iso_codes.found()
|
||||
zoitechat_gtk_sources += 'sexy-iso-codes.c'
|
||||
iso_codes_prefix = iso_codes.get_pkgconfig_variable('prefix')
|
||||
iso_codes_prefix = iso_codes.get_variable(pkgconfig : 'prefix')
|
||||
zoitechat_gtk_cflags += '-DISO_CODES_PREFIX="@0@"'.format(iso_codes_prefix)
|
||||
zoitechat_gtk_cflags += '-DISO_CODES_LOCALEDIR="@0@"'.format(
|
||||
join_paths(iso_codes_prefix, 'share/locale'))
|
||||
@@ -136,7 +140,19 @@ executable('zoitechat',
|
||||
link_args: zoitechat_gtk_ldflags,
|
||||
pie: true,
|
||||
install: true,
|
||||
gui_app: true,
|
||||
win_subsystem: 'windows',
|
||||
)
|
||||
|
||||
emoji_ui_tests = executable('emoji_ui_tests',
|
||||
['tests/test-emoji-ui.c', 'emoji-font.c', 'emoji-picker.c',
|
||||
'../common/emoji-data.c', emoji_data_table],
|
||||
include_directories: [config_h_include, include_directories('../common')],
|
||||
dependencies: emoji_font_deps,
|
||||
)
|
||||
test('Emoji UI Tests', emoji_ui_tests,
|
||||
env: ['ZOITECHAT_EMOJI_FONT=' + join_paths(meson.source_root(), 'data/fonts/NotoColorEmoji.ttf')],
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
theme_manager_policy_tests = executable('theme_manager_policy_tests',
|
||||
@@ -251,3 +267,14 @@ test('Theme Preferences GTK3 Populate Tests', theme_preferences_gtk3_populate_te
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
emoji_font_tests = executable('emoji_font_tests',
|
||||
['tests/test-emoji-font.c', 'emoji-font.c', '../common/emoji-data.c', emoji_data_table],
|
||||
include_directories: [config_h_include, include_directories('../common')],
|
||||
dependencies: emoji_font_deps,
|
||||
)
|
||||
test('Emoji Font Tests', emoji_font_tests,
|
||||
env: ['ZOITECHAT_EMOJI_FONT=' + join_paths(meson.source_root(), 'data/fonts/NotoColorEmoji.ttf')],
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
@@ -340,6 +340,9 @@ static const setting tabs_settings[] =
|
||||
{ST_TOGGLE, N_("Show close button on tabs"), P_OFFINTNL(hex_gui_tab_closebuttons), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Middle click to close tab"), P_OFFINTNL(hex_gui_tab_middleclose), 0, 0, 0},
|
||||
{ST_TOGGLE, N_("Smaller text"), P_OFFINTNL(hex_gui_tab_small), 0, 0, 0},
|
||||
{ST_EFONT, N_("Tab font:"), P_OFFSETNL(hex_gui_tab_font),
|
||||
N_("Font for server and channel tabs in the Tabs layout. Leave blank to use the theme font. Smaller text still applies."),
|
||||
0, sizeof prefs.hex_gui_tab_font},
|
||||
{ST_MENU, N_("Focus new tabs:"), P_OFFINTNL(hex_gui_tab_newtofront), 0, focusnewtabsmenu, 0},
|
||||
{ST_MENU, N_("Placement of notices:"), P_OFFINTNL(hex_irc_notice_pos), 0, noticeposmenu, 0},
|
||||
{ST_MENU, N_("Show channel switcher at:"), P_OFFINTNL(hex_gui_tab_pos), 0, cspos, 1},
|
||||
@@ -674,6 +677,11 @@ 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_("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}
|
||||
};
|
||||
|
||||
@@ -2330,6 +2338,9 @@ setup_apply (struct zoitechatprefs *pr)
|
||||
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
|
||||
DIFF (hex_gui_tab_small))
|
||||
live_changes.chanview = TRUE;
|
||||
if (pr->hex_gui_tab_layout == 0 &&
|
||||
strcmp (pr->hex_gui_tab_font, prefs.hex_gui_tab_font) != 0)
|
||||
live_changes.chanview = TRUE;
|
||||
if (DIFF (hex_gui_tab_sort))
|
||||
live_changes.tab_resort = TRUE;
|
||||
if (DIFF (hex_gui_tab_trunc))
|
||||
|
||||
174
src/fe-gtk/tests/test-emoji-font.c
Normal file
174
src/fe-gtk/tests/test-emoji-font.c
Normal file
@@ -0,0 +1,174 @@
|
||||
/* Shared headless font coverage and ordinary text regression tests.
|
||||
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||
#include "../emoji-font.h"
|
||||
#include "../../common/emoji-data.h"
|
||||
#include <pango/pangofc-fontmap.h>
|
||||
#include <pango/pangofc-font.h>
|
||||
|
||||
static GHashTable *font_checksums;
|
||||
static char *bundled_checksum;
|
||||
|
||||
static const char *
|
||||
font_checksum (const char *path)
|
||||
{
|
||||
const char *cached = g_hash_table_lookup (font_checksums, path);
|
||||
char *contents, *checksum;
|
||||
gsize length;
|
||||
|
||||
if (cached)
|
||||
return cached;
|
||||
g_assert_true (g_file_get_contents (path, &contents, &length, NULL));
|
||||
checksum = g_compute_checksum_for_data (G_CHECKSUM_SHA256,
|
||||
(const guchar *) contents, length);
|
||||
g_free (contents);
|
||||
g_hash_table_insert (font_checksums, g_strdup (path), checksum);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
static void
|
||||
assert_bundled_font (PangoFont *font, gboolean expected)
|
||||
{
|
||||
FcPattern *pattern;
|
||||
FcChar8 *path;
|
||||
const char *checksum;
|
||||
|
||||
/* Pango can describe a face by its embedded name rather than its
|
||||
* Fontconfig alias. Verify the resolved file's bytes, not that name.
|
||||
* Cache by filename so catalog coverage does not reread the font for
|
||||
* every glyph. An older system Noto must still fail this assertion. */
|
||||
g_assert_true (PANGO_IS_FC_FONT (font));
|
||||
#if PANGO_VERSION_CHECK(1, 48, 0)
|
||||
pattern = pango_fc_font_get_pattern (PANGO_FC_FONT (font));
|
||||
#else
|
||||
pattern = PANGO_FC_FONT (font)->font_pattern;
|
||||
#endif
|
||||
g_assert_cmpint (FcPatternGetString (pattern, FC_FILE, 0, &path), ==, FcResultMatch);
|
||||
checksum = font_checksum ((const char *) path);
|
||||
if (expected)
|
||||
g_assert_cmpstr (checksum, ==, bundled_checksum);
|
||||
else
|
||||
g_assert_cmpstr (checksum, !=, bundled_checksum);
|
||||
}
|
||||
|
||||
static void
|
||||
test_font_catalog (void)
|
||||
{
|
||||
PangoFontMap *map = emoji_font_get_map ();
|
||||
PangoContext *context;
|
||||
PangoLayout *layout;
|
||||
PangoFontDescription *desc;
|
||||
gsize i;
|
||||
int tone;
|
||||
|
||||
g_assert_nonnull (map);
|
||||
context = pango_font_map_create_context (map);
|
||||
g_assert_true (PANGO_IS_FC_FONT_MAP (map));
|
||||
layout = pango_layout_new (context);
|
||||
desc = pango_font_description_from_string (ZOITECHAT_EMOJI_FAMILY " 15");
|
||||
pango_layout_set_font_description (layout, desc);
|
||||
for (i = 0; i < emoji_data_count (); i++)
|
||||
{
|
||||
const EmojiEntry *entry = emoji_data_entry (i);
|
||||
for (tone = 0; tone < (entry->tone_set < 0 ? 1 : EMOJI_TONE_COUNT); tone++)
|
||||
{
|
||||
PangoLayoutIter *iter;
|
||||
pango_layout_set_text (layout, emoji_entry_sequence_for_tone (entry, tone), -1);
|
||||
g_assert_cmpint (pango_layout_get_unknown_glyphs_count (layout), ==, 0);
|
||||
iter = pango_layout_get_iter (layout);
|
||||
do
|
||||
{
|
||||
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||
if (run)
|
||||
{
|
||||
assert_bundled_font (run->item->analysis.font, TRUE);
|
||||
}
|
||||
} while (pango_layout_iter_next_run (iter));
|
||||
pango_layout_iter_free (iter);
|
||||
}
|
||||
}
|
||||
pango_font_description_free (desc);
|
||||
g_object_unref (layout);
|
||||
g_object_unref (context);
|
||||
}
|
||||
|
||||
static void
|
||||
test_plain_text_font (void)
|
||||
{
|
||||
PangoFontMap *map = emoji_font_get_map ();
|
||||
PangoContext *context;
|
||||
PangoLayout *layout;
|
||||
PangoLayoutIter *iter;
|
||||
PangoFontDescription *desc;
|
||||
|
||||
g_assert_nonnull (map);
|
||||
context = pango_font_map_create_context (map);
|
||||
layout = pango_layout_new (context);
|
||||
pango_layout_set_text (layout, "/join #channel 0123456789 *", -1);
|
||||
desc = pango_font_description_from_string ("Monospace 12");
|
||||
pango_layout_set_font_description (layout, desc);
|
||||
iter = pango_layout_get_iter (layout);
|
||||
do
|
||||
{
|
||||
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||
if (run)
|
||||
{
|
||||
assert_bundled_font (run->item->analysis.font, FALSE);
|
||||
}
|
||||
} while (pango_layout_iter_next_run (iter));
|
||||
pango_layout_iter_free (iter);
|
||||
pango_font_description_free (desc);
|
||||
g_object_unref (layout);
|
||||
g_object_unref (context);
|
||||
}
|
||||
|
||||
static void
|
||||
test_implicit_emoji_font (void)
|
||||
{
|
||||
PangoFontMap *map = emoji_font_get_map ();
|
||||
PangoContext *context = pango_font_map_create_context (map);
|
||||
PangoLayout *layout = pango_layout_new (context);
|
||||
PangoFontDescription *desc = pango_font_description_from_string ("Monospace 12");
|
||||
const char *samples[] = { "\360\237\230\200", "#\357\270\217\342\203\243", "\360\237\253\252" };
|
||||
gsize i;
|
||||
|
||||
pango_layout_set_font_description (layout, desc);
|
||||
for (i = 0; i < G_N_ELEMENTS (samples); i++)
|
||||
{
|
||||
PangoLayoutIter *iter;
|
||||
pango_layout_set_text (layout, samples[i], -1);
|
||||
g_assert_cmpint (pango_layout_get_unknown_glyphs_count (layout), ==, 0);
|
||||
iter = pango_layout_get_iter (layout);
|
||||
do
|
||||
{
|
||||
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||
if (run)
|
||||
{
|
||||
assert_bundled_font (run->item->analysis.font, TRUE);
|
||||
}
|
||||
} while (pango_layout_iter_next_run (iter));
|
||||
pango_layout_iter_free (iter);
|
||||
}
|
||||
pango_font_description_free (desc);
|
||||
g_object_unref (layout);
|
||||
g_object_unref (context);
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
const char *path;
|
||||
int result;
|
||||
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
path = g_getenv ("ZOITECHAT_EMOJI_FONT");
|
||||
g_assert_nonnull (path);
|
||||
font_checksums = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
|
||||
bundled_checksum = g_strdup (font_checksum (path));
|
||||
g_test_add_func ("/emoji-font/catalog", test_font_catalog);
|
||||
g_test_add_func ("/emoji-font/plain-text", test_plain_text_font);
|
||||
g_test_add_func ("/emoji-font/implicit-emoji", test_implicit_emoji_font);
|
||||
result = g_test_run ();
|
||||
g_free (bundled_checksum);
|
||||
g_hash_table_destroy (font_checksums);
|
||||
return result;
|
||||
}
|
||||
156
src/fe-gtk/tests/test-emoji-ui.c
Normal file
156
src/fe-gtk/tests/test-emoji-ui.c
Normal file
@@ -0,0 +1,156 @@
|
||||
/* Regression coverage shared by Meson and the Windows CI build.
|
||||
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||
#include "../fe-gtk.h"
|
||||
#include "../../common/zoitechat.h"
|
||||
#include "../../common/emoji-data.h"
|
||||
#include "../emoji-font.h"
|
||||
#include "../emoji-picker.h"
|
||||
#include "../preferences-persistence.h"
|
||||
#include <pango/pangofc-fontmap.h>
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
struct zoitechatprefs prefs;
|
||||
static char *config_dir;
|
||||
|
||||
static void
|
||||
test_theme_svg_loader (void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
GdkPixbuf *pixbuf;
|
||||
|
||||
/* Exercise the theme asset that failed in the packaged Windows runtime. */
|
||||
pixbuf = gdk_pixbuf_new_from_resource (
|
||||
"/org/gtk/libgtk/theme/Adwaita/assets/bullet-symbolic.svg", &error);
|
||||
g_assert_no_error (error);
|
||||
g_assert_nonnull (pixbuf);
|
||||
g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), >, 0);
|
||||
g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), >, 0);
|
||||
g_object_unref (pixbuf);
|
||||
}
|
||||
|
||||
char *get_xdir (void) { return config_dir; }
|
||||
PreferencesPersistenceResult preferences_persistence_save_all (void)
|
||||
{
|
||||
PreferencesPersistenceResult result = {0};
|
||||
result.success = TRUE;
|
||||
return result;
|
||||
}
|
||||
|
||||
static GtkWidget *
|
||||
find_widget (GtkWidget *widget, GType type)
|
||||
{
|
||||
GList *children, *item;
|
||||
GtkWidget *found = NULL;
|
||||
|
||||
if (G_TYPE_CHECK_INSTANCE_TYPE (widget, type))
|
||||
return widget;
|
||||
if (!GTK_IS_CONTAINER (widget))
|
||||
return NULL;
|
||||
children = gtk_container_get_children (GTK_CONTAINER (widget));
|
||||
for (item = children; item && !found; item = item->next)
|
||||
found = find_widget (item->data, type);
|
||||
g_list_free (children);
|
||||
return found;
|
||||
}
|
||||
|
||||
typedef struct { GtkWidget *window, *entry, *button, *popover, *search; } PickerFixture;
|
||||
|
||||
static void
|
||||
fixture_setup (PickerFixture *f, gconstpointer data)
|
||||
{
|
||||
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
f->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
f->entry = gtk_entry_new ();
|
||||
f->button = emoji_picker_button_new (f->entry);
|
||||
gtk_container_add (GTK_CONTAINER (f->window), box);
|
||||
gtk_box_pack_start (GTK_BOX (box), f->entry, TRUE, TRUE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (box), f->button, FALSE, FALSE, 0);
|
||||
gtk_widget_show_all (f->window);
|
||||
while (gtk_events_pending ()) gtk_main_iteration ();
|
||||
f->popover = GTK_WIDGET (gtk_menu_button_get_popover (GTK_MENU_BUTTON (f->button)));
|
||||
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (f->button), TRUE);
|
||||
f->search = find_widget (f->popover, GTK_TYPE_SEARCH_ENTRY);
|
||||
g_assert_nonnull (f->search);
|
||||
}
|
||||
|
||||
static void
|
||||
fixture_teardown (PickerFixture *f, gconstpointer data)
|
||||
{
|
||||
gtk_widget_destroy (f->window);
|
||||
while (gtk_events_pending ()) gtk_main_iteration ();
|
||||
}
|
||||
|
||||
static void
|
||||
insert_duck (PickerFixture *f)
|
||||
{
|
||||
gtk_entry_set_text (GTK_ENTRY (f->search), "bird duck");
|
||||
/* Do not wait for GTK's debounced search-changed signal. */
|
||||
g_signal_emit_by_name (f->search, "activate");
|
||||
}
|
||||
|
||||
static void
|
||||
test_search_insertion (PickerFixture *f, gconstpointer data)
|
||||
{
|
||||
gtk_entry_set_text (GTK_ENTRY (f->entry), "before after");
|
||||
gtk_editable_select_region (GTK_EDITABLE (f->entry), 7, 12);
|
||||
insert_duck (f);
|
||||
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (f->entry)), ==, "before \360\237\246\206");
|
||||
g_assert_cmpint (gtk_editable_get_position (GTK_EDITABLE (f->entry)), ==, 8);
|
||||
}
|
||||
|
||||
static void
|
||||
test_read_only (PickerFixture *f, gconstpointer data)
|
||||
{
|
||||
gtk_entry_set_text (GTK_ENTRY (f->entry), "unchanged");
|
||||
gtk_editable_set_editable (GTK_EDITABLE (f->entry), FALSE);
|
||||
insert_duck (f);
|
||||
g_signal_emit_by_name (f->search, "stop-search");
|
||||
g_assert_false (gtk_editable_get_editable (GTK_EDITABLE (f->entry)));
|
||||
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (f->entry)), ==, "unchanged");
|
||||
}
|
||||
|
||||
static void
|
||||
test_sequence_limit (PickerFixture *f, gconstpointer data)
|
||||
{
|
||||
gtk_entry_set_max_length (GTK_ENTRY (f->entry), 1);
|
||||
gtk_entry_set_text (GTK_ENTRY (f->search), "flag canada");
|
||||
g_signal_emit_by_name (f->search, "activate");
|
||||
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (f->entry)), ==, "");
|
||||
gtk_entry_set_max_length (GTK_ENTRY (f->entry), 2);
|
||||
g_signal_emit_by_name (f->search, "activate");
|
||||
g_assert_cmpstr (gtk_entry_get_text (GTK_ENTRY (f->entry)), ==, "\360\237\207\250\360\237\207\246");
|
||||
}
|
||||
|
||||
static void
|
||||
test_entry_action (PickerFixture *f, gconstpointer data)
|
||||
{
|
||||
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (f->button), FALSE);
|
||||
if (g_signal_lookup ("insert-emoji", GTK_TYPE_ENTRY))
|
||||
{
|
||||
g_signal_emit_by_name (f->entry, "insert-emoji");
|
||||
g_assert_true (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (f->button)));
|
||||
g_assert_true (gtk_widget_get_visible (f->popover));
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char **argv)
|
||||
{
|
||||
char *recents;
|
||||
gtk_test_init (&argc, &argv, NULL);
|
||||
g_object_set (gtk_settings_get_default (), "gtk-enable-animations", FALSE, NULL);
|
||||
config_dir = g_dir_make_tmp ("zoitechat-emoji-tests-XXXXXX", NULL);
|
||||
g_assert_nonnull (config_dir);
|
||||
g_test_add_func ("/emoji-ui/theme-svg-loader", test_theme_svg_loader);
|
||||
g_test_add ("/emoji-ui/search-insertion", PickerFixture, NULL, fixture_setup, test_search_insertion, fixture_teardown);
|
||||
g_test_add ("/emoji-ui/read-only", PickerFixture, NULL, fixture_setup, test_read_only, fixture_teardown);
|
||||
g_test_add ("/emoji-ui/sequence-limit", PickerFixture, NULL, fixture_setup, test_sequence_limit, fixture_teardown);
|
||||
g_test_add ("/emoji-ui/entry-action", PickerFixture, NULL, fixture_setup, test_entry_action, fixture_teardown);
|
||||
argc = g_test_run ();
|
||||
recents = g_build_filename (config_dir, "emoji-recents.conf", NULL);
|
||||
g_unlink (recents);
|
||||
g_rmdir (config_dir);
|
||||
g_free (recents);
|
||||
g_free (config_dir);
|
||||
return argc;
|
||||
}
|
||||
@@ -683,7 +683,7 @@ theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_de
|
||||
|
||||
behavior.font_desc = font_desc;
|
||||
behavior.apply_background = TRUE;
|
||||
behavior.apply_foreground = (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks) ? FALSE : TRUE;
|
||||
behavior.apply_foreground = prefs.hex_gui_ulist_color ? FALSE : TRUE;
|
||||
|
||||
return behavior;
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@ fe_userlist_rehash (session *sess, struct User *user)
|
||||
nick_token = THEME_TOKEN_TAB_AWAY;
|
||||
have_nick_token = TRUE;
|
||||
}
|
||||
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
|
||||
else if (prefs.hex_gui_ulist_color)
|
||||
{
|
||||
int mirc_index = text_color_of (user->nick);
|
||||
|
||||
@@ -635,7 +635,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
|
||||
nick_token = THEME_TOKEN_TAB_AWAY;
|
||||
have_nick_token = TRUE;
|
||||
}
|
||||
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
|
||||
else if (prefs.hex_gui_ulist_color)
|
||||
{
|
||||
int mirc_index = text_color_of (newuser->nick);
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "fe-gtk.h"
|
||||
#include "xtext.h"
|
||||
#include "fkeys.h"
|
||||
#include "emoji-font.h"
|
||||
#include "theme/theme-access.h"
|
||||
|
||||
#define charlen(str) g_utf8_skip[*(guchar *)(str)]
|
||||
@@ -92,8 +93,104 @@ struct textentry
|
||||
guchar pad1;
|
||||
guchar pad2;
|
||||
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
|
||||
{
|
||||
WORD_CLICK,
|
||||
@@ -213,6 +310,7 @@ 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 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 textentry * gtk_xtext_image_hit (GtkXText *xtext, int x, int y);
|
||||
|
||||
static inline void
|
||||
gtk_xtext_cursor_unref (GdkCursor *cursor)
|
||||
@@ -581,6 +679,7 @@ backend_init (GtkXText *xtext)
|
||||
{
|
||||
if (xtext->layout == NULL)
|
||||
{
|
||||
emoji_font_apply (GTK_WIDGET (xtext));
|
||||
xtext->layout = gtk_widget_create_pango_layout (GTK_WIDGET (xtext), 0);
|
||||
if (xtext->font)
|
||||
pango_layout_set_font_description (xtext->layout, xtext->font->font);
|
||||
@@ -1062,6 +1161,8 @@ gtk_xtext_dispose (GObject *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);
|
||||
|
||||
if (G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose)
|
||||
@@ -1443,7 +1544,7 @@ gtk_xtext_find_x (GtkXText * xtext, int x, textentry * ent, int subline,
|
||||
|
||||
str = ent->str + gtk_xtext_find_subline (xtext, ent, subline);
|
||||
if (str >= ent->str + ent->str_len)
|
||||
return 0;
|
||||
return ent->str_len; /* e.g. the blank rows of an inline image */
|
||||
|
||||
/* Let user select left a few pixels to grab hidden text e.g. '<' */
|
||||
if (x < indent - xtext->space_width)
|
||||
@@ -2143,6 +2244,7 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
|
||||
int len;
|
||||
int out_of_bounds = 0;
|
||||
int len_to_offset = 0;
|
||||
gboolean has_hidden;
|
||||
|
||||
ent = gtk_xtext_find_char (xtext, x, y, &offset, &out_of_bounds);
|
||||
if (ent == NULL || out_of_bounds || offset < 0 || offset >= ent->str_len)
|
||||
@@ -2187,10 +2289,15 @@ gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
|
||||
if (ret_len)
|
||||
*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);
|
||||
|
||||
/* avoid turning the cursor into a hand for non-url part of the word */
|
||||
if (xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
|
||||
if (!has_hidden &&
|
||||
xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
|
||||
{
|
||||
int start, end;
|
||||
url_last (&start, &end);
|
||||
@@ -2274,12 +2381,43 @@ gtk_xtext_unrender_hilight (GtkXText *xtext)
|
||||
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
|
||||
gtk_xtext_leave_notify (GtkWidget * widget, GdkEventCrossing * event)
|
||||
{
|
||||
GtkXText *xtext = GTK_XTEXT (widget);
|
||||
GdkWindow *window = gtk_widget_get_window (widget);
|
||||
|
||||
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
|
||||
|
||||
if (xtext->cursor_hand)
|
||||
{
|
||||
gtk_xtext_unrender_hilight (xtext);
|
||||
@@ -2343,6 +2481,7 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
|
||||
GSList *slp = NULL;
|
||||
unsigned char *word;
|
||||
int word_type = 0;
|
||||
gboolean placeholder = FALSE;
|
||||
|
||||
word = gtk_xtext_get_word (xtext, x, y, word_ent, offset, len, &slp);
|
||||
if (word)
|
||||
@@ -2352,7 +2491,18 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
|
||||
word_type = xtext->urlcheck_function (GTK_WIDGET (xtext), word);
|
||||
if (word_type > 0)
|
||||
{
|
||||
if (url_last (&laststart, &lastend))
|
||||
/* words with hidden text are placeholders (e.g. inline image
|
||||
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;
|
||||
offlen_t *meta;
|
||||
@@ -2382,6 +2532,9 @@ gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent,
|
||||
}
|
||||
g_slist_free_full (slp, g_free);
|
||||
|
||||
if (!placeholder)
|
||||
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
|
||||
|
||||
return word_type;
|
||||
}
|
||||
|
||||
@@ -2513,6 +2666,20 @@ gtk_xtext_motion_notify (GtkWidget * widget, GdkEventMotion * event)
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL);
|
||||
@@ -2620,6 +2787,46 @@ gtk_xtext_unselect (GtkXText *xtext)
|
||||
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
|
||||
gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
|
||||
{
|
||||
@@ -2692,8 +2899,15 @@ gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
|
||||
|
||||
if (!gtk_xtext_is_selecting (xtext))
|
||||
{
|
||||
word = gtk_xtext_get_word (xtext, event_x, event_y, 0, 0, 0, 0);
|
||||
textentry *word_ent = NULL;
|
||||
|
||||
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);
|
||||
xtext->clicked_ent = NULL;
|
||||
xtext->clicked_image_ent = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2720,14 +2934,16 @@ gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event)
|
||||
|
||||
if (event->button == 3 || event->button == 2) /* right/middle click */
|
||||
{
|
||||
word = gtk_xtext_get_word (xtext, x, y, 0, 0, 0, 0);
|
||||
word = gtk_xtext_get_word (xtext, x, y, &ent, 0, 0, 0);
|
||||
if (word)
|
||||
{
|
||||
xtext->clicked_ent = ent;
|
||||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
|
||||
word, event);
|
||||
} else
|
||||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
|
||||
"", event);
|
||||
xtext->clicked_ent = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -4105,6 +4321,7 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
|
||||
unsigned char *str;
|
||||
int indent, taken, entline, len, y, start_subline;
|
||||
int emphasis = 0;
|
||||
int image_rows;
|
||||
|
||||
entline = taken = 0;
|
||||
str = ent->str;
|
||||
@@ -4175,6 +4392,56 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
|
||||
}
|
||||
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));
|
||||
|
||||
return taken;
|
||||
@@ -4333,6 +4600,7 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
|
||||
unsigned char *str;
|
||||
int indent, len;
|
||||
int win_width;
|
||||
int image_rows, i;
|
||||
|
||||
g_slist_free (ent->sublines);
|
||||
ent->sublines = NULL;
|
||||
@@ -4342,21 +4610,27 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
|
||||
if (win_width >= ent->indent + ent->str_width)
|
||||
{
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len));
|
||||
ent->subline_count = 1;
|
||||
return ent->subline_count;
|
||||
}
|
||||
|
||||
indent = ent->indent;
|
||||
str = ent->str;
|
||||
|
||||
do
|
||||
else
|
||||
{
|
||||
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
|
||||
indent = buf->indent;
|
||||
str += len;
|
||||
indent = ent->indent;
|
||||
str = ent->str;
|
||||
|
||||
do
|
||||
{
|
||||
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
|
||||
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
|
||||
indent = buf->indent;
|
||||
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);
|
||||
return ent->subline_count;
|
||||
@@ -4779,6 +5053,8 @@ gtk_xtext_kill_ent (xtext_buffer *buffer, textentry *ent)
|
||||
ent->sublines = NULL;
|
||||
ent->subline_count = 0;
|
||||
|
||||
gtk_xtext_ent_free_image (ent);
|
||||
|
||||
g_free (ent);
|
||||
return visible;
|
||||
}
|
||||
@@ -4916,6 +5192,7 @@ gtk_xtext_clear (xtext_buffer *buf, int lines)
|
||||
while (buf->text_first)
|
||||
{
|
||||
next = buf->text_first->next;
|
||||
gtk_xtext_ent_free_image (buf->text_first);
|
||||
g_free (buf->text_first);
|
||||
buf->text_first = next;
|
||||
}
|
||||
@@ -5435,6 +5712,9 @@ gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp)
|
||||
ent->next = NULL;
|
||||
ent->marks = NULL;
|
||||
ent->subline_count = 0;
|
||||
ent->image = NULL;
|
||||
ent->image_full = NULL;
|
||||
ent->image_url = NULL;
|
||||
|
||||
if (ent->indent < MARGIN)
|
||||
ent->indent = MARGIN; /* 2 pixels is the left margin */
|
||||
@@ -5955,13 +6235,260 @@ gtk_xtext_buffer_free (xtext_buffer *buf)
|
||||
gtk_xtext_search_fini (buf);
|
||||
}
|
||||
|
||||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_buf, buf);
|
||||
|
||||
ent = buf->text_first;
|
||||
while (ent)
|
||||
{
|
||||
next = ent->next;
|
||||
gtk_xtext_ent_free_image (ent);
|
||||
g_free (ent);
|
||||
ent = next;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ struct _GtkXText
|
||||
int hilight_end;
|
||||
time_t tooltip_stamp;
|
||||
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 */
|
||||
|
||||
@@ -254,6 +255,9 @@ struct _GtkXText
|
||||
unsigned int force_render:1;
|
||||
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 */
|
||||
unsigned int auto_indent:1;
|
||||
unsigned int thinline:1;
|
||||
@@ -309,4 +313,19 @@ void gtk_xtext_buffer_free (xtext_buffer *buf);
|
||||
void gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render);
|
||||
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
|
||||
|
||||
280
tools/gen-emoji-data.py
Executable file
280
tools/gen-emoji-data.py
Executable 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()
|
||||
@@ -37,6 +37,7 @@
|
||||
<None Include="$(DepsRoot)\bin\*fribidi*.dll" />
|
||||
<None Include="$(DepsRoot)\bin\*fontconfig*.dll" />
|
||||
<None Include="$(DepsRoot)\bin\*gdk_pixbuf*.dll" />
|
||||
<None Include="$(DepsRoot)\bin\rsvg-2-2.dll" />
|
||||
<None Include="$(DepsRoot)\bin\*gdk-3*.dll" />
|
||||
<None Include="$(DepsRoot)\bin\*epoxy*.dll" />
|
||||
<None Include="$(DepsRoot)\bin\*gio-2*.dll" />
|
||||
@@ -78,10 +79,12 @@
|
||||
<Gtk3Immodules Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\immodules\**\*" />
|
||||
<Gtk3PrintBackends Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\printbackends\**\*" />
|
||||
<GSettingsSchemas Include="$(DepsRoot)\share\glib-2.0\schemas\*" />
|
||||
<GdkPixbufLoaders Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders\**\*" />
|
||||
<GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" />
|
||||
<!-- Preserve the version/loaders directories used by the relative cache. -->
|
||||
<GdkPixbufRuntime Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\*" />
|
||||
|
||||
<FontConfig Include="$(DepsRoot)\etc\fonts\*" />
|
||||
<FontConfig Include="$(DepsRoot)\etc\fonts\**\*" />
|
||||
<EmojiFont Include="..\..\data\fonts\NotoColorEmoji.ttf" />
|
||||
<EmojiFontLicense Include="..\..\data\fonts\OFL.txt;..\..\data\fonts\README.md" />
|
||||
<Share Include="share\**\*" />
|
||||
<Locale Include="$(ZoiteChatBin)locale\**\*;$(DepsRoot)\share\locale\**\*" />
|
||||
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
|
||||
@@ -90,11 +93,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(None)" DestinationFolder="$(ZoiteChatRel)" />
|
||||
<Copy SourceFiles="@(FontConfig)" DestinationFolder="$(ZoiteChatRel)\etc\fonts" />
|
||||
<Copy SourceFiles="@(FontConfig)" DestinationFiles="@(FontConfig->'$(ZoiteChatRel)\etc\fonts\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(EmojiFont)" DestinationFolder="$(ZoiteChatRel)\share\fonts\zoitechat" />
|
||||
<Copy SourceFiles="@(EmojiFontLicense)" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat\fonts" />
|
||||
<Copy SourceFiles="@(Gtk3Immodules)" DestinationFiles="@(Gtk3Immodules->'$(ZoiteChatRel)\lib\gtk-3.0\3.0.0\immodules\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(Gtk3PrintBackends)" DestinationFiles="@(Gtk3PrintBackends->'$(ZoiteChatRel)\lib\gtk-3.0\3.0.0\printbackends\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(GdkPixbufLoaders)" DestinationFiles="@(GdkPixbufLoaders->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(GdkPixbufLoaderCache)" DestinationFiles="@(GdkPixbufLoaderCache->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(GdkPixbufRuntime)" DestinationFiles="@(GdkPixbufRuntime->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="$(DepsRoot)\share\doc\librsvg\COPYING.LIB" DestinationFolder="$(ZoiteChatRel)\share\doc\librsvg" />
|
||||
<Copy SourceFiles="@(GSettingsSchemas)" DestinationFiles="@(GSettingsSchemas->'$(ZoiteChatRel)\share\glib-2.0\schemas\%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
<Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" />
|
||||
|
||||
46
win32/emoji-tests.vcxproj
Normal file
46
win32/emoji-tests.vcxproj
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{BF0C17A9-2562-4BDC-A6A2-34381C5C5173}</ProjectGuid>
|
||||
<SolutionDir>$(MSBuildThisFileDirectory)</SolutionDir>
|
||||
<EmojiTestKind Condition="'$(EmojiTestKind)'==''">ui</EmojiTestKind>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<Import Project="zoitechat.props" />
|
||||
<PropertyGroup>
|
||||
<TargetName>emoji-$(EmojiTestKind)-tests</TargetName>
|
||||
<OutDir>$(ZoiteChatBuild)\$(ZoiteChatPlatform)\tests\</OutDir>
|
||||
<IntDir>$(ZoiteChatObj)emoji-tests-$(EmojiTestKind)\</IntDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>WIN32;_WIN64;_CONSOLE;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(ZoiteChatLib);$(DepsRoot)\include;$(Glib);$(Gtk);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>$(Gtk3Lib);$(Gdk3Lib);atk-1.0.lib;gio-2.0.lib;gdk_pixbuf-2.0.lib;pangocairo-1.0.lib;pango-1.0.lib;pangoft2-1.0.lib;fontconfig.lib;cairo.lib;gobject-2.0.lib;glib-2.0.lib;$(IntlLib);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\common\emoji-data.c" />
|
||||
<ClCompile Include="..\src\fe-gtk\tests\test-emoji-font.c" Condition="'$(EmojiTestKind)'=='font'" />
|
||||
<ClCompile Include="..\src\common\tests\test-emoji-data.c" Condition="'$(EmojiTestKind)'=='data'" />
|
||||
<ClCompile Include="..\src\fe-gtk\emoji-font.c" Condition="'$(EmojiTestKind)'!='data'" />
|
||||
<ClCompile Include="..\src\fe-gtk\emoji-picker.c" Condition="'$(EmojiTestKind)'=='ui'" />
|
||||
<ClCompile Include="..\src\fe-gtk\tests\test-emoji-ui.c" Condition="'$(EmojiTestKind)'=='ui'" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -104,7 +104,10 @@ Source: "readme.url"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "cert.pem"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "share\xml\*"; DestDir: "{app}\share\xml"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
|
||||
Source: "share\doc\zoitechat\*"; DestDir: "{app}\share\doc\zoitechat"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
|
||||
Source: "share\fonts\zoitechat\*"; DestDir: "{app}\share\fonts\zoitechat"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
|
||||
Source: "share\doc\WinSparkle\*"; DestDir: "{app}\share\doc\WinSparkle"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
|
||||
Source: "share\doc\librsvg\*"; DestDir: "{app}\share\doc\librsvg"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
|
||||
Source: "lib\gdk-pixbuf-2.0\*"; DestDir: "{app}\lib\gdk-pixbuf-2.0"; Flags: ignoreversion createallsubdirs recursesubdirs; Components: libs
|
||||
Source: "share\themes\MS-Windows\*"; DestDir: "{app}\share\themes\MS-Windows"; Flags: ignoreversion createallsubdirs recursesubdirs skipifsourcedoesntexist; Components: libs
|
||||
Source: "share\glib-2.0\schemas\*"; DestDir: "{app}\share\glib-2.0\schemas"; Flags: ignoreversion createallsubdirs recursesubdirs skipifsourcedoesntexist; Components: libs
|
||||
Source: "share\icons\hicolor\*"; DestDir: "{app}\share\icons\hicolor"; Flags: ignoreversion createallsubdirs recursesubdirs skipifsourcedoesntexist; Components: libs
|
||||
@@ -117,6 +120,7 @@ Source: "freetype-6.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: li
|
||||
Source: "fribidi-0.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "fontconfig-1.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "gdk_pixbuf-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "rsvg-2-2.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "gdk-3-vs17.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "epoxy-0.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
Source: "gio-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<UsingGtk3 Condition="Exists('$(DepsRoot)\\include\\gtk-3.0\\gtk\\gtk.h')">true</UsingGtk3>
|
||||
<Gtk3>$(DepsRoot)\include\gtk-3.0;$(DepsRoot)\lib\gtk-3.0\include</Gtk3>
|
||||
<GtkCommon>$(DepsRoot)\include\atk-1.0;$(DepsRoot)\include\cairo;$(DepsRoot)\include\pango-1.0;$(DepsRoot)\include\gdk-pixbuf-2.0;$(DepsRoot)\include\harfbuzz</GtkCommon>
|
||||
<GtkCommon>$(DepsRoot)\include\atk-1.0;$(DepsRoot)\include\cairo;$(DepsRoot)\include\pango-1.0;$(DepsRoot)\include\gdk-pixbuf-2.0;$(DepsRoot)\include\harfbuzz;$(DepsRoot)\include\freetype2</GtkCommon>
|
||||
<Gtk>$(Gtk3);$(GtkCommon)</Gtk>
|
||||
|
||||
<SslLegacyLib Condition="Exists('$(DepsRoot)\\lib\\ssleay32.lib')">ssleay32.lib</SslLegacyLib>
|
||||
|
||||
Reference in New Issue
Block a user