mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-17 18:37:14 +00:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa7e20fee9 | ||
|
|
480b99b1e7 | ||
|
|
0a4367b1da | ||
|
|
39daca9d22 | ||
|
|
d614e25792 | ||
|
|
30209d0c99 | ||
|
|
e1c44feb01 | ||
|
|
5af8f4ede1 | ||
|
|
e11604fd62 | ||
|
|
a2e8d9087e | ||
|
|
edf268dc0f | ||
|
|
e03ea59f3c | ||
|
|
f0b2840668 | ||
|
|
4f2fc49475 | ||
|
|
2e4155bbfc | ||
|
|
ca2456cbc6 | ||
|
|
222b4868f5 | ||
|
|
2cf1f7e833 | ||
|
|
776d24695e | ||
|
|
64a9c6e5d7 | ||
|
|
66196013bc | ||
|
|
61ff308c9f | ||
|
|
2672f9b087 | ||
|
|
4315a75a12 | ||
|
|
15905bebb1 | ||
|
|
fc4a7454ad | ||
|
|
87410aea50 | ||
|
|
de98d00a1c | ||
|
|
e94ffd9d79 | ||
|
|
d7aebb784d | ||
|
|
20b8cb939d | ||
|
|
ff4217c3d1 | ||
|
|
4fb72d30d8 | ||
|
|
d2b8fc0bda | ||
|
|
951181628a | ||
|
|
ccee274847 | ||
|
|
967f6728ef | ||
|
|
bf11ffb626 | ||
|
|
c12cabbe4e | ||
|
|
040021ee0e | ||
|
|
5018a3a848 | ||
|
|
df1c74a91d |
@@ -33,7 +33,7 @@ jobs:
|
||||
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 \
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Vendored
+153
@@ -78,6 +78,11 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
color: #fff8ea;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
@@ -139,6 +144,7 @@ select {
|
||||
z-index: 2000;
|
||||
border-bottom: 3px solid var(--rule);
|
||||
background: rgba(238, 229, 211, 0.94);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
@@ -231,6 +237,17 @@ select {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.site-nav a[aria-current="page"] {
|
||||
border-block-color: var(--rule);
|
||||
background: var(--panel-dark);
|
||||
color: var(--paper-warm);
|
||||
box-shadow: inset 0 -0.3rem 0 var(--accent);
|
||||
}
|
||||
|
||||
.site-nav a[aria-current="page"]::before {
|
||||
color: #d19b25;
|
||||
}
|
||||
|
||||
.site-nav a:hover,
|
||||
.site-nav a:focus-visible {
|
||||
border-block-color: var(--rule);
|
||||
@@ -511,6 +528,7 @@ select {
|
||||
font-weight: 900;
|
||||
line-height: 1.18;
|
||||
box-shadow: 4px 4px 0 var(--rule);
|
||||
touch-action: manipulation;
|
||||
transition: transform 0.12s ease, background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
@@ -527,6 +545,11 @@ select {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translate(3px, 3px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background: var(--accent);
|
||||
color: #fff8ea;
|
||||
@@ -1176,6 +1199,11 @@ select {
|
||||
color: rgba(255, 248, 234, 0.82);
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
width: fit-content;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.footer-links a::before {
|
||||
color: #d19b25;
|
||||
content: "↳ ";
|
||||
@@ -1184,6 +1212,7 @@ select {
|
||||
.footer-links a:hover,
|
||||
.footer-links a:focus-visible {
|
||||
color: #fff8ea;
|
||||
border-bottom-color: currentColor;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1346,6 +1375,70 @@ select {
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Automatic dark appearance, matching the main ZoiteChat site palette. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--paper: #161514;
|
||||
--paper-warm: #f4ead9;
|
||||
--paper-cool: #1c2522;
|
||||
--ink: #f2eadb;
|
||||
--ink-soft: #ddd2c1;
|
||||
--muted: #bdb19f;
|
||||
--quiet: #928777;
|
||||
--rule: #080706;
|
||||
--rule-soft: rgba(242, 234, 219, 0.16);
|
||||
--rule-mid: rgba(242, 234, 219, 0.3);
|
||||
--panel: #211e1a;
|
||||
--panel-alt: #2b261f;
|
||||
--panel-dark: #0b0c0c;
|
||||
--accent: #c34720;
|
||||
--accent-dark: #ff966d;
|
||||
--accent-soft: rgba(195, 71, 32, 0.17);
|
||||
--green: #367662;
|
||||
--blue: #386b8d;
|
||||
--warning: #9b6508;
|
||||
--success: #43856a;
|
||||
--shadow: 8px 8px 0 rgba(0, 0, 0, 0.82);
|
||||
--shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.36);
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(242, 234, 219, 0.025) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||
linear-gradient(180deg, rgba(242, 234, 219, 0.022) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||
radial-gradient(circle at 11% 18%, rgba(195, 71, 32, 0.15), transparent 24rem),
|
||||
radial-gradient(circle at 86% 4%, rgba(54, 118, 98, 0.16), transparent 28rem),
|
||||
linear-gradient(180deg, #181715 0%, #111311 58%, #191714 100%);
|
||||
}
|
||||
|
||||
body::before {
|
||||
background:
|
||||
repeating-linear-gradient(90deg, transparent 0 17px, rgba(242, 234, 219, 0.025) 17px 18px),
|
||||
repeating-linear-gradient(0deg, transparent 0 5px, rgba(255, 255, 255, 0.025) 5px 6px);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
background: rgba(22, 21, 20, 0.94);
|
||||
}
|
||||
|
||||
.site-nav a,
|
||||
.nav-menu summary {
|
||||
background: rgba(39, 35, 30, 0.52);
|
||||
}
|
||||
|
||||
.eyebrow::before,
|
||||
.footer-links a::before {
|
||||
color: #e0aa3c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sphinx/Read the Docs bridge: aggressively map documentation onto the main ZoiteChat site skin. */
|
||||
html {
|
||||
font-size: 16px !important;
|
||||
@@ -1485,6 +1578,17 @@ body a {
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a[aria-current="page"] {
|
||||
border-block-color: var(--rule) !important;
|
||||
background: var(--panel-dark) !important;
|
||||
color: var(--paper-warm) !important;
|
||||
box-shadow: inset 0 -0.3rem 0 var(--accent) !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a[aria-current="page"]::before {
|
||||
color: #d19b25 !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a:hover,
|
||||
#navbar-top .site-nav a:focus-visible,
|
||||
#navbar-top .site-nav summary:hover,
|
||||
@@ -1671,6 +1775,7 @@ footer.site-footer-lite {
|
||||
}
|
||||
|
||||
.document .body p {
|
||||
max-width: 78ch;
|
||||
margin: 0 0 1rem !important;
|
||||
}
|
||||
|
||||
@@ -1716,6 +1821,7 @@ ul.search a:focus-visible {
|
||||
|
||||
.document .body ul,
|
||||
.document .body ol {
|
||||
max-width: 78ch;
|
||||
margin: 0.75rem 0 1.4rem !important;
|
||||
padding-left: 1.35rem !important;
|
||||
}
|
||||
@@ -1865,6 +1971,11 @@ ul.search a:focus-visible {
|
||||
text-transform: uppercase !important;
|
||||
}
|
||||
|
||||
.document .body table tbody tr:hover td,
|
||||
.document .body table tbody tr:focus-within td {
|
||||
background: var(--accent-soft) !important;
|
||||
}
|
||||
|
||||
.admonition,
|
||||
.note,
|
||||
.warning,
|
||||
@@ -2004,6 +2115,7 @@ input[type="submit"],
|
||||
line-height: 1.18 !important;
|
||||
text-shadow: none !important;
|
||||
box-shadow: 4px 4px 0 var(--rule) !important;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
@@ -2019,6 +2131,13 @@ input[type="submit"]:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.btn:active,
|
||||
button:active,
|
||||
input[type="submit"]:active {
|
||||
transform: translate(3px, 3px) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#navbar-related {
|
||||
min-height: auto !important;
|
||||
margin: 1rem 0 1.5rem !important;
|
||||
@@ -2275,3 +2394,37 @@ footer:not(.site-footer) {
|
||||
letter-spacing: -0.04em !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep basicstrap's stronger overrides readable with the main site's dark palette. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#navbar-top.site-header {
|
||||
background: rgba(22, 21, 20, 0.94) !important;
|
||||
}
|
||||
|
||||
#navbar-top .site-nav a,
|
||||
#navbar-top .site-nav summary {
|
||||
background: rgba(39, 35, 30, 0.52) !important;
|
||||
}
|
||||
|
||||
.document::after,
|
||||
.sidebar::after,
|
||||
#navbar-related::after {
|
||||
background: linear-gradient(135deg, transparent 50%, rgba(242, 234, 219, 0.09) 50%) !important;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
.search-query,
|
||||
.form-control,
|
||||
textarea,
|
||||
select {
|
||||
background: var(--panel) !important;
|
||||
box-shadow: inset 3px 3px 0 rgba(0, 0, 0, 0.32) !important;
|
||||
}
|
||||
|
||||
.warning,
|
||||
.caution,
|
||||
.danger {
|
||||
background: #3a2e19 !important;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
-1
@@ -209,6 +209,8 @@
|
||||
{%- endif %}
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="theme-color" content="#eee5d3" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#161514" media="(prefers-color-scheme: dark)">
|
||||
|
||||
{{ css() }}
|
||||
{%- if not embedded %}
|
||||
@@ -260,8 +262,9 @@
|
||||
<nav class="site-nav" aria-label="Primary">
|
||||
<a href="https://zoitechat.org/">Home</a>
|
||||
<a href="https://zoitechat.org/addons.php">Addons</a>
|
||||
<a href="https://zoitechat.org/announcements.php">Announcements</a>
|
||||
<a href="https://zoitechat.org/download.php">Download</a>
|
||||
<a href="{{ pathto(master_doc) }}">Docs</a>
|
||||
<a href="{{ pathto(master_doc) }}" aria-current="page">Docs</a>
|
||||
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">GitHub</a>
|
||||
<details class="nav-menu">
|
||||
<summary>IRC Support</summary>
|
||||
@@ -336,6 +339,7 @@
|
||||
<div class="footer-links">
|
||||
<a href="https://zoitechat.org/download.php">Download</a>
|
||||
<a href="https://zoitechat.org/addons.php">Addons</a>
|
||||
<a href="https://zoitechat.org/announcements.php">Announcements</a>
|
||||
<a href="https://zoitechat.org/privacy.php">Privacy Policy</a>
|
||||
<a href="{{ pathto(master_doc) }}">Documentation</a>
|
||||
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">Source code</a>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -710,6 +710,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
||||
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
|
||||
|
||||
fishlim_target_entry = gtk_entry_new();
|
||||
gtk_widget_set_hexpand(fishlim_target_entry, TRUE);
|
||||
if (target)
|
||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target nick or channel"), 0, 0, 1, 1);
|
||||
@@ -760,6 +761,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
||||
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
|
||||
|
||||
scroll = gtk_scrolled_window_new(NULL, NULL);
|
||||
gtk_widget_set_hexpand(scroll, TRUE);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(scroll), view);
|
||||
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
|
||||
|
||||
@@ -26,7 +26,7 @@ if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system =
|
||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||
sysinfo_sources += 'unix/pci.c'
|
||||
|
||||
picidsdir = libpci.get_pkgconfig_variable('idsdir')
|
||||
picidsdir = libpci.get_variable(pkgconfig : 'idsdir')
|
||||
pciids = join_paths(picidsdir, 'pci.ids')
|
||||
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
||||
endif
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<ClInclude Include="typedef.h" />
|
||||
<ClInclude Include="url.h" />
|
||||
<ClInclude Include="userlist.h" />
|
||||
<ClInclude Include="hexcolor.h" />
|
||||
<ClInclude Include="util.h" />
|
||||
<ClInclude Include="zoitechat-plugin.h" />
|
||||
<ClInclude Include="zoitechat.h" />
|
||||
@@ -79,6 +80,7 @@
|
||||
<ClCompile Include="tree.c" />
|
||||
<ClCompile Include="url.c" />
|
||||
<ClCompile Include="userlist.c" />
|
||||
<ClCompile Include="hexcolor.c" />
|
||||
<ClCompile Include="util.c" />
|
||||
<ClCompile Include="zoitechat.c" />
|
||||
<ClCompile Include="theme-service.c" />
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
<ClInclude Include="userlist.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="hexcolor.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="util.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -202,6 +205,9 @@
|
||||
<ClCompile Include="userlist.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="hexcolor.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="util.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -70,6 +70,9 @@ void fe_add_rawlog (struct server *serv, char *text, int len, int outbound);
|
||||
#define FE_MSG_ERROR 8
|
||||
#define FE_MSG_MARKUP 16
|
||||
void fe_message (char *msg, int flags);
|
||||
#ifdef __linux__
|
||||
void fe_root_warning (void);
|
||||
#endif
|
||||
#define FIA_READ 1
|
||||
#define FIA_WRITE 2
|
||||
#define FIA_EX 4
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/* ZoiteChat
|
||||
* Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB].
|
||||
* Kept free of other application dependencies so the xtext widget tests can
|
||||
* compile it on its own.
|
||||
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||
|
||||
#include "hexcolor.h"
|
||||
|
||||
static int
|
||||
hexcolor_component (const char *s, int avail, guint32 *rgb)
|
||||
{
|
||||
int i;
|
||||
guint32 value = 0;
|
||||
|
||||
if (avail < 6)
|
||||
return FALSE;
|
||||
for (i = 0; i < 6; i++)
|
||||
{
|
||||
if (!g_ascii_isxdigit (s[i]))
|
||||
return FALSE;
|
||||
value = (value << 4) | g_ascii_xdigit_value (s[i]);
|
||||
}
|
||||
if (rgb)
|
||||
*rgb = value;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int
|
||||
hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg)
|
||||
{
|
||||
if (has_bg)
|
||||
*has_bg = FALSE;
|
||||
if (!hexcolor_component (s, avail, fg))
|
||||
return 0;
|
||||
if (avail >= 7 && s[6] == ',' && hexcolor_component (s + 7, avail - 7, bg))
|
||||
{
|
||||
if (has_bg)
|
||||
*has_bg = TRUE;
|
||||
return 13;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* ZoiteChat
|
||||
* Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB].
|
||||
* See https://modern.ircdocs.horse/formatting
|
||||
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||
|
||||
#ifndef ZOITECHAT_HEXCOLOR_H
|
||||
#define ZOITECHAT_HEXCOLOR_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
/* Parses the parameters following a \004 byte. Returns the number of bytes
|
||||
* consumed (6 or 13), or 0 if no valid RRGGBB follows. fg, bg and has_bg
|
||||
* may be NULL. */
|
||||
int hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg);
|
||||
|
||||
#endif
|
||||
+74
-34
@@ -1880,38 +1880,51 @@ inbound_toggle_caps (server *serv, const char *extensions_str, gboolean enable)
|
||||
char **extensions;
|
||||
gsize i;
|
||||
|
||||
if (!extensions_str)
|
||||
return;
|
||||
|
||||
extensions = g_strsplit (extensions_str, " ", 0);
|
||||
|
||||
for (i = 0; extensions[i]; i++)
|
||||
{
|
||||
const char *extension = extensions[i];
|
||||
gboolean cap_enable = enable;
|
||||
|
||||
/* ACK prefixes disabled capabilities with '-', unlike CAP DEL. */
|
||||
if (*extension == '-')
|
||||
{
|
||||
cap_enable = FALSE;
|
||||
extension++;
|
||||
}
|
||||
|
||||
if (!strcmp (extension, "solanum.chat/identify-msg"))
|
||||
serv->have_idmsg = enable;
|
||||
serv->have_idmsg = cap_enable;
|
||||
else if (!strcmp (extension, "multi-prefix"))
|
||||
serv->have_namesx = enable;
|
||||
serv->have_namesx = cap_enable;
|
||||
else if (!strcmp (extension, "account-notify"))
|
||||
serv->have_accnotify = enable;
|
||||
serv->have_accnotify = cap_enable;
|
||||
else if (!strcmp (extension, "extended-join"))
|
||||
serv->have_extjoin = enable;
|
||||
serv->have_extjoin = cap_enable;
|
||||
else if (!strcmp (extension, "userhost-in-names"))
|
||||
serv->have_uhnames = enable;
|
||||
serv->have_uhnames = cap_enable;
|
||||
else if (!strcmp (extension, "server-time")
|
||||
|| !strcmp (extension, "znc.in/server-time")
|
||||
|| !strcmp (extension, "znc.in/server-time-iso"))
|
||||
serv->have_server_time = enable;
|
||||
serv->have_server_time = cap_enable;
|
||||
else if (!strcmp (extension, "away-notify"))
|
||||
serv->have_awaynotify = enable;
|
||||
serv->have_awaynotify = cap_enable;
|
||||
else if (!strcmp (extension, "account-tag"))
|
||||
serv->have_account_tag = enable;
|
||||
serv->have_account_tag = cap_enable;
|
||||
else if (!strcmp (extension, "message-tags"))
|
||||
serv->have_message_tags = enable;
|
||||
serv->have_message_tags = cap_enable;
|
||||
else if (!strcmp (extension, "echo-message"))
|
||||
serv->have_echo_message = enable;
|
||||
serv->have_echo_message = cap_enable;
|
||||
else if (!strcmp (extension, "sasl"))
|
||||
{
|
||||
serv->have_sasl = enable;
|
||||
if (enable)
|
||||
gboolean start_auth = cap_enable && !serv->have_sasl;
|
||||
|
||||
serv->have_sasl = cap_enable;
|
||||
if (start_auth)
|
||||
{
|
||||
#ifdef USE_OPENSSL
|
||||
if (serv->loginmethod == LOGIN_SASLEXTERNAL)
|
||||
@@ -1962,10 +1975,15 @@ inbound_cap_ack (server *serv, char *nick, char *extensions,
|
||||
inbound_toggle_caps (serv, extensions, TRUE);
|
||||
}
|
||||
|
||||
static void inbound_request_new_caps (server *serv, const char *extensions_str,
|
||||
const message_tags_data *tags_data);
|
||||
|
||||
void
|
||||
inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||
const message_tags_data *tags_data)
|
||||
{
|
||||
gboolean sts_upgrade_triggered = FALSE;
|
||||
|
||||
if (extensions)
|
||||
{
|
||||
char **tokens = g_strsplit (extensions, " ", 0);
|
||||
@@ -1977,7 +1995,7 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||
|
||||
if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
|
||||
{
|
||||
sts_handle_capability (serv, parts[1]);
|
||||
sts_upgrade_triggered |= sts_handle_capability (serv, parts[1]);
|
||||
}
|
||||
|
||||
g_strfreev (parts);
|
||||
@@ -1986,38 +2004,22 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPLIST, serv->server_session, nick, extensions,
|
||||
NULL, NULL, 0, tags_data->timestamp);
|
||||
|
||||
inbound_toggle_caps (serv, extensions, TRUE);
|
||||
/* NEW advertises availability; only ACK confirms an enabled capability. */
|
||||
if (!sts_upgrade_triggered)
|
||||
inbound_request_new_caps (serv, extensions, tags_data);
|
||||
}
|
||||
|
||||
void
|
||||
inbound_cap_del (server *serv, char *nick, char *extensions,
|
||||
const message_tags_data *tags_data)
|
||||
{
|
||||
if (extensions)
|
||||
{
|
||||
char **tokens = g_strsplit (extensions, " ", 0);
|
||||
int i;
|
||||
|
||||
for (i = 0; tokens[i]; i++)
|
||||
{
|
||||
if (!g_strcmp0 (tokens[i], "sts") ||
|
||||
g_str_has_prefix (tokens[i], "sts="))
|
||||
{
|
||||
/* STS cannot be disabled via CAP DEL. */
|
||||
g_strfreev (tokens);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g_strfreev (tokens);
|
||||
}
|
||||
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPDEL, serv->server_session, nick, extensions,
|
||||
NULL, NULL, 0, tags_data->timestamp);
|
||||
|
||||
/* STS has no toggle, but must not prevent other capabilities being removed. */
|
||||
inbound_toggle_caps (serv, extensions, FALSE);
|
||||
}
|
||||
|
||||
@@ -2053,6 +2055,44 @@ static const char * const supported_caps[] = {
|
||||
"solanum.chat/identify-msg",
|
||||
};
|
||||
|
||||
static void
|
||||
inbound_request_new_caps (server *serv, const char *extensions_str,
|
||||
const message_tags_data *tags_data)
|
||||
{
|
||||
char **extensions;
|
||||
gsize i, x;
|
||||
|
||||
if (!extensions_str)
|
||||
return;
|
||||
|
||||
extensions = g_strsplit (extensions_str, " ", 0);
|
||||
for (i = 0; extensions[i]; i++)
|
||||
{
|
||||
char *value = strchr (extensions[i], '=');
|
||||
|
||||
if (value)
|
||||
*value = '\0';
|
||||
}
|
||||
|
||||
/* Request each supported capability once, keeping every IRC line bounded.
|
||||
* SASL is negotiated during login and is not in supported_caps. */
|
||||
for (x = 0; x < G_N_ELEMENTS (supported_caps); x++)
|
||||
{
|
||||
for (i = 0; extensions[i]; i++)
|
||||
{
|
||||
if (!strcmp (extensions[i], supported_caps[x]))
|
||||
{
|
||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
|
||||
extensions[i], NULL, NULL, NULL, 0,
|
||||
tags_data->timestamp);
|
||||
tcp_sendf (serv, "CAP REQ :%s\r\n", extensions[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_strfreev (extensions);
|
||||
}
|
||||
|
||||
static int
|
||||
get_supported_mech (server *serv, const char *list)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ common_sources = [
|
||||
'tree.c',
|
||||
'url.c',
|
||||
'userlist.c',
|
||||
'hexcolor.c',
|
||||
'util.c'
|
||||
]
|
||||
|
||||
@@ -197,3 +198,18 @@ test('Emoji Data Tests', emoji_data_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
hexcolor_tests = executable('hexcolor_tests',
|
||||
[
|
||||
'tests/test-hexcolor.c',
|
||||
'hexcolor.c',
|
||||
'util.c',
|
||||
],
|
||||
include_directories: [config_h_include, include_directories('.')],
|
||||
dependencies: [libgio_dep] + (libssl_dep.found() ? [libssl_dep] : []),
|
||||
)
|
||||
|
||||
test('Hex Color Tests', hexcolor_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
@@ -2826,6 +2826,7 @@ static gboolean
|
||||
client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
gboolean blocked = FALSE;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
@@ -2837,15 +2838,16 @@ client_tag_allowed (server *serv, const char *tag)
|
||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||
for (i = 0; deny[i]; i++)
|
||||
{
|
||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
{
|
||||
g_strfreev (deny);
|
||||
return FALSE;
|
||||
}
|
||||
if (!strcmp (deny[i], "*"))
|
||||
blocked = TRUE;
|
||||
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
|
||||
blocked = FALSE;
|
||||
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
blocked = TRUE;
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
return !blocked;
|
||||
}
|
||||
|
||||
static char *
|
||||
|
||||
+3
-3
@@ -155,7 +155,7 @@ _SSL_get_cert_info (struct cert_info *cert_info, SSL * ssl)
|
||||
{
|
||||
X509 *peer_cert;
|
||||
const X509_PUBKEY *key;
|
||||
X509_ALGOR *algor = NULL;
|
||||
ASN1_OBJECT *obj = NULL;
|
||||
EVP_PKEY *peer_pkey;
|
||||
char notBefore[64];
|
||||
char notAfter[64];
|
||||
@@ -174,10 +174,10 @@ _SSL_get_cert_info (struct cert_info *cert_info, SSL * ssl)
|
||||
broke_oneline (cert_info->issuer, cert_info->issuer_word);
|
||||
|
||||
key = X509_get_X509_PUBKEY(peer_cert);
|
||||
if (!X509_PUBKEY_get0_param(NULL, NULL, 0, &algor, key))
|
||||
if (!X509_PUBKEY_get0_param(&obj, NULL, NULL, NULL, key))
|
||||
return 1;
|
||||
|
||||
alg = OBJ_obj2nid (algor->algorithm);
|
||||
alg = OBJ_obj2nid (obj);
|
||||
#ifndef HAVE_X509_GET_SIGNATURE_NID
|
||||
sign_alg = OBJ_obj2nid (peer_cert->sig_alg->algorithm);
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/* ZoiteChat
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include "../hexcolor.h"
|
||||
#include "../util.h"
|
||||
|
||||
static void
|
||||
test_parse_foreground (void)
|
||||
{
|
||||
guint32 fg = 0, bg = 0xDEAD;
|
||||
int has_bg = TRUE;
|
||||
|
||||
g_assert_cmpint (hexcolor_parse ("FF8800text", 10, &fg, &bg, &has_bg), ==, 6);
|
||||
g_assert_cmphex (fg, ==, 0xFF8800);
|
||||
g_assert_false (has_bg);
|
||||
g_assert_cmphex (bg, ==, 0xDEAD);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_foreground_and_background (void)
|
||||
{
|
||||
guint32 fg = 0, bg = 0;
|
||||
int has_bg = FALSE;
|
||||
|
||||
g_assert_cmpint (hexcolor_parse ("112233,a0b0c0x", 14, &fg, &bg, &has_bg), ==, 13);
|
||||
g_assert_cmphex (fg, ==, 0x112233);
|
||||
g_assert_cmphex (bg, ==, 0xA0B0C0);
|
||||
g_assert_true (has_bg);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_bad_background_keeps_foreground (void)
|
||||
{
|
||||
guint32 fg = 0;
|
||||
int has_bg = TRUE;
|
||||
|
||||
/* ",12" is plain text after a valid foreground, not a background */
|
||||
g_assert_cmpint (hexcolor_parse ("112233,12 hi", 12, &fg, NULL, &has_bg), ==, 6);
|
||||
g_assert_cmphex (fg, ==, 0x112233);
|
||||
g_assert_false (has_bg);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_bare (void)
|
||||
{
|
||||
/* a bare \004 resets colors, like a bare \003 */
|
||||
g_assert_cmpint (hexcolor_parse (" text", 5, NULL, NULL, NULL), ==, 0);
|
||||
g_assert_cmpint (hexcolor_parse ("12345G", 6, NULL, NULL, NULL), ==, 0);
|
||||
g_assert_cmpint (hexcolor_parse ("", 0, NULL, NULL, NULL), ==, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_respects_length (void)
|
||||
{
|
||||
g_assert_cmpint (hexcolor_parse ("FF8800", 5, NULL, NULL, NULL), ==, 0);
|
||||
g_assert_cmpint (hexcolor_parse ("FF8800,112233", 12, NULL, NULL, NULL), ==, 6);
|
||||
}
|
||||
|
||||
static void
|
||||
assert_stripped (const char *in, int flags, const char *expected)
|
||||
{
|
||||
char out[128];
|
||||
|
||||
strip_color2 (in, -1, out, flags);
|
||||
g_assert_cmpstr (out, ==, expected);
|
||||
}
|
||||
|
||||
static void
|
||||
test_strip (void)
|
||||
{
|
||||
assert_stripped ("\004FF8800alice\017: hi", STRIP_ALL, "alice: hi");
|
||||
assert_stripped ("\004112233,445566bg", STRIP_ALL, "bg");
|
||||
assert_stripped ("a\004 b", STRIP_ALL, "a b");
|
||||
assert_stripped ("\004FF8800\00312x", STRIP_ALL, "x");
|
||||
/* a trailing code with too few digits leaves the rest as text */
|
||||
assert_stripped ("x\004FF88", STRIP_ALL, "xFF88");
|
||||
}
|
||||
|
||||
static void
|
||||
test_strip_keeps_hex_without_strip_color (void)
|
||||
{
|
||||
assert_stripped ("\004FF8800red\002", STRIP_ATTRIB, "\004FF8800red");
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
|
||||
g_test_add_func ("/hexcolor/parse/foreground", test_parse_foreground);
|
||||
g_test_add_func ("/hexcolor/parse/foreground-and-background", test_parse_foreground_and_background);
|
||||
g_test_add_func ("/hexcolor/parse/bad-background", test_parse_bad_background_keeps_foreground);
|
||||
g_test_add_func ("/hexcolor/parse/bare", test_parse_bare);
|
||||
g_test_add_func ("/hexcolor/parse/length", test_parse_respects_length);
|
||||
g_test_add_func ("/hexcolor/strip/all", test_strip);
|
||||
g_test_add_func ("/hexcolor/strip/attrib-only", test_strip_keeps_hex_without_strip_color);
|
||||
|
||||
return g_test_run ();
|
||||
}
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "zoitechatc.h"
|
||||
#include <ctype.h>
|
||||
#include "util.h"
|
||||
#include "hexcolor.h"
|
||||
|
||||
#if defined (__FreeBSD__) || defined (__APPLE__)
|
||||
#include <sys/sysctl.h>
|
||||
@@ -330,6 +331,14 @@ strip_color2 (const char *src, int len, char *dst, int flags)
|
||||
if (!(flags & STRIP_COLOR)) goto pass_char;
|
||||
rcol = 2;
|
||||
break;
|
||||
case '\004': /*ATTR_HEXCOLOR: */
|
||||
if (!(flags & STRIP_COLOR)) goto pass_char;
|
||||
{
|
||||
int skip = hexcolor_parse (src + 1, len, NULL, NULL, NULL);
|
||||
src += skip;
|
||||
len -= skip;
|
||||
}
|
||||
break;
|
||||
case HIDDEN_CHAR: /* CL: invisible text (for event formats only) */ /* this takes care of the topic */
|
||||
if (!(flags & STRIP_HIDDEN)) goto pass_char;
|
||||
break;
|
||||
|
||||
+22
-3
@@ -95,6 +95,7 @@ int zoitechat_is_quitting = FALSE;
|
||||
/* command-line args */
|
||||
int arg_dont_autoconnect = FALSE;
|
||||
int arg_skip_plugins = FALSE;
|
||||
gint arg_run_as_root = FALSE;
|
||||
char *arg_url = NULL;
|
||||
char **arg_urls = NULL;
|
||||
char *arg_command = NULL;
|
||||
@@ -463,8 +464,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++;
|
||||
@@ -1176,6 +1176,25 @@ main (int argc, char *argv[])
|
||||
HRESULT coinit_result;
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
for (i = 1; i < argc; i++)
|
||||
{
|
||||
if (strcmp (argv[i], "--") == 0)
|
||||
break;
|
||||
if (strcmp (argv[i], "--run-as-root") == 0)
|
||||
{
|
||||
arg_run_as_root = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (geteuid () == 0 && !arg_run_as_root)
|
||||
{
|
||||
fe_root_warning ();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
#endif
|
||||
|
||||
srand ((unsigned int) time (NULL)); /* CL: do this only once! */
|
||||
|
||||
/* We must check for the config dir parameter, otherwise load_config() will behave incorrectly.
|
||||
@@ -1264,7 +1283,7 @@ main (int argc, char *argv[])
|
||||
#ifndef WIN32
|
||||
#ifndef __EMX__
|
||||
/* OS/2 uses UID 0 all the time */
|
||||
if (getuid () == 0)
|
||||
if (getuid () == 0 && !arg_run_as_root)
|
||||
fe_message (_("* Running IRC as root is stupid! You should\n"
|
||||
" create a User Account and use that to login.\n"), FE_MSG_WARN|FE_MSG_WAIT);
|
||||
#endif
|
||||
|
||||
@@ -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];
|
||||
@@ -448,6 +450,7 @@ typedef struct session
|
||||
int mode_timeout_tag;
|
||||
int typing_timeout_tag;
|
||||
int typing_status;
|
||||
gint64 typing_last_sent;
|
||||
int typing_animation_tag;
|
||||
int typing_animation_frame;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ extern struct zoitechatprefs prefs;
|
||||
extern int zoitechat_is_quitting;
|
||||
extern gint arg_skip_plugins; /* command-line args */
|
||||
extern gint arg_dont_autoconnect;
|
||||
extern gint arg_run_as_root;
|
||||
extern char *arg_url;
|
||||
extern char **arg_urls;
|
||||
extern char *arg_command;
|
||||
|
||||
+3
-3
@@ -138,7 +138,7 @@ ascii_open (void)
|
||||
{
|
||||
table_pos++;
|
||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_container_add (GTK_CONTAINER (vbox), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
|
||||
gtk_widget_show (hbox);
|
||||
i++;
|
||||
continue;
|
||||
@@ -155,14 +155,14 @@ ascii_open (void)
|
||||
G_CALLBACK (ascii_click), NULL);
|
||||
g_signal_connect (G_OBJECT (but), "enter-notify-event",
|
||||
G_CALLBACK (ascii_enter), label);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), but, 0, 0, 0);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), but, TRUE, TRUE, 0);
|
||||
gtk_widget_show (but);
|
||||
|
||||
table_pos += len;
|
||||
}
|
||||
|
||||
frame = gtk_frame_new ("");
|
||||
gtk_container_add (GTK_CONTAINER (hbox), frame);
|
||||
gtk_box_pack_start (GTK_BOX (hbox), frame, TRUE, TRUE, 0);
|
||||
gtk_container_add (GTK_CONTAINER (frame), label);
|
||||
gtk_widget_show (label);
|
||||
gtk_widget_show (frame);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+38
-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"
|
||||
@@ -89,6 +90,9 @@ static const GOptionEntry gopt_entries[] =
|
||||
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
|
||||
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
|
||||
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
|
||||
#ifdef __linux__
|
||||
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
|
||||
#endif
|
||||
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
|
||||
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
|
||||
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel?key URL"), "URL"},
|
||||
@@ -102,6 +106,33 @@ static const GOptionEntry gopt_entries[] =
|
||||
{NULL}
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
void
|
||||
fe_root_warning (void)
|
||||
{
|
||||
const char *message =
|
||||
"ZoiteChat refuses to run as root on Linux.\n\n"
|
||||
"Run it as a normal user, or pass --run-as-root to override this protection.";
|
||||
GtkWidget *dialog;
|
||||
|
||||
g_printerr ("%s\n", message);
|
||||
|
||||
/* A graphical session may not be accessible to root, particularly under
|
||||
* Wayland. Keep stderr as the fallback if GTK cannot open the display. */
|
||||
if (!gtk_init_check (NULL, NULL))
|
||||
return;
|
||||
|
||||
dialog = gtk_message_dialog_new (NULL,
|
||||
GTK_DIALOG_MODAL,
|
||||
GTK_MESSAGE_ERROR,
|
||||
GTK_BUTTONS_CLOSE,
|
||||
"%s", message);
|
||||
gtk_window_set_title (GTK_WINDOW (dialog), "ZoiteChat");
|
||||
gtk_dialog_run (GTK_DIALOG (dialog));
|
||||
gtk_widget_destroy (dialog);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
static void
|
||||
create_msg_dialog (gchar *title, gchar *message)
|
||||
@@ -943,7 +974,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>
|
||||
|
||||
+13
-3
@@ -1221,7 +1221,7 @@ key_dialog_load (GtkListStore *store)
|
||||
void
|
||||
key_dialog_show ()
|
||||
{
|
||||
GtkWidget *vbox, *box;
|
||||
GtkWidget *vbox, *box, *pane, *scroll;
|
||||
GtkWidget *view, *xtext, *delete_button;
|
||||
GtkListStore *store;
|
||||
XTextColor xtext_palette[XTEXT_COLS];
|
||||
@@ -1237,10 +1237,20 @@ key_dialog_show ()
|
||||
key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close,
|
||||
NULL, 600, 360, &vbox, 0);
|
||||
|
||||
view = key_dialog_treeview_new (vbox);
|
||||
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
|
||||
box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
gtk_paned_pack1 (GTK_PANED (pane), box, TRUE, FALSE);
|
||||
view = key_dialog_treeview_new (box);
|
||||
scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 80);
|
||||
gtk_paned_pack2 (GTK_PANED (pane), scroll, TRUE, FALSE);
|
||||
gtk_paned_set_position (GTK_PANED (pane), 220);
|
||||
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
|
||||
xtext = gtk_xtext_new (xtext_palette, 0);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), xtext, FALSE, TRUE, 2);
|
||||
gtk_container_add (GTK_CONTAINER (scroll), xtext);
|
||||
gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font);
|
||||
|
||||
g_object_set_data (G_OBJECT (key_dialog), "view", view);
|
||||
|
||||
+17
-11
@@ -641,9 +641,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtkutil_place_dialog (dialog);
|
||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
||||
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
@@ -653,7 +652,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
||||
G_CALLBACK (gtkutil_str_enter), dialog);
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
|
||||
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
|
||||
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||
@@ -661,7 +661,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (gtkutil_get_str_response), entry);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
hbox, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
@@ -742,9 +743,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtkutil_place_dialog (dialog);
|
||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
||||
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
@@ -755,7 +755,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
gtk_adjustment_set_upper (adj, 1024);
|
||||
gtk_adjustment_set_step_increment (adj, 1);
|
||||
gtk_spin_button_set_value ((GtkSpinButton*)spin, def);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), spin, 0, 0, 0);
|
||||
gtk_widget_set_valign (spin, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), spin, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||
@@ -763,7 +764,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (gtkutil_get_number_response), spin);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
hbox, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
@@ -783,17 +785,18 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
|
||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtkutil_place_dialog (dialog);
|
||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
||||
|
||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||
|
||||
prompt_label = gtk_label_new (prompt);
|
||||
gtk_label_set_line_wrap (GTK_LABEL (prompt_label), TRUE);
|
||||
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (gtkutil_get_bool_response), NULL);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), prompt_label);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
prompt_label, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
@@ -885,7 +888,10 @@ gtkutil_window_new (char *title, char *role, int width, int height, int flags)
|
||||
gtk_window_set_wmclass (GTK_WINDOW (win), "ZoiteChat", "zoitechat");
|
||||
#endif
|
||||
gtk_window_set_title (GTK_WINDOW (win), title);
|
||||
gtk_window_set_default_size (GTK_WINDOW (win), width, height);
|
||||
/* Zero means content-sized to callers; GTK requires -1 for that. */
|
||||
gtk_window_set_default_size (GTK_WINDOW (win),
|
||||
width > 0 ? width : -1,
|
||||
height > 0 ? height : -1);
|
||||
gtk_window_set_role (GTK_WINDOW (win), role);
|
||||
if (flags & 1)
|
||||
gtk_window_set_position (GTK_WINDOW (win), GTK_WIN_POS_MOUSE);
|
||||
|
||||
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
|
||||
@@ -137,7 +137,6 @@ joind_show_dialog (server *serv)
|
||||
gtk_window_set_position (GTK_WINDOW (dialog1), GTK_WIN_POS_CENTER_ON_PARENT);
|
||||
gtk_window_set_transient_for (GTK_WINDOW(dialog1), GTK_WINDOW(serv->front_session->gui->window));
|
||||
gtk_window_set_modal (GTK_WINDOW (dialog1), TRUE);
|
||||
gtk_window_set_resizable (GTK_WINDOW (dialog1), FALSE);
|
||||
|
||||
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog1));
|
||||
gtk_widget_show (dialog_vbox1);
|
||||
|
||||
+292
-18
@@ -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"
|
||||
|
||||
@@ -777,10 +778,13 @@ mg_inputbox_focus (GtkWidget *widget, GdkEventFocus *event, session_gui *gui)
|
||||
}
|
||||
|
||||
|
||||
#define TYPING_THROTTLE_USEC (3 * G_USEC_PER_SEC)
|
||||
|
||||
static gboolean
|
||||
mg_client_tag_allowed (server *serv, const char *tag)
|
||||
{
|
||||
char **deny;
|
||||
gboolean blocked = FALSE;
|
||||
int i;
|
||||
|
||||
if (!serv->have_message_tags)
|
||||
@@ -792,30 +796,42 @@ mg_client_tag_allowed (server *serv, const char *tag)
|
||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||
for (i = 0; deny[i]; i++)
|
||||
{
|
||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
{
|
||||
g_strfreev (deny);
|
||||
return FALSE;
|
||||
}
|
||||
if (!strcmp (deny[i], "*"))
|
||||
blocked = TRUE;
|
||||
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
|
||||
blocked = FALSE;
|
||||
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||
blocked = TRUE;
|
||||
}
|
||||
|
||||
g_strfreev (deny);
|
||||
return TRUE;
|
||||
return !blocked;
|
||||
}
|
||||
|
||||
static void
|
||||
static gboolean
|
||||
mg_typing_can_send (session *sess)
|
||||
{
|
||||
gint64 now = g_get_monotonic_time ();
|
||||
|
||||
return sess->typing_last_sent == 0 ||
|
||||
now - sess->typing_last_sent >= TYPING_THROTTLE_USEC;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
mg_send_typing (session *sess, const char *state)
|
||||
{
|
||||
char tags[32];
|
||||
|
||||
if (!sess || !sess->server->connected || !mg_client_tag_allowed (sess->server, "typing") || !sess->channel[0])
|
||||
return;
|
||||
return FALSE;
|
||||
|
||||
if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG)
|
||||
return;
|
||||
return FALSE;
|
||||
|
||||
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
||||
sess->server->p_tagmsg (sess->server, tags, sess->channel);
|
||||
sess->typing_last_sent = g_get_monotonic_time ();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -844,17 +860,15 @@ mg_typing_update (session *sess, const char *text)
|
||||
|
||||
if (!text || !*text || text[0] == prefs.hex_input_command_char[0])
|
||||
{
|
||||
if (sess->typing_status)
|
||||
if (sess->typing_status && mg_typing_can_send (sess))
|
||||
mg_send_typing (sess, "done");
|
||||
sess->typing_status = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sess->typing_status != 1)
|
||||
{
|
||||
if (mg_typing_can_send (sess))
|
||||
mg_send_typing (sess, "active");
|
||||
sess->typing_status = 1;
|
||||
}
|
||||
sess->typing_status = 1;
|
||||
sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess);
|
||||
}
|
||||
|
||||
@@ -926,10 +940,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)));
|
||||
}
|
||||
@@ -2090,7 +2330,6 @@ mg_open_quit_dialog (gboolean minimize_button)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
|
||||
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
|
||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||
gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
|
||||
|
||||
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
||||
gtk_widget_show (dialog_vbox1);
|
||||
@@ -3692,7 +3931,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 +3949,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 +3991,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 +4939,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 +4982,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");
|
||||
|
||||
+61
-5
@@ -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)
|
||||
{
|
||||
@@ -1539,16 +1595,16 @@ menu_join (GtkWidget * wid, gpointer none)
|
||||
NULL);
|
||||
theme_manager_attach_window (dialog);
|
||||
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
||||
gtk_box_set_homogeneous (GTK_BOX (content_area), TRUE);
|
||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||
|
||||
entry = gtk_entry_new ();
|
||||
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE); /* avoid auto-selection */
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), "#");
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (menu_join_entry_cb), dialog);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
|
||||
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (_("Enter Channel to Join:"));
|
||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||
@@ -1556,7 +1612,7 @@ menu_join (GtkWidget * wid, gpointer none)
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (menu_join_cb), entry);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (content_area), hbox);
|
||||
gtk_box_pack_start (GTK_BOX (content_area), hbox, TRUE, TRUE, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+10
-2
@@ -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',
|
||||
@@ -277,3 +278,10 @@ test('Emoji Font Tests', emoji_font_tests,
|
||||
protocol: 'tap',
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
xtext_tests = executable('xtext_tests',
|
||||
['tests/test-xtext.c', '../common/hexcolor.c'],
|
||||
include_directories: [config_h_include],
|
||||
dependencies: [gtk_dep],
|
||||
)
|
||||
test('XText Tests', xtext_tests, protocol: 'tap', timeout: 120)
|
||||
|
||||
+11
-6
@@ -391,34 +391,39 @@ fe_notify_ask (char *nick, char *networks)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (table), 12);
|
||||
gtk_grid_set_row_spacing (GTK_GRID (table), 3);
|
||||
gtk_grid_set_column_spacing (GTK_GRID (table), 8);
|
||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), table);
|
||||
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||
table, TRUE, TRUE, 0);
|
||||
|
||||
label = gtk_label_new (msg);
|
||||
gtkutil_grid_attach_defaults (table, label, 0, 1, 0, 1);
|
||||
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
|
||||
gtkutil_grid_attach (table, label, 0, 1, 0, 1, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
entry = gtk_entry_new ();
|
||||
gtk_entry_set_text (GTK_ENTRY (entry), nick);
|
||||
g_signal_connect (G_OBJECT (entry), "activate",
|
||||
G_CALLBACK (notifygui_add_enter), dialog);
|
||||
gtkutil_grid_attach_defaults (table, entry, 1, 2, 0, 1);
|
||||
gtkutil_grid_attach (table, entry, 1, 2, 0, 1,
|
||||
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
g_signal_connect (G_OBJECT (dialog), "response",
|
||||
G_CALLBACK (notifygui_add_cb), entry);
|
||||
|
||||
label = gtk_label_new (_("Notify on these networks:"));
|
||||
gtkutil_grid_attach_defaults (table, label, 0, 1, 2, 3);
|
||||
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
|
||||
gtkutil_grid_attach (table, label, 0, 1, 2, 3, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
wid = gtk_entry_new ();
|
||||
g_object_set_data (G_OBJECT (entry), "net", wid);
|
||||
g_signal_connect (G_OBJECT (wid), "activate",
|
||||
G_CALLBACK (notifygui_add_enter), dialog);
|
||||
gtk_entry_set_text (GTK_ENTRY (wid), networks ? networks : "ALL");
|
||||
gtkutil_grid_attach_defaults (table, wid, 1, 2, 2, 3);
|
||||
gtkutil_grid_attach (table, wid, 1, 2, 2, 3,
|
||||
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
label = gtk_label_new (NULL);
|
||||
g_snprintf (buf, sizeof (buf), "<i><span size=\"smaller\">%s</span></i>", _("Comma separated list of networks is accepted."));
|
||||
gtk_label_set_markup (GTK_LABEL (label), buf);
|
||||
gtkutil_grid_attach_defaults (table, label, 1, 2, 3, 4);
|
||||
gtkutil_grid_attach (table, label, 1, 2, 3, 4, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||
|
||||
gtk_widget_show_all (dialog);
|
||||
}
|
||||
|
||||
@@ -2423,6 +2423,8 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
GtkWidget *comboboxentry_charset;
|
||||
GtkWidget *combobox_logintypes;
|
||||
GtkWidget *hbox1;
|
||||
GtkWidget *content_scroll;
|
||||
GtkWidget *content_box;
|
||||
GtkWidget *scrolledwindow2;
|
||||
GtkWidget *scrolledwindow4;
|
||||
GtkWidget *scrolledwindow5;
|
||||
@@ -2449,7 +2451,9 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
|
||||
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
|
||||
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
|
||||
gtk_window_set_default_size (GTK_WINDOW (editwindow), netedit_win_width, netedit_win_height);
|
||||
gtk_window_set_default_size (GTK_WINDOW (editwindow),
|
||||
netedit_win_width > 0 ? netedit_win_width : -1,
|
||||
netedit_win_height > 0 ? netedit_win_height : -1);
|
||||
gtk_window_set_transient_for (GTK_WINDOW (editwindow), GTK_WINDOW (parent));
|
||||
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
|
||||
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
|
||||
@@ -2458,10 +2462,20 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
|
||||
|
||||
/* Keep Close accessible on small screens, while the lists still receive
|
||||
all spare space when the complete form fits in the window. */
|
||||
content_scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (content_scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
|
||||
gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), content_scroll, TRUE, TRUE, 0);
|
||||
content_box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||
gtk_container_add (GTK_CONTAINER (content_scroll), content_box);
|
||||
|
||||
/* Tabs and buttons */
|
||||
hbox1 = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), hbox1, TRUE, TRUE, 4);
|
||||
gtk_box_pack_start (GTK_BOX (content_box), hbox1, TRUE, TRUE, 4);
|
||||
|
||||
scrolledwindow2 = gtk_scrolled_window_new (NULL, NULL);
|
||||
scrolledwindow4 = gtk_scrolled_window_new (NULL, NULL);
|
||||
@@ -2599,7 +2613,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
||||
|
||||
/* Checkboxes and entries */
|
||||
table3 = gtkutil_grid_new (19, 2, FALSE);
|
||||
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (content_box), table3, FALSE, FALSE, 0);
|
||||
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
|
||||
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
|
||||
|
||||
@@ -2828,7 +2842,9 @@ servlist_open_networks (void)
|
||||
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
|
||||
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
|
||||
gtk_window_set_title (GTK_WINDOW (servlist), buf);
|
||||
gtk_window_set_default_size (GTK_WINDOW (servlist), netlist_win_width, netlist_win_height);
|
||||
gtk_window_set_default_size (GTK_WINDOW (servlist),
|
||||
netlist_win_width > 0 ? netlist_win_width : -1,
|
||||
netlist_win_height > 0 ? netlist_win_height : -1);
|
||||
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
|
||||
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
|
||||
if (current_sess)
|
||||
|
||||
+25
-8
@@ -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}
|
||||
};
|
||||
|
||||
@@ -1248,7 +1256,7 @@ setup_create_id_menu (GtkWidget *table, char *label, int row, char *dest)
|
||||
static void
|
||||
setup_create_menu (GtkWidget *table, int row, const setting *set)
|
||||
{
|
||||
GtkWidget *wid, *cbox, *box;
|
||||
GtkWidget *wid, *cbox;
|
||||
const char **text = (const char **)set->list;
|
||||
int i;
|
||||
|
||||
@@ -1269,9 +1277,7 @@ setup_create_menu (GtkWidget *table, int row, const setting *set)
|
||||
g_signal_connect (G_OBJECT (cbox), "changed",
|
||||
G_CALLBACK (setup_menu_cb), (gpointer)set);
|
||||
|
||||
box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||
gtk_box_pack_start (GTK_BOX (box), cbox, 0, 0, 0);
|
||||
setup_table_attach (table, box, 3, 4, row, row + 1, TRUE, FALSE,
|
||||
setup_table_attach (table, cbox, 3, 6, row, row + 1, TRUE, FALSE,
|
||||
SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0);
|
||||
}
|
||||
|
||||
@@ -1955,7 +1961,8 @@ setup_add_page (const char *title, GtkWidget *book, GtkWidget *tab)
|
||||
|
||||
sw = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new (NULL, NULL));
|
||||
gtk_scrolled_window_set_shadow_type (sw, GTK_SHADOW_IN);
|
||||
gtk_scrolled_window_set_policy (sw, GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_policy (sw, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_propagate_natural_width (sw, TRUE);
|
||||
gtk_container_add (GTK_CONTAINER (sw), vvbox);
|
||||
|
||||
viewport = gtk_bin_get_child (GTK_BIN(sw));
|
||||
@@ -1999,7 +2006,9 @@ setup_ensure_page_created (int page)
|
||||
}
|
||||
|
||||
page_widget = setup_page_factories[page] (setup_page_settings[page]);
|
||||
gtk_container_add (GTK_CONTAINER (container), page_widget);
|
||||
/* Lazy pages must pass the notebook's spare space to their content,
|
||||
particularly the independently scrolling Sounds list. */
|
||||
gtk_box_pack_start (GTK_BOX (container), page_widget, TRUE, TRUE, 0);
|
||||
gtk_widget_show_all (container);
|
||||
}
|
||||
|
||||
@@ -2177,9 +2186,14 @@ setup_create_tree (GtkWidget *box, GtkWidget *book)
|
||||
-1, _("Categories"), renderer, "text", 0, NULL);
|
||||
gtk_tree_view_expand_all (GTK_TREE_VIEW (tree));
|
||||
|
||||
frame = gtk_frame_new (NULL);
|
||||
frame = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (frame),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (frame), GTK_SHADOW_IN);
|
||||
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (frame), TRUE);
|
||||
gtk_widget_set_vexpand (frame, TRUE);
|
||||
gtk_container_add (GTK_CONTAINER (frame), tree);
|
||||
gtk_box_pack_start (GTK_BOX (box), frame, 0, 0, 0);
|
||||
gtk_box_pack_start (GTK_BOX (box), frame, FALSE, TRUE, 0);
|
||||
gtk_box_reorder_child (GTK_BOX (box), frame, 0);
|
||||
|
||||
if (sel_iter)
|
||||
@@ -2330,6 +2344,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))
|
||||
|
||||
@@ -1266,8 +1266,6 @@ attr_list_has_attrs (PangoAttrList *attrs)
|
||||
static void
|
||||
sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
||||
{
|
||||
GdkRectangle rect;
|
||||
GtkAllocation allocation;
|
||||
GtkWidget *widget = GTK_WIDGET(entry);
|
||||
int length, i, text_len;
|
||||
const char *text;
|
||||
@@ -1299,15 +1297,8 @@ sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
||||
|
||||
gtk_entry_set_attributes (GTK_ENTRY (entry), attr_list_has_attrs (entry->priv->attr_list) ? entry->priv->attr_list : NULL);
|
||||
|
||||
if (gtk_widget_get_realized (GTK_WIDGET(entry)))
|
||||
{
|
||||
gtk_widget_get_allocation (GTK_WIDGET(entry), &allocation);
|
||||
|
||||
rect.x = 0; rect.y = 0;
|
||||
rect.width = allocation.width;
|
||||
rect.height = allocation.height;
|
||||
gdk_window_invalidate_rect(gtk_widget_get_window (widget), &rect, TRUE);
|
||||
}
|
||||
/* GtkEntry shares its parent's GdkWindow in GTK3; redraw the widget area. */
|
||||
gtk_widget_queue_draw (widget);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
/* XText regression tests. Distributed under GPL-2.0-or-later; see COPYING.
|
||||
* Include the implementation to check layout/selection invariants without
|
||||
* adding test-only symbols to the widget's public API. Application services
|
||||
* are stubbed; GTK, Pango, Cairo and the widget itself are exercised normally. */
|
||||
#include "../xtext.c"
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
struct zoitechatprefs prefs;
|
||||
|
||||
void emoji_font_apply (GtkWidget *widget) { }
|
||||
int url_last (int *start, int *end) { return 0; }
|
||||
gsize strftime_utf8 (char *dest, gsize size, const char *format, time_t stamp)
|
||||
{
|
||||
return strftime (dest, size, format, localtime (&stamp));
|
||||
}
|
||||
int xtext_get_stamp_str (time_t stamp, char **out)
|
||||
{
|
||||
*out = g_strdup ("[12:34] ");
|
||||
return strlen (*out);
|
||||
}
|
||||
void theme_get_xtext_colors_for_widget (GtkWidget *widget, XTextColor *colors, size_t len)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < len; i++)
|
||||
{
|
||||
colors[i].red = colors[i].green = colors[i].blue = 0.0;
|
||||
colors[i].alpha = 1.0;
|
||||
}
|
||||
colors[XTEXT_FG].red = colors[XTEXT_FG].green = colors[XTEXT_FG].blue = 1.0;
|
||||
colors[XTEXT_MARK_BG].blue = 1.0;
|
||||
colors[XTEXT_MARK_FG] = colors[XTEXT_FG];
|
||||
colors[XTEXT_MARKER].red = 1.0;
|
||||
}
|
||||
|
||||
typedef struct { GtkWidget *window; GtkXText *xtext; } Fixture;
|
||||
|
||||
static void drain_events (void)
|
||||
{
|
||||
while (g_main_context_iteration (NULL, FALSE)) { }
|
||||
}
|
||||
|
||||
static void fixture_setup (Fixture *f, gconstpointer data)
|
||||
{
|
||||
XTextColor colors[XTEXT_COLS];
|
||||
GtkWidget *scrolled = gtk_scrolled_window_new (NULL, NULL);
|
||||
theme_get_xtext_colors_for_widget (NULL, colors, G_N_ELEMENTS (colors));
|
||||
f->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_default_size (GTK_WINDOW (f->window), 640, 360);
|
||||
f->xtext = GTK_XTEXT (gtk_xtext_new (colors, TRUE));
|
||||
g_object_ref (f->xtext);
|
||||
gtk_container_add (GTK_CONTAINER (scrolled), GTK_WIDGET (f->xtext));
|
||||
gtk_container_add (GTK_CONTAINER (f->window), scrolled);
|
||||
g_assert_true (gtk_xtext_set_font (f->xtext, "monospace 12"));
|
||||
gtk_widget_show_all (f->window);
|
||||
drain_events ();
|
||||
}
|
||||
|
||||
static void fixture_teardown (Fixture *f, gconstpointer data)
|
||||
{
|
||||
gtk_widget_destroy (f->window);
|
||||
g_object_unref (f->xtext);
|
||||
drain_events ();
|
||||
g_assert_null (image_pending_list);
|
||||
}
|
||||
|
||||
static void append (Fixture *f, const char *text)
|
||||
{
|
||||
gtk_xtext_append (f->xtext->buffer, (guchar *)text, -1, 1234);
|
||||
}
|
||||
|
||||
static cairo_surface_t *snapshot (Fixture *f, gboolean partial)
|
||||
{
|
||||
GtkWidget *widget = GTK_WIDGET (f->xtext);
|
||||
int width = gtk_widget_get_allocated_width (widget);
|
||||
int height = gtk_widget_get_allocated_height (widget);
|
||||
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
|
||||
cairo_t *cr = cairo_create (surface);
|
||||
if (partial)
|
||||
{
|
||||
cairo_rectangle (cr, 0, f->xtext->fontsize, width, height - f->xtext->fontsize);
|
||||
cairo_clip (cr);
|
||||
}
|
||||
gtk_widget_draw (widget, cr);
|
||||
g_assert_cmpint (cairo_status (cr), ==, CAIRO_STATUS_SUCCESS);
|
||||
cairo_destroy (cr);
|
||||
cairo_surface_flush (surface);
|
||||
return surface;
|
||||
}
|
||||
|
||||
static void test_empty_and_bounded_append (Fixture *f, gconstpointer data)
|
||||
{
|
||||
const guchar bytes[] = {'a', 'b', 'c'}; /* deliberately no terminating NUL */
|
||||
append (f, "");
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_last->str, ==, "");
|
||||
g_assert_cmpint (f->xtext->buffer->text_last->subline_count, ==, 1);
|
||||
append (f, "\n");
|
||||
g_assert_cmpint (f->xtext->buffer->text_last->str_len, ==, 0);
|
||||
gtk_xtext_append (f->xtext->buffer, (guchar *)bytes, sizeof (bytes), 1234);
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_last->str, ==, "abc");
|
||||
gtk_xtext_append_indent (f->xtext->buffer, (guchar *)"", 0, (guchar *)"", 0, 1234);
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_last->str, ==, " ");
|
||||
}
|
||||
|
||||
static void test_utf8_limits (Fixture *f, gconstpointer data)
|
||||
{
|
||||
GString *text = g_string_new (NULL);
|
||||
int i;
|
||||
for (i = 0; i < 1500; i++) g_string_append (text, "猫");
|
||||
gtk_xtext_append (f->xtext->buffer, (guchar *)text->str, text->len, 1234);
|
||||
g_assert_cmpint (f->xtext->buffer->text_last->str_len, ==, 4095);
|
||||
g_assert_true (g_utf8_validate ((char *)f->xtext->buffer->text_last->str, -1, NULL));
|
||||
gtk_xtext_append_indent (f->xtext->buffer, (guchar *)text->str, text->len,
|
||||
(guchar *)"猫", -1, 1234);
|
||||
g_assert_cmpint (f->xtext->buffer->text_last->str_len, <=, 4095);
|
||||
g_assert_true (g_utf8_validate ((char *)f->xtext->buffer->text_last->str, -1, NULL));
|
||||
gtk_xtext_append (f->xtext->buffer, (guchar *)"hello 猫", 8, 1234);
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_last->str, ==, "hello ");
|
||||
gtk_xtext_append (f->xtext->buffer, (guchar *)"ok\xff" "bad", 6, 1234);
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_last->str, ==, "ok");
|
||||
g_string_free (text, TRUE);
|
||||
}
|
||||
|
||||
static void test_null_empty_prefix (Fixture *f, gconstpointer data)
|
||||
{
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
/* PrintTextLine passes NULL/0 for indented server text without a tab. */
|
||||
gtk_xtext_append_indent (buf, NULL, 0, (guchar *)"server notice", -1, 1234);
|
||||
g_assert_cmpstr ((char *)buf->text_last->str, ==, " server notice");
|
||||
g_assert_cmpint (buf->text_last->left_len, ==, 0);
|
||||
g_assert_cmpint (buf->text_last->stamp, ==, 1234);
|
||||
gtk_xtext_append_indent (buf, (guchar *)"nick", -1, NULL, 0, 1234);
|
||||
g_assert_cmpstr ((char *)buf->text_last->str, ==, "nick ");
|
||||
gtk_xtext_append_indent (buf, NULL, 0, NULL, 0, 1234);
|
||||
g_assert_cmpstr ((char *)buf->text_last->str, ==, " ");
|
||||
g_assert_cmpint (buf->num_lines, ==, 3);
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
}
|
||||
|
||||
static void test_wrapping (Fixture *f, gconstpointer data)
|
||||
{
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
textentry *ent;
|
||||
int start = 0, i;
|
||||
append (f, "猫猫猫猫猫猫猫猫猫猫");
|
||||
ent = buf->text_last;
|
||||
/* A character wider than the row must still be kept whole. */
|
||||
buf->window_width = 3;
|
||||
gtk_xtext_lines_taken (buf, ent);
|
||||
g_assert_cmpint (ent->subline_count, ==, 10);
|
||||
for (i = 0; i < ent->subline_count; i++)
|
||||
{
|
||||
int end = gtk_xtext_subline_end (ent, i);
|
||||
g_assert_cmpint (end, >, start);
|
||||
g_assert_true (g_utf8_validate ((char *)ent->str + start, end - start, NULL));
|
||||
start = end;
|
||||
}
|
||||
buf->window_width = 120;
|
||||
gtk_xtext_lines_taken (buf, ent);
|
||||
g_assert_cmpint (ent->subline_count, >, 1);
|
||||
buf->window_width = 640;
|
||||
gtk_xtext_lines_taken (buf, ent);
|
||||
g_assert_cmpint (ent->subline_count, ==, 1);
|
||||
}
|
||||
|
||||
static void test_font_isolation (Fixture *f, gconstpointer data)
|
||||
{
|
||||
Fixture second;
|
||||
int original = gtk_xtext_text_width (f->xtext, (guchar *)"WWii hello", 10);
|
||||
fixture_setup (&second, NULL);
|
||||
g_assert_true (gtk_xtext_set_font (second.xtext, "sans 30"));
|
||||
g_assert_cmpint (gtk_xtext_text_width (second.xtext, (guchar *)"WWii hello", 10), >, original);
|
||||
g_assert_cmpint (gtk_xtext_text_width (f->xtext, (guchar *)"WWii hello", 10), ==, original);
|
||||
fixture_teardown (&second, NULL);
|
||||
g_assert_cmpint (gtk_xtext_text_width (f->xtext, (guchar *)"WWii hello", 10), ==, original);
|
||||
}
|
||||
|
||||
static void test_wide_metrics (Fixture *f, gconstpointer data)
|
||||
{
|
||||
char *text = g_strnfill (4000, 'W');
|
||||
g_assert_true (gtk_xtext_set_font (f->xtext, "sans 40"));
|
||||
append (f, text);
|
||||
g_assert_cmpint (f->xtext->buffer->text_last->str_width, >, 65535);
|
||||
g_assert_cmpint (f->xtext->buffer->text_last->subline_count, >, 1);
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
static void test_formatting (Fixture *f, gconstpointer data)
|
||||
{
|
||||
const char *formatted = "a\002bold\002 \035italic\035 \00304,02red\017 \037u\037 \036s\036 \026r\026";
|
||||
char *plain;
|
||||
GSList *runs = NULL;
|
||||
int len;
|
||||
plain = (char *)gtk_xtext_strip_color ((guchar *)formatted, strlen (formatted), NULL, &len, &runs, FALSE);
|
||||
g_assert_cmpstr (plain, ==, "abold italic red u s r");
|
||||
g_assert_cmpint (len, ==, strlen (plain));
|
||||
g_assert_cmpint (((offlen_t *)runs->data)->off, ==, 0);
|
||||
g_slist_free_full (runs, g_free);
|
||||
g_free (plain);
|
||||
plain = (char *)gtk_xtext_strip_color ((guchar *)"a\010secret\010b", 10, NULL, &len, NULL, TRUE);
|
||||
g_assert_cmpstr (plain, ==, "ab");
|
||||
g_free (plain);
|
||||
append (f, formatted);
|
||||
gtk_xtext_append_indent (f->xtext->buffer, (guchar *)"nick", -1, (guchar *)formatted, -1, 1234);
|
||||
f->xtext->auto_indent = TRUE;
|
||||
f->xtext->buffer->time_stamp = TRUE;
|
||||
f->xtext->buffer->indent = 130;
|
||||
gtk_xtext_recalc_widths (f->xtext->buffer, FALSE);
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_last->str, ==,
|
||||
"nick a\002bold\002 \035italic\035 \00304,02red\017 \037u\037 \036s\036 \026r\026");
|
||||
}
|
||||
|
||||
static void select_entries (Fixture *f, textentry *start, textentry *end)
|
||||
{
|
||||
textentry *ent;
|
||||
for (ent = start; ent; ent = ent->next)
|
||||
{
|
||||
ent->mark_start = 0;
|
||||
ent->mark_end = ent->str_len;
|
||||
if (ent == end) break;
|
||||
}
|
||||
gtk_xtext_selection_render (f->xtext, start, end);
|
||||
f->xtext->selection_buffer = f->xtext->buffer;
|
||||
}
|
||||
|
||||
static void test_selection_and_clipboard (Fixture *f, gconstpointer data)
|
||||
{
|
||||
char *selected;
|
||||
int len;
|
||||
append (f, "");
|
||||
append (f, "\002bold\002 猫");
|
||||
append (f, "last");
|
||||
select_entries (f, f->xtext->buffer->text_first, f->xtext->buffer->text_last);
|
||||
selected = gtk_xtext_selection_get_text (f->xtext, &len);
|
||||
g_assert_cmpstr (selected, ==, "\nbold 猫\nlast");
|
||||
g_assert_cmpint (len, ==, strlen (selected));
|
||||
g_free (selected);
|
||||
f->xtext->color_paste = TRUE;
|
||||
f->xtext->mark_stamp = TRUE;
|
||||
selected = gtk_xtext_selection_get_text (f->xtext, &len);
|
||||
g_assert_cmpstr (selected, ==, "\n[12:34] \002bold\002 猫\n[12:34] last");
|
||||
g_free (selected);
|
||||
f->xtext->color_paste = f->xtext->mark_stamp = FALSE;
|
||||
gtk_xtext_copy_selection (f->xtext);
|
||||
selected = gtk_clipboard_wait_for_text (gtk_widget_get_clipboard (GTK_WIDGET (f->xtext), GDK_SELECTION_CLIPBOARD));
|
||||
g_assert_cmpstr (selected, ==, "\nbold 猫\nlast");
|
||||
g_free (selected);
|
||||
selected = gtk_clipboard_wait_for_text (gtk_widget_get_clipboard (GTK_WIDGET (f->xtext), GDK_SELECTION_PRIMARY));
|
||||
g_assert_cmpstr (selected, ==, "\nbold 猫\nlast");
|
||||
g_free (selected);
|
||||
gtk_xtext_unselect (f->xtext);
|
||||
g_assert_false (gtk_xtext_is_selecting (f->xtext));
|
||||
}
|
||||
|
||||
static void test_search (Fixture *f, gconstpointer data)
|
||||
{
|
||||
GError *error = NULL;
|
||||
textentry *first;
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
append (f, "one \002needle\002 then NEEDLE");
|
||||
first = buf->text_last;
|
||||
append (f, "second needle");
|
||||
g_assert_true (gtk_xtext_search (f->xtext, "needle", highlight, &error) == first);
|
||||
g_assert_no_error (error);
|
||||
g_assert_cmpint (g_list_length (first->marks), ==, 2);
|
||||
g_assert_true (gtk_xtext_search (f->xtext, "needle", highlight, &error) == first);
|
||||
g_assert_true (gtk_xtext_search (f->xtext, "needle", highlight, &error) == buf->text_last);
|
||||
gtk_xtext_search (f->xtext, "", 0, &error);
|
||||
g_assert_null (buf->search_found);
|
||||
g_assert_true (gtk_xtext_search (f->xtext, "needle", backward | case_match, &error) == buf->text_last);
|
||||
gtk_xtext_search (f->xtext, "n[a-z]+", regexp | case_match, &error);
|
||||
g_assert_no_error (error);
|
||||
g_assert_nonnull (buf->search_re);
|
||||
gtk_xtext_search (f->xtext, "[", regexp, &error);
|
||||
g_assert_error (error, G_REGEX_ERROR, G_REGEX_ERROR_UNTERMINATED_CHARACTER_CLASS);
|
||||
g_clear_error (&error);
|
||||
/* A no-match search must also release its regex/text on clear. */
|
||||
gtk_xtext_search (f->xtext, "nomatch", regexp, &error);
|
||||
g_assert_null (buf->search_found);
|
||||
gtk_xtext_clear (buf, 0);
|
||||
g_assert_null (buf->search_re);
|
||||
g_assert_null (buf->search_text);
|
||||
}
|
||||
|
||||
static void test_follow_search (Fixture *f, gconstpointer data)
|
||||
{
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
append (f, "match first");
|
||||
gtk_xtext_search (f->xtext, "match", 0, NULL);
|
||||
append (f, "match second");
|
||||
gtk_xtext_search (f->xtext, NULL, follow, NULL);
|
||||
g_assert_cmpint (g_list_length (buf->search_found), ==, 2);
|
||||
GList *current = buf->curmark;
|
||||
gtk_xtext_search (f->xtext, NULL, 0, NULL);
|
||||
gtk_xtext_search (f->xtext, NULL, follow, NULL);
|
||||
g_assert_cmpint (g_list_length (buf->search_found), ==, 2);
|
||||
g_assert_true (buf->curmark == current);
|
||||
append (f, "match third");
|
||||
g_assert_cmpint (g_list_length (buf->search_found), ==, 3);
|
||||
}
|
||||
|
||||
static void test_search_unicode_offsets (Fixture *f, gconstpointer data)
|
||||
{
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
offsets_t mark;
|
||||
append (f, "İ \002NEEDLE\002 Straße");
|
||||
gtk_xtext_search (f->xtext, "needle", 0, NULL);
|
||||
mark.u = GPOINTER_TO_UINT (buf->text_first->marks->data);
|
||||
g_assert_cmpint (mark.o.start, ==, 4);
|
||||
/* The established color-offset mapping includes the closing control. */
|
||||
g_assert_cmpint (mark.o.end, ==, 11);
|
||||
gtk_xtext_search (f->xtext, "ss", 0, NULL);
|
||||
mark.u = GPOINTER_TO_UINT (buf->text_first->marks->data);
|
||||
g_assert_cmpint (mark.o.start, ==, 16);
|
||||
g_assert_cmpint (mark.o.end, ==, 18);
|
||||
gtk_xtext_clear (buf, 0);
|
||||
append (f, "ſ needle");
|
||||
gtk_xtext_search (f->xtext, "needle", 0, NULL);
|
||||
mark.u = GPOINTER_TO_UINT (buf->text_first->marks->data);
|
||||
g_assert_cmpint (mark.o.start, ==, 3);
|
||||
g_assert_cmpint (mark.o.end, ==, 9);
|
||||
}
|
||||
|
||||
static void test_rawlog_hidden_text (Fixture *f, gconstpointer data)
|
||||
{
|
||||
int hidden, visible;
|
||||
append (f, "a\010secret\010b");
|
||||
hidden = f->xtext->buffer->text_last->str_width;
|
||||
f->xtext->ignore_hidden = TRUE;
|
||||
gtk_xtext_recalc_widths (f->xtext->buffer, TRUE);
|
||||
visible = f->xtext->buffer->text_last->str_width;
|
||||
g_assert_cmpint (visible, >, hidden);
|
||||
g_assert_cmpint (visible, ==, gtk_xtext_text_width (f->xtext, (guchar *)"asecretb", 8));
|
||||
}
|
||||
|
||||
static void test_save (Fixture *f, gconstpointer data)
|
||||
{
|
||||
char *path, *saved;
|
||||
GError *error = NULL;
|
||||
int fd = g_file_open_tmp ("zoitechat-xtext-XXXXXX", &path, &error);
|
||||
g_assert_no_error (error);
|
||||
append (f, "\002bold\002 猫");
|
||||
append (f, "");
|
||||
append (f, "last");
|
||||
gtk_xtext_save (f->xtext, fd);
|
||||
close (fd);
|
||||
g_assert_true (g_file_get_contents (path, &saved, NULL, &error));
|
||||
g_assert_no_error (error);
|
||||
g_assert_cmpstr (saved, ==, "bold 猫\n\nlast\n");
|
||||
g_free (saved);
|
||||
g_unlink (path);
|
||||
g_free (path);
|
||||
}
|
||||
|
||||
static void test_scrollable_ownership (Fixture *f, gconstpointer data)
|
||||
{
|
||||
GtkAdjustment *horizontal = GTK_ADJUSTMENT (gtk_adjustment_new (0, 0, 1, 1, 1, 1));
|
||||
GtkAdjustment *vertical = GTK_ADJUSTMENT (gtk_adjustment_new (0, 0, 100, 1, 10, 10));
|
||||
GtkAdjustment *old = g_object_ref (f->xtext->adj);
|
||||
g_object_set (f->xtext, "hadjustment", horizontal, "vadjustment", vertical, NULL);
|
||||
g_assert_false (g_object_is_floating (horizontal));
|
||||
g_assert_false (g_object_is_floating (vertical));
|
||||
g_assert_true (f->xtext->hadj == horizontal);
|
||||
g_assert_true (f->xtext->adj == vertical);
|
||||
/* A replaced adjustment must no longer call into the widget. */
|
||||
g_assert_cmpuint (g_signal_handler_find (old, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, f->xtext), ==, 0);
|
||||
g_signal_emit_by_name (f->xtext, "set-scroll-adjustments", horizontal, vertical);
|
||||
g_assert_true (f->xtext->adj == vertical);
|
||||
g_object_set (f->xtext, "hadjustment", NULL, "vadjustment", NULL, NULL);
|
||||
g_assert_true (GTK_IS_ADJUSTMENT (f->xtext->hadj));
|
||||
g_assert_true (GTK_IS_ADJUSTMENT (f->xtext->adj));
|
||||
g_object_unref (old);
|
||||
}
|
||||
|
||||
static void test_clear_and_marker (Fixture *f, gconstpointer data)
|
||||
{
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
append (f, "first"); append (f, "second"); append (f, "third");
|
||||
buf->marker_pos = buf->text_first;
|
||||
buf->marker_state = MARKER_IS_SET;
|
||||
gtk_xtext_clear (buf, 1);
|
||||
g_assert_cmpstr ((char *)buf->text_first->str, ==, "second");
|
||||
g_assert_cmpint (buf->marker_state, ==, MARKER_RESET_BY_CLEAR);
|
||||
gtk_xtext_clear (buf, -1);
|
||||
g_assert_cmpstr ((char *)buf->text_last->str, ==, "second");
|
||||
select_entries (f, buf->text_first, buf->text_last);
|
||||
f->xtext->hilight_ent = buf->text_first;
|
||||
gtk_xtext_clear (buf, 0);
|
||||
g_assert_null (f->xtext->hilight_ent);
|
||||
g_assert_null (buf->pagetop_ent);
|
||||
g_assert_null (buf->last_ent_start);
|
||||
g_assert_cmpint (buf->num_lines, ==, 0);
|
||||
g_assert_true (gtk_xtext_is_empty (buf));
|
||||
append (f, "after clear");
|
||||
g_assert_cmpint (buf->num_lines, ==, 1);
|
||||
}
|
||||
|
||||
static void test_scrollback_limit (Fixture *f, gconstpointer data)
|
||||
{
|
||||
int i;
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
gtk_xtext_set_max_lines (f->xtext, 10);
|
||||
for (i = 0; i < 100; i++) append (f, "line");
|
||||
g_assert_cmpint (buf->num_lines, ==, 10);
|
||||
g_assert_null (buf->text_first->prev);
|
||||
g_assert_null (buf->text_last->next);
|
||||
g_assert_cmpfloat (gtk_adjustment_get_value (f->xtext->adj), >=, 0);
|
||||
}
|
||||
|
||||
static void test_follow_trim_oversized_entry (Fixture *f, gconstpointer data)
|
||||
{
|
||||
char *long_text = g_strnfill (4000, 'W');
|
||||
gtk_xtext_set_max_lines (f->xtext, 3);
|
||||
append (f, "needle");
|
||||
gtk_xtext_search (f->xtext, "W", follow, NULL);
|
||||
gtk_xtext_clear (f->xtext->buffer, 1);
|
||||
append (f, long_text);
|
||||
g_assert_true (gtk_xtext_is_empty (f->xtext->buffer));
|
||||
g_assert_null (f->xtext->buffer->search_found);
|
||||
g_free (long_text);
|
||||
append (f, "after oversized");
|
||||
gtk_xtext_clear (f->xtext->buffer, G_MININT);
|
||||
g_assert_true (gtk_xtext_is_empty (f->xtext->buffer));
|
||||
}
|
||||
|
||||
static void test_stress (Fixture *f, gconstpointer data)
|
||||
{
|
||||
static const char *tokens[] = {"text ", "猫", "😀", "\002bold\002", "\00304,02red\017", "\010hidden\010", "\037u\037", "\036s\036"};
|
||||
GRand *random = g_rand_new_with_seed (0x78746578);
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
int i;
|
||||
gtk_xtext_set_max_lines (f->xtext, 80);
|
||||
for (i = 0; i < 2000; i++)
|
||||
{
|
||||
GString *text = g_string_new (NULL);
|
||||
textentry *ent;
|
||||
int j, lines = 0;
|
||||
for (j = 0; j < 30; j++)
|
||||
g_string_append (text, tokens[g_rand_int_range (random, 0, G_N_ELEMENTS (tokens))]);
|
||||
if (i % 2)
|
||||
gtk_xtext_append_indent (buf, (guchar *)"nick", -1, (guchar *)text->str, text->len, 1234);
|
||||
else
|
||||
gtk_xtext_append (buf, (guchar *)text->str, text->len, 1234);
|
||||
g_string_free (text, TRUE);
|
||||
if (i % 29 == 0) gtk_xtext_search (f->xtext, "text", follow | highlight, NULL);
|
||||
if (i % 31 == 0) gtk_xtext_clear (buf, 1);
|
||||
if (i % 37 == 0) gtk_xtext_clear (buf, -1);
|
||||
if (i % 101 == 0) gtk_xtext_clear (buf, 0);
|
||||
for (ent = buf->text_first; ent; ent = ent->next)
|
||||
{
|
||||
g_assert_true (g_utf8_validate ((char *)ent->str, ent->str_len, NULL));
|
||||
g_assert_cmpint (ent->subline_count, >, 0);
|
||||
lines += ent->subline_count;
|
||||
}
|
||||
g_assert_cmpint (buf->num_lines, ==, lines);
|
||||
if (i % 43 == 0) cairo_surface_destroy (snapshot (f, FALSE));
|
||||
}
|
||||
g_rand_free (random);
|
||||
}
|
||||
|
||||
static void test_buffers (Fixture *f, gconstpointer data)
|
||||
{
|
||||
xtext_buffer *original = f->xtext->buffer;
|
||||
xtext_buffer *second = gtk_xtext_buffer_new (f->xtext);
|
||||
append (f, "original");
|
||||
gtk_xtext_append (second, (guchar *)"alternate", -1, 1234);
|
||||
gtk_xtext_buffer_show (f->xtext, second, TRUE);
|
||||
g_assert_true (f->xtext->buffer == second);
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
gtk_xtext_buffer_show (f->xtext, original, TRUE);
|
||||
g_assert_cmpstr ((char *)f->xtext->buffer->text_first->str, ==, "original");
|
||||
gtk_xtext_buffer_free (second);
|
||||
}
|
||||
|
||||
static void test_partial_draw_after_scroll (Fixture *f, gconstpointer data)
|
||||
{
|
||||
cairo_surface_t *partial, *full;
|
||||
int i, stride, height, y;
|
||||
for (i = 0; i < 80; i++)
|
||||
{
|
||||
char *line = g_strdup_printf ("line %d — 猫 \002bold\002", i);
|
||||
append (f, line);
|
||||
g_free (line);
|
||||
}
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
gtk_adjustment_set_value (f->xtext->adj, 12.5);
|
||||
partial = snapshot (f, TRUE);
|
||||
full = snapshot (f, FALSE);
|
||||
stride = cairo_image_surface_get_stride (full);
|
||||
height = cairo_image_surface_get_height (full);
|
||||
for (y = f->xtext->fontsize; y < height; y++)
|
||||
g_assert_cmpmem (cairo_image_surface_get_data (partial) + y * stride, stride,
|
||||
cairo_image_surface_get_data (full) + y * stride, stride);
|
||||
cairo_surface_destroy (partial);
|
||||
cairo_surface_destroy (full);
|
||||
}
|
||||
|
||||
static void test_image_lifecycle (Fixture *f, gconstpointer data)
|
||||
{
|
||||
GdkPixbuf *image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, 800, 600);
|
||||
xtext_buffer *buf = f->xtext->buffer;
|
||||
textentry *ent;
|
||||
guint handle;
|
||||
int before;
|
||||
gdk_pixbuf_fill (image, 0xff0000ff);
|
||||
append (f, "image");
|
||||
ent = buf->text_last;
|
||||
before = buf->num_lines;
|
||||
handle = gtk_xtext_image_load_begin (f->xtext, ent, "https://example.invalid/image.png");
|
||||
g_assert_cmpuint (handle, !=, 0);
|
||||
g_assert_cmpuint (gtk_xtext_image_load_begin (f->xtext, ent, "duplicate"), ==, 0);
|
||||
g_assert_true (gtk_xtext_image_load_finish (handle, image));
|
||||
g_assert_cmpint (buf->num_lines, >, before);
|
||||
g_assert_true (gtk_xtext_entry_has_image (f->xtext, ent));
|
||||
g_assert_cmpint (gdk_pixbuf_get_width (ent->image), <=, XTEXT_IMAGE_MAX_WIDTH);
|
||||
g_assert_true (gtk_xtext_entry_get_full_image (f->xtext, ent) == image);
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
g_assert_true (gtk_xtext_image_remove (f->xtext, ent));
|
||||
g_assert_cmpint (buf->num_lines, ==, before);
|
||||
handle = gtk_xtext_image_load_begin (f->xtext, ent, "late");
|
||||
gtk_xtext_clear (buf, 0);
|
||||
g_assert_false (gtk_xtext_image_load_finish (handle, image));
|
||||
g_assert_false (gtk_xtext_entry_has_image (f->xtext, ent));
|
||||
g_assert_null (gtk_xtext_entry_get_full_image (f->xtext, ent));
|
||||
g_object_unref (image);
|
||||
}
|
||||
|
||||
static void test_background (Fixture *f, gconstpointer data)
|
||||
{
|
||||
cairo_surface_t *background = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 32, 32);
|
||||
cairo_t *cr = cairo_create (background);
|
||||
cairo_set_source_rgb (cr, 0.2, 0.3, 0.4);
|
||||
cairo_paint (cr);
|
||||
cairo_destroy (cr);
|
||||
gtk_xtext_set_background (f->xtext, background);
|
||||
cairo_surface_destroy (background);
|
||||
gtk_xtext_set_background (f->xtext, f->xtext->background_surface);
|
||||
append (f, "background text");
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
gtk_xtext_set_background (f->xtext, NULL);
|
||||
cairo_surface_destroy (snapshot (f, FALSE));
|
||||
}
|
||||
|
||||
static void test_destroy_pending_sources (Fixture *f, gconstpointer data)
|
||||
{
|
||||
guint image;
|
||||
append (f, "pending");
|
||||
image = gtk_xtext_image_load_begin (f->xtext, f->xtext->buffer->text_last, "late");
|
||||
f->xtext->io_tag = g_timeout_add (10000, gtk_xtext_adjustment_timeout, f->xtext);
|
||||
f->xtext->add_io_tag = g_timeout_add (10000, gtk_xtext_render_page_timeout, f->xtext);
|
||||
f->xtext->scroll_tag = g_timeout_add (10000, gtk_xtext_scrollup_timeout, f->xtext);
|
||||
gtk_widget_destroy (f->window);
|
||||
g_assert_cmpint (f->xtext->io_tag, ==, 0);
|
||||
g_assert_cmpint (f->xtext->add_io_tag, ==, 0);
|
||||
g_assert_cmpint (f->xtext->scroll_tag, ==, 0);
|
||||
g_assert_false (gtk_xtext_image_load_finish (image, NULL));
|
||||
/* GtkWidget destruction must be safe if GObject dispose runs again. */
|
||||
g_object_run_dispose (G_OBJECT (f->xtext));
|
||||
f->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
|
||||
}
|
||||
|
||||
int main (int argc, char **argv)
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
if (!gtk_init_check (&argc, &argv))
|
||||
{
|
||||
g_test_message ("GTK display unavailable; run under Xvfb for widget coverage");
|
||||
return 77;
|
||||
}
|
||||
prefs.hex_gui_mouse_scroll_speed = 10;
|
||||
#define ADD(name, function) g_test_add ("/xtext/" name, Fixture, NULL, fixture_setup, function, fixture_teardown)
|
||||
ADD ("append/empty-bounded", test_empty_and_bounded_append);
|
||||
ADD ("append/utf8-limits", test_utf8_limits);
|
||||
ADD ("append/null-empty-prefix", test_null_empty_prefix);
|
||||
ADD ("layout/wrapping", test_wrapping);
|
||||
ADD ("layout/font-isolation", test_font_isolation);
|
||||
ADD ("layout/wide-metrics", test_wide_metrics);
|
||||
ADD ("render/formatting-timestamps", test_formatting);
|
||||
ADD ("selection/clipboard", test_selection_and_clipboard);
|
||||
ADD ("search/navigation-regex-clear", test_search);
|
||||
ADD ("search/follow", test_follow_search);
|
||||
ADD ("search/unicode-offsets", test_search_unicode_offsets);
|
||||
ADD ("render/rawlog-hidden", test_rawlog_hidden_text);
|
||||
ADD ("buffer/save", test_save);
|
||||
ADD ("lifecycle/scrollable-ownership", test_scrollable_ownership);
|
||||
ADD ("buffer/clear-marker", test_clear_and_marker);
|
||||
ADD ("buffer/scrollback-limit", test_scrollback_limit);
|
||||
ADD ("buffer/follow-trim-oversized", test_follow_trim_oversized_entry);
|
||||
ADD ("buffer/stress", test_stress);
|
||||
ADD ("buffer/switch", test_buffers);
|
||||
ADD ("render/partial-scroll", test_partial_draw_after_scroll);
|
||||
ADD ("images/lifecycle", test_image_lifecycle);
|
||||
ADD ("render/background", test_background);
|
||||
ADD ("lifecycle/pending-sources", test_destroy_pending_sources);
|
||||
#undef ADD
|
||||
return g_test_run ();
|
||||
}
|
||||
+10
-7
@@ -400,7 +400,6 @@ pevent_treeview_new (GtkWidget *box)
|
||||
scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_IN);
|
||||
gtk_widget_set_size_request (GTK_WIDGET (scroll), -1, 250);
|
||||
|
||||
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
|
||||
g_return_val_if_fail (store != NULL, NULL);
|
||||
@@ -437,7 +436,7 @@ pevent_treeview_new (GtkWidget *box)
|
||||
gtk_tree_view_column_set_min_width (col, 100);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (scroll), view);
|
||||
gtk_container_add (GTK_CONTAINER (box), scroll);
|
||||
gtk_paned_pack1 (GTK_PANED (box), scroll, TRUE, FALSE);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -481,7 +480,7 @@ pevent_hlist_treeview_new (GtkWidget *box)
|
||||
gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
|
||||
|
||||
gtk_container_add (GTK_CONTAINER (scroll), view);
|
||||
gtk_container_add (GTK_CONTAINER (box), scroll);
|
||||
gtk_paned_pack2 (GTK_PANED (box), scroll, TRUE, FALSE);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -489,7 +488,7 @@ pevent_hlist_treeview_new (GtkWidget *box)
|
||||
void
|
||||
pevent_dialog_show ()
|
||||
{
|
||||
GtkWidget *vbox, *hbox, *wid, *pane;
|
||||
GtkWidget *vbox, *hbox, *wid, *pane, *preview_pane;
|
||||
XTextColor xtext_palette[XTEXT_COLS];
|
||||
|
||||
if (pevent_dialog)
|
||||
@@ -503,8 +502,12 @@ pevent_dialog_show ()
|
||||
TRUE, FALSE, pevent_dialog_close, NULL,
|
||||
600, 455, &vbox, 0);
|
||||
|
||||
preview_pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), preview_pane, TRUE, TRUE, 0);
|
||||
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
|
||||
gtk_paned_pack1 (GTK_PANED (preview_pane), pane, TRUE, FALSE);
|
||||
gtk_paned_set_position (GTK_PANED (pane), 220);
|
||||
gtk_paned_set_position (GTK_PANED (preview_pane), 330);
|
||||
|
||||
pevent_dialog_list = pevent_treeview_new (pane);
|
||||
pevent_dialog_fill (pevent_dialog_list);
|
||||
@@ -513,12 +516,12 @@ pevent_dialog_show ()
|
||||
|
||||
wid = gtk_scrolled_window_new (NULL, NULL);
|
||||
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (wid), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
|
||||
gtk_box_pack_start (GTK_BOX (vbox), wid, FALSE, TRUE, 0);
|
||||
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (wid), 75);
|
||||
gtk_paned_pack2 (GTK_PANED (preview_pane), wid, TRUE, FALSE);
|
||||
|
||||
theme_get_xtext_colors_for_widget (wid, xtext_palette, XTEXT_COLS);
|
||||
pevent_dialog_twid = gtk_xtext_new (xtext_palette, 0);
|
||||
gtk_widget_set_sensitive (pevent_dialog_twid, FALSE);
|
||||
gtk_widget_set_size_request (pevent_dialog_twid, -1, 75);
|
||||
gtk_container_add (GTK_CONTAINER (wid), pevent_dialog_twid);
|
||||
gtk_xtext_set_font (GTK_XTEXT (pevent_dialog_twid), prefs.hex_text_font);
|
||||
g_object_set_data (G_OBJECT (pevent_dialog), "xtext", pevent_dialog_twid);
|
||||
|
||||
@@ -683,7 +683,7 @@ theme_manager_get_userlist_palette_behavior (const PangoFontDescription *font_de
|
||||
|
||||
behavior.font_desc = font_desc;
|
||||
behavior.apply_background = TRUE;
|
||||
behavior.apply_foreground = (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks) ? FALSE : TRUE;
|
||||
behavior.apply_foreground = prefs.hex_gui_ulist_color ? FALSE : TRUE;
|
||||
|
||||
return behavior;
|
||||
}
|
||||
|
||||
@@ -1911,6 +1911,7 @@ theme_preferences_create_page (GtkWindow *parent,
|
||||
G_TYPE_STRING,
|
||||
G_TYPE_INT);
|
||||
ui->gtk3_combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (gtk3_store));
|
||||
gtk_widget_set_hexpand (ui->gtk3_combo, TRUE);
|
||||
g_object_unref (gtk3_store);
|
||||
renderer = gtk_cell_renderer_text_new ();
|
||||
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->gtk3_combo), renderer, TRUE);
|
||||
|
||||
@@ -594,7 +594,7 @@ fe_userlist_rehash (session *sess, struct User *user)
|
||||
nick_token = THEME_TOKEN_TAB_AWAY;
|
||||
have_nick_token = TRUE;
|
||||
}
|
||||
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
|
||||
else if (prefs.hex_gui_ulist_color)
|
||||
{
|
||||
int mirc_index = text_color_of (user->nick);
|
||||
|
||||
@@ -635,7 +635,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
|
||||
nick_token = THEME_TOKEN_TAB_AWAY;
|
||||
have_nick_token = TRUE;
|
||||
}
|
||||
else if (prefs.hex_gui_ulist_color || prefs.hex_text_color_nicks)
|
||||
else if (prefs.hex_gui_ulist_color)
|
||||
{
|
||||
int mirc_index = text_color_of (newuser->nick);
|
||||
|
||||
|
||||
+1128
-1084
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ GType gtk_xtext_get_type (void);
|
||||
|
||||
#define ATTR_BOLD '\002'
|
||||
#define ATTR_COLOR '\003'
|
||||
#define ATTR_HEXCOLOR '\004'
|
||||
#define ATTR_BLINK '\006'
|
||||
#define ATTR_BEEP '\007'
|
||||
#define ATTR_HIDDEN '\010'
|
||||
@@ -57,6 +58,8 @@ GType gtk_xtext_get_type (void);
|
||||
#define XTEXT_BG 102
|
||||
#define XTEXT_MARKER 103 /* for marker line */
|
||||
#define XTEXT_MAX_COLOR 98
|
||||
#define XTEXT_HEX_FG 200 /* not palette slots: colors from \004RRGGBB */
|
||||
#define XTEXT_HEX_BG 201
|
||||
typedef struct textentry textentry;
|
||||
|
||||
/*
|
||||
@@ -181,6 +184,8 @@ struct _GtkXText
|
||||
|
||||
int col_fore;
|
||||
int col_back;
|
||||
XTextColor hex_fg;
|
||||
XTextColor hex_bg;
|
||||
|
||||
int depth; /* gdk window depth */
|
||||
|
||||
@@ -192,6 +197,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 +260,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 +318,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
|
||||
|
||||
@@ -467,6 +467,9 @@ static const GOptionEntry gopt_entries[] =
|
||||
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
|
||||
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
|
||||
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
|
||||
#ifdef __linux__
|
||||
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
|
||||
#endif
|
||||
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
|
||||
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
|
||||
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel URL"), "URL"},
|
||||
@@ -475,6 +478,16 @@ static const GOptionEntry gopt_entries[] =
|
||||
{NULL}
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
void
|
||||
fe_root_warning (void)
|
||||
{
|
||||
fputs ("ZoiteChat refuses to run as root on Linux.\n"
|
||||
"Run it as a normal user, or pass --run-as-root to override this protection.\n",
|
||||
stderr);
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
fe_args (int argc, char *argv[])
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user