mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-09-23 16:17:10 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2922c9982a | ||
|
|
062c105bc1 | ||
|
|
18d77b7279 | ||
|
|
a1cd9897ea | ||
|
|
dad6abee58 | ||
|
|
2672f9b087 | ||
|
|
4315a75a12 | ||
|
|
fc4a7454ad | ||
|
|
87410aea50 | ||
|
|
de98d00a1c | ||
|
|
e94ffd9d79 | ||
|
|
d7aebb784d | ||
|
|
20b8cb939d | ||
|
|
ff4217c3d1 | ||
|
|
4fb72d30d8 | ||
|
|
d2b8fc0bda | ||
|
|
951181628a | ||
|
|
ccee274847 | ||
|
|
967f6728ef | ||
|
|
bf11ffb626 | ||
|
|
c12cabbe4e | ||
|
|
e997594db9 | ||
|
|
6f5e3b395f | ||
|
|
040021ee0e | ||
|
|
5018a3a848 | ||
|
|
df1c74a91d |
@@ -30,10 +30,10 @@ jobs:
|
||||
gettext \
|
||||
libcanberra-dev libglib2.0-dev \
|
||||
libarchive-dev \
|
||||
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth \
|
||||
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 \
|
||||
breeze-gtk-theme \
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
ninja -C build
|
||||
|
||||
- name: Test emoji catalog, font coverage and picker
|
||||
run: xvfb-run -a meson test -C build --print-errorlogs "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
|
||||
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: |
|
||||
@@ -135,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
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -179,6 +203,7 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
-------------------
|
||||
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+50
-34
@@ -6,50 +6,66 @@ sysinfo_sources = [
|
||||
sysinfo_deps = [
|
||||
libgio_dep,
|
||||
zoitechat_plugin_dep,
|
||||
common_sysinfo_deps,
|
||||
]
|
||||
|
||||
sysinfo_includes = []
|
||||
sysinfo_cargs = ['-DHAVE_CONFIG_H']
|
||||
|
||||
system = host_machine.system()
|
||||
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'darwin' or system == 'freebsd'
|
||||
sysinfo_includes += 'shared'
|
||||
sysinfo_sources += [
|
||||
'shared/df.c'
|
||||
]
|
||||
|
||||
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'freebsd'
|
||||
libpci = dependency('libpci', required: false, method: 'pkg-config')
|
||||
if libpci.found()
|
||||
sysinfo_deps += libpci
|
||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||
sysinfo_sources += 'unix/pci.c'
|
||||
|
||||
picidsdir = libpci.get_pkgconfig_variable('idsdir')
|
||||
pciids = join_paths(picidsdir, 'pci.ids')
|
||||
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
||||
endif
|
||||
if get_option('gtk-frontend')
|
||||
sysinfo_deps += dependency('gtk+-3.0', version: '>= 3.22')
|
||||
endif
|
||||
|
||||
sysinfo_includes += 'unix'
|
||||
sysinfo_sources += [
|
||||
'unix/backend.c',
|
||||
'unix/match.c',
|
||||
'unix/parse.c',
|
||||
]
|
||||
elif system == 'darwin'
|
||||
add_languages('objc')
|
||||
sysinfo_sources += 'osx/backend.m'
|
||||
if system == 'linux'
|
||||
if get_option('gtk-frontend')
|
||||
sysinfo_deps += dependency('gtk+-3.0', version: '>= 3.22')
|
||||
endif
|
||||
|
||||
elif system == 'windows'
|
||||
libpci = dependency('libpci', required: false, method: 'pkg-config')
|
||||
if libpci.found()
|
||||
sysinfo_deps += libpci
|
||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||
endif
|
||||
|
||||
sysinfo_includes += 'unix'
|
||||
sysinfo_sources += 'unix/modern-backend.c'
|
||||
|
||||
elif system == 'freebsd' or system == 'gnu' or system.startswith('gnu/')
|
||||
sysinfo_includes += ['shared', 'unix']
|
||||
sysinfo_sources += [
|
||||
'win32/backend.c',
|
||||
'../../src/common/sysinfo/win32/backend.c'
|
||||
'shared/df.c',
|
||||
'unix/backend.c',
|
||||
'unix/match.c',
|
||||
'unix/parse.c',
|
||||
]
|
||||
|
||||
libpci = dependency('libpci', required: false, method: 'pkg-config')
|
||||
if libpci.found()
|
||||
sysinfo_deps += libpci
|
||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||
sysinfo_sources += 'unix/pci.c'
|
||||
|
||||
picidsdir = libpci.get_variable(pkgconfig: 'idsdir')
|
||||
pciids = join_paths(picidsdir, 'pci.ids')
|
||||
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
||||
endif
|
||||
|
||||
if get_option('gtk-frontend')
|
||||
sysinfo_deps += dependency('gtk+-3.0', version: '>= 3.22')
|
||||
endif
|
||||
|
||||
elif system == 'darwin'
|
||||
sysinfo_includes += 'shared'
|
||||
sysinfo_sources += 'shared/df.c'
|
||||
add_languages('objc')
|
||||
sysinfo_sources += 'osx/backend.m'
|
||||
|
||||
elif system == 'windows'
|
||||
cc = meson.get_compiler('c')
|
||||
sysinfo_deps += [
|
||||
cc.find_library('advapi32', required: true),
|
||||
cc.find_library('dxgi', required: true),
|
||||
cc.find_library('dxguid', required: true),
|
||||
cc.find_library('iphlpapi', required: true),
|
||||
cc.find_library('winmm', required: true),
|
||||
]
|
||||
sysinfo_sources += 'win32/backend.c'
|
||||
else
|
||||
error('sysinfo: Unknown system?')
|
||||
endif
|
||||
|
||||
@@ -40,8 +40,8 @@ static zoitechat_plugin *ph;
|
||||
|
||||
static char name[] = "Sysinfo";
|
||||
static char desc[] = "Display info about your hardware and OS";
|
||||
static char version[] = "1.0";
|
||||
static char sysinfo_help[] = "SysInfo Usage:\n /SYSINFO [-e|-o] [CLIENT|UI|OS|CPU|RAM|STORAGE|GPU|CHIPSET|SOUND|ETHERNET|UPTIME], print various details about your system or print a summary without arguments\n /SYSINFO SET <variable>\n";
|
||||
static char version[] = "2.0";
|
||||
static char sysinfo_help[] = "SysInfo Usage:\n /SYSINFO [-e|-o] [CLIENT|UI|OS|CPU|RAM|MEMORY|STORAGE|DISK|GPU|VGA|CHIPSET|SOUND|NETWORK|ETHERNET|UPTIME], print various details about your system or print a summary without arguments\n /SYSINFO SET <variable>\n";
|
||||
|
||||
typedef struct
|
||||
{
|
||||
@@ -68,11 +68,11 @@ static hwinfo hwinfos[] = {
|
||||
{"os", "OS", sysinfo_backend_get_os},
|
||||
{"cpu", "CPU", sysinfo_backend_get_cpu},
|
||||
{"memory", "Memory", sysinfo_backend_get_memory},
|
||||
{"storage", "Storage", sysinfo_backend_get_disk, TRUE},
|
||||
{"storage", "Storage", sysinfo_backend_get_disk},
|
||||
{"gpu", "GPU", sysinfo_backend_get_gpu},
|
||||
{"chipset", "CHIPSET", sysinfo_backend_get_chipset, TRUE},
|
||||
{"chipset", "Chipset", sysinfo_backend_get_chipset, TRUE},
|
||||
{"sound", "Sound", sysinfo_backend_get_sound, TRUE},
|
||||
{"ethernet", "Ethernet", sysinfo_backend_get_network, TRUE},
|
||||
{"ethernet", "Network", sysinfo_backend_get_network, TRUE},
|
||||
{"uptime", "Uptime", sysinfo_backend_get_uptime},
|
||||
{NULL, NULL},
|
||||
};
|
||||
@@ -115,14 +115,30 @@ print_summary (gboolean announce)
|
||||
g_free (output);
|
||||
}
|
||||
|
||||
static const char *
|
||||
normalize_info_name (const char *info)
|
||||
{
|
||||
if (!g_ascii_strcasecmp (info, "ram"))
|
||||
return "memory";
|
||||
if (!g_ascii_strcasecmp (info, "disk"))
|
||||
return "storage";
|
||||
if (!g_ascii_strcasecmp (info, "vga"))
|
||||
return "gpu";
|
||||
if (!g_ascii_strcasecmp (info, "network"))
|
||||
return "ethernet";
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
static void
|
||||
print_info (char *info, gboolean announce)
|
||||
{
|
||||
const char *normalized = normalize_info_name (info);
|
||||
int i;
|
||||
|
||||
for (i = 0; hwinfos[i].name != NULL; i++)
|
||||
{
|
||||
if (!g_ascii_strcasecmp (info, hwinfos[i].name))
|
||||
if (!g_ascii_strcasecmp (normalized, hwinfos[i].name))
|
||||
{
|
||||
char *str = hwinfos[i].callback();
|
||||
if (str)
|
||||
@@ -183,12 +199,17 @@ sysinfo_set_pref (char *key, char *value)
|
||||
}
|
||||
else if (g_str_has_prefix (key, "hide_"))
|
||||
{
|
||||
const char *normalized = normalize_info_name (key + 5);
|
||||
int i;
|
||||
|
||||
for (i = 0; hwinfos[i].name != NULL; i++)
|
||||
{
|
||||
if (!strcmp (key + 5, hwinfos[i].name))
|
||||
if (!strcmp (normalized, hwinfos[i].name))
|
||||
{
|
||||
sysinfo_set_pref_real (key, value, hwinfos[i].def);
|
||||
char canonical[32];
|
||||
|
||||
g_snprintf (canonical, sizeof (canonical), "hide_%s", hwinfos[i].name);
|
||||
sysinfo_set_pref_real (canonical, value, hwinfos[i].def);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+709
-83
@@ -1,28 +1,34 @@
|
||||
/* ZoiteChat
|
||||
* Copyright (c) 2011-2012 Berke Viktor.
|
||||
/*
|
||||
* Modern Windows SysInfo backend for ZoiteChat.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS 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. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
* Avoids WMI in the plugin hot path and uses native Win32/DXGI APIs.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0601
|
||||
#elif _WIN32_WINNT < 0x0601
|
||||
#undef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0601
|
||||
#endif
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <wbemidl.h>
|
||||
#include <winternl.h>
|
||||
#include <dxgi.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <ipifcons.h>
|
||||
#include <mmsystem.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma comment(lib, "advapi32.lib")
|
||||
#pragma comment(lib, "dxgi.lib")
|
||||
#pragma comment(lib, "dxguid.lib")
|
||||
#pragma comment(lib, "iphlpapi.lib")
|
||||
#pragma comment(lib, "winmm.lib")
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
@@ -30,63 +36,548 @@
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "../../../src/common/sysinfo/sysinfo.h"
|
||||
|
||||
#include "../format.h"
|
||||
#include "../sysinfo-backend.h"
|
||||
|
||||
static char *get_memory_info (void);
|
||||
#define SYSINFO_SEP " \xC2\xB7 "
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_sound (void)
|
||||
typedef struct
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
char *os;
|
||||
char *cpu;
|
||||
char *gpu;
|
||||
char *sound;
|
||||
} WinStaticSnapshot;
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_network (void)
|
||||
static WinStaticSnapshot snapshot;
|
||||
static gsize os_initialized = 0;
|
||||
static gsize cpu_initialized = 0;
|
||||
static gsize gpu_initialized = 0;
|
||||
static gsize sound_initialized = 0;
|
||||
|
||||
static char *wide_to_utf8 (const WCHAR *value);
|
||||
static char *read_registry_string (HKEY root, const WCHAR *subkey, const WCHAR *value_name);
|
||||
static gboolean read_registry_dword (HKEY root, const WCHAR *subkey, const WCHAR *value_name, DWORD *value);
|
||||
static const char *get_native_arch (void);
|
||||
static DWORD get_windows_build_number (void);
|
||||
static char *build_os_string (void);
|
||||
static DWORD count_physical_cores (void);
|
||||
static char *build_cpu_string (void);
|
||||
static char *build_gpu_string (void);
|
||||
static char *build_sound_string (void);
|
||||
static void ensure_os_snapshot (void);
|
||||
static void ensure_cpu_snapshot (void);
|
||||
static void ensure_gpu_snapshot (void);
|
||||
static void ensure_sound_snapshot (void);
|
||||
static char *format_usage (guint64 total, guint64 available);
|
||||
static char *join_ptr_array (GPtrArray *items, const char *separator);
|
||||
static void ptr_array_add_unique (GPtrArray *items, const char *value);
|
||||
static gboolean adapter_looks_virtual (const char *name);
|
||||
|
||||
static char *
|
||||
wide_to_utf8 (const WCHAR *value)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_uptime (void)
|
||||
{
|
||||
return sysinfo_format_uptime (GetTickCount64 () / 1000);
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_disk (void)
|
||||
{
|
||||
guint64 hdd_capacity;
|
||||
guint64 hdd_free_space;
|
||||
|
||||
sysinfo_get_hdd_info (&hdd_capacity, &hdd_free_space);
|
||||
|
||||
if (hdd_capacity != 0)
|
||||
if (!value || value[0] == L'\0')
|
||||
{
|
||||
return sysinfo_format_disk(hdd_capacity, hdd_free_space);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
return g_utf16_to_utf8 ((const gunichar2 *) value, -1, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
static char *
|
||||
read_registry_string (HKEY root, const WCHAR *subkey, const WCHAR *value_name)
|
||||
{
|
||||
HKEY key;
|
||||
DWORD type = 0;
|
||||
DWORD size = 0;
|
||||
WCHAR *buffer;
|
||||
char *result = NULL;
|
||||
LONG status;
|
||||
|
||||
status = RegOpenKeyExW (root, subkey, 0, KEY_READ | KEY_WOW64_64KEY, &key);
|
||||
if (status != ERROR_SUCCESS)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
status = RegQueryValueExW (key, value_name, NULL, &type, NULL, &size);
|
||||
if (status != ERROR_SUCCESS || (type != REG_SZ && type != REG_EXPAND_SZ) || size < sizeof (WCHAR))
|
||||
{
|
||||
RegCloseKey (key);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buffer = g_malloc0 ((gsize) size + sizeof (WCHAR));
|
||||
status = RegQueryValueExW (key, value_name, NULL, &type, (LPBYTE) buffer, &size);
|
||||
if (status == ERROR_SUCCESS)
|
||||
{
|
||||
result = wide_to_utf8 (buffer);
|
||||
if (result)
|
||||
{
|
||||
g_strstrip (result);
|
||||
}
|
||||
}
|
||||
|
||||
g_free (buffer);
|
||||
RegCloseKey (key);
|
||||
return result;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
read_registry_dword (HKEY root, const WCHAR *subkey, const WCHAR *value_name, DWORD *value)
|
||||
{
|
||||
HKEY key;
|
||||
DWORD type = 0;
|
||||
DWORD size = sizeof (*value);
|
||||
LONG status;
|
||||
|
||||
status = RegOpenKeyExW (root, subkey, 0, KEY_READ | KEY_WOW64_64KEY, &key);
|
||||
if (status != ERROR_SUCCESS)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
status = RegQueryValueExW (key, value_name, NULL, &type, (LPBYTE) value, &size);
|
||||
RegCloseKey (key);
|
||||
return status == ERROR_SUCCESS && type == REG_DWORD;
|
||||
}
|
||||
|
||||
static const char *
|
||||
get_native_arch (void)
|
||||
{
|
||||
SYSTEM_INFO info;
|
||||
|
||||
GetNativeSystemInfo (&info);
|
||||
switch (info.wProcessorArchitecture)
|
||||
{
|
||||
case PROCESSOR_ARCHITECTURE_AMD64:
|
||||
return "x64";
|
||||
case PROCESSOR_ARCHITECTURE_ARM64:
|
||||
return "ARM64";
|
||||
case PROCESSOR_ARCHITECTURE_INTEL:
|
||||
return "x86";
|
||||
default:
|
||||
return "unknown-arch";
|
||||
}
|
||||
}
|
||||
|
||||
static DWORD
|
||||
get_windows_build_number (void)
|
||||
{
|
||||
typedef LONG (WINAPI *RtlGetVersionFn) (PRTL_OSVERSIONINFOW);
|
||||
HMODULE ntdll;
|
||||
RtlGetVersionFn rtl_get_version;
|
||||
RTL_OSVERSIONINFOW version;
|
||||
DWORD build = 0;
|
||||
|
||||
ntdll = GetModuleHandleW (L"ntdll.dll");
|
||||
if (ntdll)
|
||||
{
|
||||
rtl_get_version = (RtlGetVersionFn) GetProcAddress (ntdll, "RtlGetVersion");
|
||||
if (rtl_get_version)
|
||||
{
|
||||
ZeroMemory (&version, sizeof (version));
|
||||
version.dwOSVersionInfoSize = sizeof (version);
|
||||
if (rtl_get_version (&version) == 0)
|
||||
{
|
||||
build = version.dwBuildNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return build;
|
||||
}
|
||||
|
||||
static char *
|
||||
build_os_string (void)
|
||||
{
|
||||
static const WCHAR current_version_key[] = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
|
||||
char *product;
|
||||
char *display_version;
|
||||
char *registry_build;
|
||||
char *fixed_product = NULL;
|
||||
char *result;
|
||||
DWORD ubr = 0;
|
||||
DWORD build;
|
||||
guint64 parsed_build = 0;
|
||||
|
||||
product = read_registry_string (HKEY_LOCAL_MACHINE, current_version_key, L"ProductName");
|
||||
display_version = read_registry_string (HKEY_LOCAL_MACHINE, current_version_key, L"DisplayVersion");
|
||||
registry_build = read_registry_string (HKEY_LOCAL_MACHINE, current_version_key, L"CurrentBuildNumber");
|
||||
read_registry_dword (HKEY_LOCAL_MACHINE, current_version_key, L"UBR", &ubr);
|
||||
|
||||
build = get_windows_build_number ();
|
||||
if (registry_build)
|
||||
{
|
||||
parsed_build = g_ascii_strtoull (registry_build, NULL, 10);
|
||||
if (parsed_build != 0)
|
||||
{
|
||||
build = (DWORD) parsed_build;
|
||||
}
|
||||
}
|
||||
|
||||
if (!product)
|
||||
{
|
||||
product = g_strdup (build >= 22000 ? "Windows 11" : "Windows");
|
||||
}
|
||||
else if (build >= 22000 && g_str_has_prefix (product, "Windows 10"))
|
||||
{
|
||||
fixed_product = g_strdup_printf ("Windows 11%s", product + strlen ("Windows 10"));
|
||||
g_free (product);
|
||||
product = fixed_product;
|
||||
}
|
||||
|
||||
if (display_version && build != 0)
|
||||
{
|
||||
result = g_strdup_printf ("%s %s" SYSINFO_SEP "build %lu.%lu" SYSINFO_SEP "%s",
|
||||
product, display_version, (unsigned long) build, (unsigned long) ubr, get_native_arch ());
|
||||
}
|
||||
else if (build != 0)
|
||||
{
|
||||
result = g_strdup_printf ("%s" SYSINFO_SEP "build %lu.%lu" SYSINFO_SEP "%s",
|
||||
product, (unsigned long) build, (unsigned long) ubr, get_native_arch ());
|
||||
}
|
||||
else
|
||||
{
|
||||
result = g_strdup_printf ("%s" SYSINFO_SEP "%s", product, get_native_arch ());
|
||||
}
|
||||
|
||||
g_free (product);
|
||||
g_free (display_version);
|
||||
g_free (registry_build);
|
||||
return result;
|
||||
}
|
||||
|
||||
static DWORD
|
||||
count_physical_cores (void)
|
||||
{
|
||||
DWORD length = 0;
|
||||
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX buffer;
|
||||
BYTE *cursor;
|
||||
BYTE *end;
|
||||
DWORD cores = 0;
|
||||
|
||||
GetLogicalProcessorInformationEx (RelationProcessorCore, NULL, &length);
|
||||
if (GetLastError () != ERROR_INSUFFICIENT_BUFFER || length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
buffer = g_malloc0 (length);
|
||||
if (!GetLogicalProcessorInformationEx (RelationProcessorCore, buffer, &length))
|
||||
{
|
||||
g_free (buffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cursor = (BYTE *) buffer;
|
||||
end = cursor + length;
|
||||
while (cursor < end)
|
||||
{
|
||||
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX info = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX) cursor;
|
||||
|
||||
if (info->Size == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (info->Relationship == RelationProcessorCore)
|
||||
{
|
||||
cores++;
|
||||
}
|
||||
cursor += info->Size;
|
||||
}
|
||||
|
||||
g_free (buffer);
|
||||
return cores;
|
||||
}
|
||||
|
||||
static char *
|
||||
build_cpu_string (void)
|
||||
{
|
||||
static const WCHAR cpu_key[] = L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0";
|
||||
char *name;
|
||||
DWORD cores;
|
||||
DWORD threads;
|
||||
SYSTEM_INFO info;
|
||||
char *result;
|
||||
|
||||
name = read_registry_string (HKEY_LOCAL_MACHINE, cpu_key, L"ProcessorNameString");
|
||||
if (!name)
|
||||
{
|
||||
name = g_strdup ("Unknown CPU");
|
||||
}
|
||||
|
||||
cores = count_physical_cores ();
|
||||
threads = GetActiveProcessorCount (ALL_PROCESSOR_GROUPS);
|
||||
if (threads == 0)
|
||||
{
|
||||
GetNativeSystemInfo (&info);
|
||||
threads = info.dwNumberOfProcessors;
|
||||
}
|
||||
if (cores != 0)
|
||||
{
|
||||
result = g_strdup_printf ("%s" SYSINFO_SEP "%luC/%luT",
|
||||
name, (unsigned long) cores, (unsigned long) threads);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Logical processor count cannot establish the physical core count. */
|
||||
result = g_strdup_printf ("%s" SYSINFO_SEP "%luT", name, (unsigned long) threads);
|
||||
}
|
||||
g_free (name);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char *
|
||||
join_ptr_array (GPtrArray *items, const char *separator)
|
||||
{
|
||||
GString *joined;
|
||||
guint i;
|
||||
|
||||
if (!items || items->len == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
joined = g_string_new (NULL);
|
||||
for (i = 0; i < items->len; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
g_string_append (joined, separator);
|
||||
}
|
||||
g_string_append (joined, g_ptr_array_index (items, i));
|
||||
}
|
||||
return g_string_free (joined, FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
ptr_array_add_unique (GPtrArray *items, const char *value)
|
||||
{
|
||||
guint i;
|
||||
|
||||
if (!value || value[0] == '\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < items->len; i++)
|
||||
{
|
||||
if (g_ascii_strcasecmp (g_ptr_array_index (items, i), value) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_ptr_array_add (items, g_strdup (value));
|
||||
}
|
||||
|
||||
static char *
|
||||
build_gpu_string (void)
|
||||
{
|
||||
IDXGIFactory1 *factory = NULL;
|
||||
GPtrArray *adapters;
|
||||
UINT index;
|
||||
HRESULT hr;
|
||||
char *result;
|
||||
|
||||
hr = CreateDXGIFactory1 (&IID_IDXGIFactory1, (void **) &factory);
|
||||
if (FAILED (hr) || !factory)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
adapters = g_ptr_array_new_with_free_func (g_free);
|
||||
for (index = 0; ; index++)
|
||||
{
|
||||
IDXGIAdapter1 *adapter = NULL;
|
||||
DXGI_ADAPTER_DESC1 desc;
|
||||
char *name;
|
||||
char *label;
|
||||
|
||||
hr = factory->lpVtbl->EnumAdapters1 (factory, index, &adapter);
|
||||
if (hr == DXGI_ERROR_NOT_FOUND)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (FAILED (hr) || !adapter)
|
||||
{
|
||||
/* A failed enumeration is not another adapter. Retrying every
|
||||
* index can freeze the client indefinitely after a device error. */
|
||||
break;
|
||||
}
|
||||
|
||||
ZeroMemory (&desc, sizeof (desc));
|
||||
hr = adapter->lpVtbl->GetDesc1 (adapter, &desc);
|
||||
adapter->lpVtbl->Release (adapter);
|
||||
if (FAILED (hr) || (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
name = wide_to_utf8 (desc.Description);
|
||||
if (!name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g_strstrip (name);
|
||||
|
||||
if ((guint64) desc.DedicatedVideoMemory >= (512ULL * 1024ULL * 1024ULL))
|
||||
{
|
||||
char *vram = g_format_size_full ((guint64) desc.DedicatedVideoMemory, G_FORMAT_SIZE_IEC_UNITS);
|
||||
label = g_strdup_printf ("%s (%s VRAM)", name, vram);
|
||||
g_free (vram);
|
||||
}
|
||||
else
|
||||
{
|
||||
label = g_strdup (name);
|
||||
}
|
||||
|
||||
ptr_array_add_unique (adapters, label);
|
||||
g_free (label);
|
||||
g_free (name);
|
||||
}
|
||||
|
||||
factory->lpVtbl->Release (factory);
|
||||
result = join_ptr_array (adapters, ", ");
|
||||
g_ptr_array_free (adapters, TRUE);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char *
|
||||
build_sound_string (void)
|
||||
{
|
||||
UINT count;
|
||||
UINT i;
|
||||
GPtrArray *devices;
|
||||
char *result;
|
||||
|
||||
count = waveOutGetNumDevs ();
|
||||
if (count == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
devices = g_ptr_array_new_with_free_func (g_free);
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
WAVEOUTCAPSW caps;
|
||||
char *name;
|
||||
|
||||
ZeroMemory (&caps, sizeof (caps));
|
||||
if (waveOutGetDevCapsW ((UINT_PTR) i, &caps, sizeof (caps)) != MMSYSERR_NOERROR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
name = wide_to_utf8 (caps.szPname);
|
||||
if (name)
|
||||
{
|
||||
g_strstrip (name);
|
||||
ptr_array_add_unique (devices, name);
|
||||
g_free (name);
|
||||
}
|
||||
}
|
||||
|
||||
result = join_ptr_array (devices, ", ");
|
||||
g_ptr_array_free (devices, TRUE);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_os_snapshot (void)
|
||||
{
|
||||
if (g_once_init_enter (&os_initialized))
|
||||
{
|
||||
snapshot.os = build_os_string ();
|
||||
g_once_init_leave (&os_initialized, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_cpu_snapshot (void)
|
||||
{
|
||||
if (g_once_init_enter (&cpu_initialized))
|
||||
{
|
||||
snapshot.cpu = build_cpu_string ();
|
||||
g_once_init_leave (&cpu_initialized, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_gpu_snapshot (void)
|
||||
{
|
||||
if (g_once_init_enter (&gpu_initialized))
|
||||
{
|
||||
snapshot.gpu = build_gpu_string ();
|
||||
g_once_init_leave (&gpu_initialized, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
ensure_sound_snapshot (void)
|
||||
{
|
||||
if (g_once_init_enter (&sound_initialized))
|
||||
{
|
||||
snapshot.sound = build_sound_string ();
|
||||
g_once_init_leave (&sound_initialized, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static char *
|
||||
format_usage (guint64 total, guint64 available)
|
||||
{
|
||||
guint64 used;
|
||||
char *used_fmt;
|
||||
char *total_fmt;
|
||||
char *result;
|
||||
double percent;
|
||||
|
||||
if (total == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (available > total)
|
||||
{
|
||||
available = total;
|
||||
}
|
||||
used = total - available;
|
||||
percent = ((double) used / (double) total) * 100.0;
|
||||
used_fmt = g_format_size_full (used, G_FORMAT_SIZE_IEC_UNITS);
|
||||
total_fmt = g_format_size_full (total, G_FORMAT_SIZE_IEC_UNITS);
|
||||
result = g_strdup_printf ("%s / %s used (%.0f%%)", used_fmt, total_fmt, percent);
|
||||
g_free (used_fmt);
|
||||
g_free (total_fmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_os (void)
|
||||
{
|
||||
ensure_os_snapshot ();
|
||||
return g_strdup (snapshot.os);
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_cpu (void)
|
||||
{
|
||||
return sysinfo_get_cpu ();
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_memory (void)
|
||||
{
|
||||
/* Memory information is always loaded dynamically since it includes the current amount of free memory */
|
||||
return get_memory_info ();
|
||||
ensure_cpu_snapshot ();
|
||||
return g_strdup (snapshot.cpu);
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_gpu (void)
|
||||
{
|
||||
return sysinfo_get_gpu ();
|
||||
ensure_gpu_snapshot ();
|
||||
return g_strdup (snapshot.gpu);
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_sound (void)
|
||||
{
|
||||
ensure_sound_snapshot ();
|
||||
return g_strdup (snapshot.sound);
|
||||
}
|
||||
|
||||
char *
|
||||
@@ -96,43 +587,178 @@ sysinfo_backend_get_chipset (void)
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_os (void)
|
||||
sysinfo_backend_get_memory (void)
|
||||
{
|
||||
return sysinfo_get_os ();
|
||||
}
|
||||
MEMORYSTATUSEX memory;
|
||||
|
||||
static char *get_memory_info (void)
|
||||
{
|
||||
MEMORYSTATUSEX meminfo = { 0 };
|
||||
meminfo.dwLength = sizeof (meminfo);
|
||||
|
||||
if (!GlobalMemoryStatusEx (&meminfo))
|
||||
ZeroMemory (&memory, sizeof (memory));
|
||||
memory.dwLength = sizeof (memory);
|
||||
if (!GlobalMemoryStatusEx (&memory))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return sysinfo_format_memory (meminfo.ullTotalPhys, meminfo.ullAvailPhys);
|
||||
return format_usage ((guint64) memory.ullTotalPhys, (guint64) memory.ullAvailPhys);
|
||||
}
|
||||
|
||||
static const char *sysinfo_detect_toolkit(void)
|
||||
char *
|
||||
sysinfo_backend_get_disk (void)
|
||||
{
|
||||
#if defined(USE_GTK_FRONTEND)
|
||||
return "GTK3";
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
WCHAR windows_dir[MAX_PATH];
|
||||
WCHAR volume_path[MAX_PATH];
|
||||
ULARGE_INTEGER available;
|
||||
ULARGE_INTEGER total;
|
||||
ULARGE_INTEGER total_free;
|
||||
char *usage;
|
||||
char *volume_utf8;
|
||||
char *result;
|
||||
|
||||
if (GetWindowsDirectoryW (windows_dir, G_N_ELEMENTS (windows_dir)) == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
if (!GetVolumePathNameW (windows_dir, volume_path, G_N_ELEMENTS (volume_path)))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
if (!GetDiskFreeSpaceExW (volume_path, &available, &total, &total_free))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
usage = format_usage ((guint64) total.QuadPart, (guint64) available.QuadPart);
|
||||
if (!usage)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
volume_utf8 = wide_to_utf8 (volume_path);
|
||||
if (!volume_utf8)
|
||||
{
|
||||
return usage;
|
||||
}
|
||||
g_strchomp (volume_utf8);
|
||||
result = g_strdup_printf ("%s" SYSINFO_SEP "%s", usage, volume_utf8);
|
||||
g_free (volume_utf8);
|
||||
g_free (usage);
|
||||
return result;
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_uptime (void)
|
||||
{
|
||||
return sysinfo_format_uptime ((gint64) (GetTickCount64 () / 1000ULL));
|
||||
}
|
||||
|
||||
static gboolean
|
||||
adapter_looks_virtual (const char *name)
|
||||
{
|
||||
char *lower;
|
||||
gboolean result;
|
||||
|
||||
if (!name)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
lower = g_ascii_strdown (name, -1);
|
||||
result = strstr (lower, "virtual") != NULL ||
|
||||
strstr (lower, "hyper-v") != NULL ||
|
||||
strstr (lower, "vmware") != NULL ||
|
||||
strstr (lower, "virtualbox") != NULL ||
|
||||
strstr (lower, "tap-windows") != NULL ||
|
||||
strstr (lower, "wireguard") != NULL ||
|
||||
strstr (lower, "vpn") != NULL;
|
||||
g_free (lower);
|
||||
return result;
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_network (void)
|
||||
{
|
||||
ULONG size = 16 * 1024;
|
||||
ULONG status;
|
||||
IP_ADAPTER_ADDRESSES *addresses;
|
||||
IP_ADAPTER_ADDRESSES *adapter;
|
||||
GPtrArray *physical;
|
||||
GPtrArray *fallback;
|
||||
GPtrArray *selected;
|
||||
char *result;
|
||||
|
||||
addresses = g_malloc0 (size);
|
||||
status = GetAdaptersAddresses (AF_UNSPEC,
|
||||
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
|
||||
NULL, addresses, &size);
|
||||
if (status == ERROR_BUFFER_OVERFLOW)
|
||||
{
|
||||
g_free (addresses);
|
||||
addresses = g_malloc0 (size);
|
||||
status = GetAdaptersAddresses (AF_UNSPEC,
|
||||
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
|
||||
NULL, addresses, &size);
|
||||
}
|
||||
if (status != NO_ERROR)
|
||||
{
|
||||
g_free (addresses);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
physical = g_ptr_array_new_with_free_func (g_free);
|
||||
fallback = g_ptr_array_new_with_free_func (g_free);
|
||||
for (adapter = addresses; adapter != NULL; adapter = adapter->Next)
|
||||
{
|
||||
char *description;
|
||||
char *friendly;
|
||||
const char *name;
|
||||
const char *type;
|
||||
char *label;
|
||||
|
||||
if (adapter->OperStatus != IfOperStatusUp || adapter->PhysicalAddressLength == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (adapter->IfType != IF_TYPE_ETHERNET_CSMACD && adapter->IfType != IF_TYPE_IEEE80211)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
description = wide_to_utf8 (adapter->Description);
|
||||
friendly = wide_to_utf8 (adapter->FriendlyName);
|
||||
name = description && description[0] ? description : friendly;
|
||||
if (!name)
|
||||
{
|
||||
g_free (description);
|
||||
g_free (friendly);
|
||||
continue;
|
||||
}
|
||||
|
||||
type = adapter->IfType == IF_TYPE_IEEE80211 ? "Wi-Fi" : "Ethernet";
|
||||
label = g_strdup_printf ("%s: %s", type, name);
|
||||
ptr_array_add_unique (fallback, label);
|
||||
if (!adapter_looks_virtual (name))
|
||||
{
|
||||
ptr_array_add_unique (physical, label);
|
||||
}
|
||||
|
||||
g_free (label);
|
||||
g_free (description);
|
||||
g_free (friendly);
|
||||
}
|
||||
g_free (addresses);
|
||||
|
||||
selected = physical->len != 0 ? physical : fallback;
|
||||
result = join_ptr_array (selected, ", ");
|
||||
g_ptr_array_free (physical, TRUE);
|
||||
g_ptr_array_free (fallback, TRUE);
|
||||
return result;
|
||||
}
|
||||
|
||||
char *
|
||||
sysinfo_backend_get_ui (void)
|
||||
{
|
||||
const char *toolkit = sysinfo_detect_toolkit();
|
||||
|
||||
/* On Windows we don't have X11/Wayland. Keep it simple. */
|
||||
if (toolkit)
|
||||
{
|
||||
return g_strdup_printf ("Windows / %s", toolkit);
|
||||
}
|
||||
|
||||
#if defined(USE_GTK_FRONTEND)
|
||||
return g_strdup ("Windows / GTK3");
|
||||
#else
|
||||
return g_strdup ("Windows");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ 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
|
||||
|
||||
@@ -443,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},
|
||||
@@ -565,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},
|
||||
@@ -778,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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -314,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);
|
||||
|
||||
+28
-19
@@ -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);
|
||||
|
||||
@@ -78,7 +78,9 @@ emoji_font_get_map (void)
|
||||
"<edit name='family' mode='assign'><string>" ZOITECHAT_EMOJI_FAMILY
|
||||
"</string></edit></match>"
|
||||
"<match target='pattern'><test name='family'><string>emoji</string></test>"
|
||||
"<edit name='family' mode='prepend' binding='strong'><string>"
|
||||
/* 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) &&
|
||||
|
||||
+8
-1
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
@@ -104,6 +105,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
||||
<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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
+265
-3
@@ -61,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"
|
||||
|
||||
@@ -926,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)));
|
||||
}
|
||||
@@ -3692,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;
|
||||
}
|
||||
|
||||
@@ -3704,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);
|
||||
}
|
||||
}
|
||||
@@ -3732,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;
|
||||
@@ -4679,9 +4927,19 @@ 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;
|
||||
|
||||
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");
|
||||
gtk_widget_set_no_show_all (gui->reply_box, TRUE);
|
||||
@@ -4712,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");
|
||||
|
||||
+57
-1
@@ -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);
|
||||
|
||||
@@ -25,6 +25,7 @@ zoitechat_gtk_sources = [
|
||||
'gtkutil.c',
|
||||
'icon-resolver.c',
|
||||
'ignoregui.c',
|
||||
'inline-image.c',
|
||||
'joind.c',
|
||||
'menu.c',
|
||||
'maingui.c',
|
||||
@@ -110,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'))
|
||||
@@ -139,7 +140,7 @@ executable('zoitechat',
|
||||
link_args: zoitechat_gtk_ldflags,
|
||||
pie: true,
|
||||
install: true,
|
||||
gui_app: true,
|
||||
win_subsystem: 'windows',
|
||||
)
|
||||
|
||||
emoji_ui_tests = executable('emoji_ui_tests',
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
#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)
|
||||
@@ -34,9 +80,7 @@ test_font_catalog (void)
|
||||
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||
if (run)
|
||||
{
|
||||
PangoFontDescription *actual = pango_font_describe (run->item->analysis.font);
|
||||
g_assert_cmpstr (pango_font_description_get_family (actual), ==, ZOITECHAT_EMOJI_FAMILY);
|
||||
pango_font_description_free (actual);
|
||||
assert_bundled_font (run->item->analysis.font, TRUE);
|
||||
}
|
||||
} while (pango_layout_iter_next_run (iter));
|
||||
pango_layout_iter_free (iter);
|
||||
@@ -68,9 +112,7 @@ test_plain_text_font (void)
|
||||
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||
if (run)
|
||||
{
|
||||
PangoFontDescription *actual = pango_font_describe (run->item->analysis.font);
|
||||
g_assert_cmpstr (pango_font_description_get_family (actual), !=, ZOITECHAT_EMOJI_FAMILY);
|
||||
pango_font_description_free (actual);
|
||||
assert_bundled_font (run->item->analysis.font, FALSE);
|
||||
}
|
||||
} while (pango_layout_iter_next_run (iter));
|
||||
pango_layout_iter_free (iter);
|
||||
@@ -101,9 +143,7 @@ test_implicit_emoji_font (void)
|
||||
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||
if (run)
|
||||
{
|
||||
PangoFontDescription *actual = pango_font_describe (run->item->analysis.font);
|
||||
g_assert_cmpstr (pango_font_description_get_family (actual), ==, ZOITECHAT_EMOJI_FAMILY);
|
||||
pango_font_description_free (actual);
|
||||
assert_bundled_font (run->item->analysis.font, TRUE);
|
||||
}
|
||||
} while (pango_layout_iter_next_run (iter));
|
||||
pango_layout_iter_free (iter);
|
||||
@@ -116,9 +156,19 @@ test_implicit_emoji_font (void)
|
||||
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);
|
||||
return g_test_run ();
|
||||
result = g_test_run ();
|
||||
g_free (bundled_checksum);
|
||||
g_hash_table_destroy (font_checksums);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,22 @@
|
||||
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)
|
||||
{
|
||||
@@ -125,6 +141,7 @@ main (int argc, char **argv)
|
||||
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);
|
||||
|
||||
+542
-17
@@ -93,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,
|
||||
@@ -214,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)
|
||||
@@ -1064,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)
|
||||
@@ -1445,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)
|
||||
@@ -2145,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)
|
||||
@@ -2189,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);
|
||||
@@ -2276,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);
|
||||
@@ -2345,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)
|
||||
@@ -2354,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;
|
||||
@@ -2384,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;
|
||||
}
|
||||
|
||||
@@ -2515,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);
|
||||
@@ -2622,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)
|
||||
{
|
||||
@@ -2694,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2722,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;
|
||||
}
|
||||
|
||||
@@ -4107,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;
|
||||
@@ -4177,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;
|
||||
@@ -4335,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;
|
||||
@@ -4344,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;
|
||||
@@ -4781,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;
|
||||
}
|
||||
@@ -4918,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;
|
||||
}
|
||||
@@ -5437,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 */
|
||||
@@ -5957,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
|
||||
|
||||
@@ -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,8 +79,8 @@
|
||||
<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\**\*" />
|
||||
<EmojiFont Include="..\..\data\fonts\NotoColorEmoji.ttf" />
|
||||
@@ -97,8 +98,8 @@
|
||||
<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" />
|
||||
|
||||
@@ -106,6 +106,8 @@ Source: "share\xml\*"; DestDir: "{app}\share\xml"; Flags: ignoreversion createal
|
||||
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
|
||||
@@ -118,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