22 Commits

Author SHA1 Message Date
58208b81f2 Fix away tracking
(cherry picked from commit 87410aea50)
2026-09-15 13:04:10 -06:00
e7631701ce release: prepare 2.19.1 2026-09-15 09:28:29 -06:00
b5c3be8ff1 defer channel tree scrolling until realized
(cherry picked from commit e94ffd9d79)
2026-09-15 09:12:34 -06:00
2b9a39783b fix theme service tests
(cherry picked from commit ff4217c3d1)
2026-09-15 09:12:30 -06:00
Christopher Byrne
ecdb462ff8 meson.build: Reflect usage of 0.59 Meson features
src/fe-gtk/meson.build calls feature_opt.allowed() which wasn't
added until 0.59.

Signed-off-by: Christopher Byrne <salah.coronya@gmail.com>
(cherry picked from commit 5018a3a848)
2026-09-15 09:12:25 -06:00
Christopher Byrne
78eb1882cb data/misc/meson.build: Fix referencing possibly undefined variable "appstreamcli"
If meson is passed -Dinstall-plugin-metainfo=false, the "appstreamcli"
will not be defined, but it is later referenced in global scope, which
causes and "undefined variable" error in Meson

Signed-off-by: Christopher Byrne <salah.coronya@gmail.com>
(cherry picked from commit df1c74a91d)
2026-09-15 09:12:21 -06:00
6c95db8539 Fix channel ACTION routing after self-message handling
(cherry picked from commit bb3893d36c)
2026-09-15 09:12:16 -06:00
ThePeGaSuS
75286642df Fix CTCP ACTION not rerouting self-sent /me to correct PM window
Mirrors the PRIVMSG fix: replayed /me messages sent by our own nick
via WeeChat relay backlog were always passed with fromme=FALSE,
so they never got rerouted to the actual PM query window.

(cherry picked from commit c725adaa09)
2026-09-15 09:12:13 -06:00
ThePeGaSuS
8044b8562d Fix: private messages split into two windows over WeeChat's IRC relay
When you send a private message, ZoiteChat sometimes gets the same
message echoed back by the server so it can show it in the right
window. Whether it recognizes that echo depended on the server first
confirming a feature called "echo-message" during connection setup.

WeeChat's relay doesn't seem to confirm that feature the way other
servers (or ZNC) do, so ZoiteChat didn't recognize its own echoed
messages and treated them as brand new incoming messages from
yourself, opening a second, separate conversation window instead of
using the one you were already typing in.

This makes ZoiteChat recognize its own message by checking who sent
it, instead of only trusting that feature confirmation. That way,
sent and received messages always end up in the same conversation
window, regardless of how well the server/relay announces its
capabilities.

(cherry picked from commit 0ba523d071)
2026-09-15 09:12:09 -06:00
de6e16cbdf fix startup error on flatpak
(cherry picked from commit 36f0d7450c)
2026-09-15 09:12:02 -06:00
909309cc2e WIN32: fix installer theme download URL. 2026-08-15 12:49:58 -06:00
deepend-tildeclub
73d958911f Merge pull request #364 from ZoiteChat/master
run Ci for release branches
2026-08-15 12:10:29 -06:00
6b4a2b5406 fix unmark away option 2026-08-15 12:07:41 -06:00
cd8d1c9b10 Run Ci on Release/version branches on commit 2026-08-04 12:11:30 -06:00
deepend-tildeclub
a79fedda77 Merge pull request #360 from ZoiteChat/release/2.19
fix duplicate /msg output + Escaping of the intial slash
2026-08-04 12:10:53 -06:00
558ef7fbdf fix Escaping of the intial slash 2026-08-04 11:58:30 -06:00
7c6609a414 fix duplicate /msg output 2026-08-04 11:35:48 -06:00
54514f494f Version bump for 2.19.0 Release. 2026-07-28 10:26:04 -06:00
deepend-tildeclub
67aa7ca79f Merge pull request #355 from ZoiteChat/centralize_version_management
Centralize release version in root VERSION file.
2026-07-28 09:31:12 -06:00
344f5af01b Centralize release version in root VERSION file. 2026-07-28 09:12:53 -06:00
deepend-tildeclub
8150d6c48c Merge pull request #351 from ZoiteChat/reduce-client-restarts
Reduce needed client restarts for settings changes.
2026-07-19 17:02:04 -06:00
05ee0dbdb5 Reduce needed client restarts for settings changes. 2026-07-16 08:48:33 -06:00
30 changed files with 643 additions and 105 deletions

View File

@@ -2,7 +2,7 @@ name: AppImage Build
on:
push:
branches: [master]
branches: [master, 'release/**']
pull_request:
branches: [master]

View File

@@ -2,7 +2,7 @@ name: Flatpak Build
on:
push:
branches: [master]
branches: [master, 'release/**']
pull_request:
branches: [master]

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- master
- 'release/**'
pull_request:
branches:
- master

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2.19.1

View File

@@ -1,6 +1,20 @@
ZoiteChat ChangeLog
=================
2.19.0 (2026-07-28)
-------------------
- Added Python 3.8 compatibility for older Windows releases.
- Added on-demand SASL password prompts, including renewed prompts after reconnecting.
- Corrected NickServ authentication to use the decrypted password for the active network.
- Made system theme matching the default and polished GTK3 theme integration.
- Applied more settings changes immediately, reducing the need to restart ZoiteChat.
- Added descriptive tooltips to the color picker choices and refined default text-event colors.
- Stabilized channel tree font handling across palette and theme updates.
- Corrected the chat scrollbar increment used by its arrow buttons.
- Improved Windows URL launching through the native shell.
- Restored expected minimize behavior when clicking ZoiteChat on the Windows taskbar.
2.18.3 (2026-06-29)
-------------------

View File

@@ -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]

View File

@@ -29,6 +29,22 @@
<id>zoitechat.desktop</id>
</provides>
<releases>
<release date="2026-07-28" version="2.19.0">
<description>
<ul>
<li>Added Python 3.8 compatibility for older Windows releases.</li>
<li>Added on-demand SASL password prompts, including renewed prompts after reconnecting.</li>
<li>Corrected NickServ authentication to use the decrypted password for the active network.</li>
<li>Made system theme matching the default and polished GTK3 theme integration.</li>
<li>Applied more settings changes immediately, reducing the need to restart ZoiteChat.</li>
<li>Added descriptive tooltips to the color picker choices and refined default text-event colors.</li>
<li>Stabilized channel tree font handling across palette and theme updates.</li>
<li>Corrected the chat scrollbar increment used by its arrow buttons.</li>
<li>Improved Windows URL launching through the native shell.</li>
<li>Restored expected minimize behavior when clicking ZoiteChat on the Windows taskbar.</li>
</ul>
</description>
</release>
<release date="2026-06-29" version="2.18.3">
<description>
<ul>

View File

@@ -1,6 +1,6 @@
project('zoitechat', 'c',
version: '2.18.3',
meson_version: '>= 0.55.0',
version: run_command('cat', 'VERSION', check: true).stdout().strip(),
meson_version: '>= 0.59.0',
default_options: [
'c_std=c17',
'buildtype=debugoptimized',

View File

@@ -1,5 +1,5 @@
Name: zoitechat
Version: 2.18.3
Version: %(cat VERSION)
Release: %autorelease
Summary: HexChat-based IRC client
License: GPL-2.0-or-later WITH cryptsetup-OpenSSL-exception

View File

@@ -13,8 +13,16 @@ else
python_dep = dependency(python_opt, version: '>= 2.7')
endif
python_py_conf = configuration_data()
python_py_conf.set('VERSION', meson.project_version())
python_py = configure_file(
input: 'python.py',
output: 'python.py',
configuration: python_py_conf
)
python3_source = custom_target('python-bindings',
input: ['../../src/common/zoitechat-plugin.h', 'python.py'],
input: ['../../src/common/zoitechat-plugin.h', python_py],
output: 'python.c',
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
)

View File

@@ -19,7 +19,7 @@ else:
if not hasattr(sys, 'argv'):
sys.argv = ['<zoitechat>']
VERSION = b'2.18.3'
VERSION = b'@VERSION@'
PLUGIN_NAME = ffi.new('char[]', b'Python')
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
PLUGIN_VERSION = ffi.new('char[]', VERSION)

View File

@@ -126,7 +126,21 @@ ctcp_handle (session *sess, char *to, char *nick, char *ip,
if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset))
goto generic;
inbound_action (sess, to, nick, ip, msg + 7, FALSE, tags_data->identified, tags_data);
{
gboolean private_fromme = !serv->p_cmp (nick, serv->nick) && !is_channel (serv, to);
if (private_fromme)
{
session *target = find_dialog (serv, to);
if (target)
sess = target;
else if (serv->front_session)
sess = serv->front_session;
}
inbound_action (sess, to, nick, ip, msg + 7, private_fromme, tags_data->identified, tags_data);
}
return;
}

View File

@@ -13,9 +13,9 @@ dbus_includes = [
dbus_service_dir = join_paths(get_option('datadir'), 'dbus-1/services')
if get_option('dbus-service-use-appid')
dbus_service_name = 'io.github.Hexchat'
dbus_output_file = 'io.github.Hexchat.service'
dbus_cargs = '-DDBUS_SERVICE="io.github.Hexchat"'
dbus_service_name = 'net.zoite.Zoitechat'
dbus_output_file = 'net.zoite.Zoitechat.service'
dbus_cargs = '-DDBUS_SERVICE="net.zoite.Zoitechat"'
else
dbus_service_name = 'org.zoitechat.service'
dbus_output_file = 'org.zoitechat.service.service'

View File

@@ -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)

View File

@@ -675,7 +675,8 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from,
if (fromme)
{
if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp)
if (prefs.hex_away_auto_unmark && serv->is_away &&
(!tags_data->timestamp || serv->have_echo_message))
sess->server->p_set_back (sess->server);
reply_context_print (sess, tags_data);
EMIT_SIGNAL_TIMESTAMP (XP_TE_UCHANMSG, sess, from, text, nickchar, NULL,

View File

@@ -3025,7 +3025,7 @@ cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[])
newsess->server->nick, msg + offset, TRUE, FALSE,
&no_tags);
}
else
else if (!sess->server->have_echo_message)
{
/* mask out passwords */
if (g_ascii_strcasecmp (nick, "nickserv") == 0)
@@ -5058,6 +5058,11 @@ handle_user_input (session *sess, char *text, int history, int nocommand)
handle_say (sess, text, TRUE);
return 1;
}
if (text[0] == cmd_char && text[1] == cmd_char)
{
handle_say (sess, text + 1, TRUE);
return 1;
}
for (i = 1; text[i]; i++)
{

View File

@@ -1368,7 +1368,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
{
if (ignore_check (word[1], IG_PRIV))
return;
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick))
if (!serv->p_cmp (nick, serv->nick))
{
session *target_sess = find_dialog (serv, to);

View File

@@ -463,8 +463,7 @@ doover:
while (list)
{
sess = list->data;
if (!sess->server->have_awaynotify)
sess->done_away_check = FALSE;
sess->done_away_check = FALSE;
list = list->next;
}
loop++;

View File

@@ -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);

View File

@@ -307,6 +307,109 @@ chanview_set_impl (chanview *cv, int type)
cv->func_focus (cv->focused);
}
void
chanview_set_sorted (chanview *cv, gboolean sorted)
{
cv->sorted = sorted ? 1 : 0;
}
void
chanview_set_use_icons (chanview *cv, gboolean use_icons)
{
cv->use_icons = use_icons ? 1 : 0;
}
void
chanview_set_trunc_len (chanview *cv, int trunc_len)
{
cv->trunc_len = trunc_len;
}
typedef struct
{
chan *ch;
int old_pos;
} chanview_sort_entry;
/* re-sort the session rows below one parent with cb_compare, leaving
* utility rows (tag != 0) in place; mirrors chanview_insert_sorted() */
static void
chanview_resort_children (chanview *cv, GtkTreeIter *parent)
{
GtkTreeModel *model = GTK_TREE_MODEL (cv->store);
GtkTreeIter iter;
chanview_sort_entry *entries;
int *positions;
gint *new_order;
chan *ch;
int n, i, j, count;
n = gtk_tree_model_iter_n_children (model, parent);
if (n < 2)
return;
entries = g_new0 (chanview_sort_entry, n);
positions = g_new0 (int, n);
new_order = g_new0 (gint, n);
count = 0;
for (i = 0; i < n; i++)
{
new_order[i] = i;
if (!gtk_tree_model_iter_nth_child (model, &iter, parent, i))
continue;
gtk_tree_model_get (model, &iter, COL_CHAN, &ch, -1);
if (ch && ch->tag == 0 && ch->userdata)
{
entries[count].ch = ch;
entries[count].old_pos = i;
positions[count] = i;
count++;
}
}
/* stable insertion sort; tab groups stay small so this is cheap */
for (i = 1; i < count; i++)
{
chanview_sort_entry key = entries[i];
j = i - 1;
while (j >= 0 && cv->cb_compare (entries[j].ch->userdata, key.ch->userdata) > 0)
{
entries[j + 1] = entries[j];
j--;
}
entries[j + 1] = key;
}
for (i = 0; i < count; i++)
new_order[positions[i]] = entries[i].old_pos;
gtk_tree_store_reorder (cv->store, parent, new_order);
g_free (entries);
g_free (positions);
g_free (new_order);
}
/* top-level rows keep their order (insertion never sorts them either);
* only the children within each family are sorted */
void
chanview_resort (chanview *cv)
{
GtkTreeIter iter;
if (!cv->cb_compare)
return;
if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (cv->store), &iter))
{
do
chanview_resort_children (cv, &iter);
while (gtk_tree_model_iter_next (GTK_TREE_MODEL (cv->store), &iter));
}
}
static void
chanview_free_ch (chanview *cv, GtkTreeIter *iter)
{

View File

@@ -34,6 +34,10 @@ void chanview_set_callbacks (chanview *cv,
gboolean (*cb_contextmenu) (chanview *, chan *, int tag, void *userdata, GdkEventButton *),
int (*cb_compare) (void *a, void *b));
void chanview_set_impl (chanview *cv, int type);
void chanview_set_sorted (chanview *cv, gboolean sorted);
void chanview_set_use_icons (chanview *cv, gboolean use_icons);
void chanview_set_trunc_len (chanview *cv, int trunc_len);
void chanview_resort (chanview *cv);
chan *chanview_add (chanview *cv, char *name, void *family, void *userdata, gboolean allow_closure, int tag, GdkPixbuf *icon);
int chanview_get_size (chanview *cv);
GtkWidget *chanview_get_box (chanview *cv);

View File

@@ -569,6 +569,18 @@ mg_create_tab_colors (void)
away_list = mg_attr_list_create (&gui_palette[THEME_TOKEN_TAB_AWAY], FALSE);
}
static PangoAttrList *
mg_attr_list_for_tab_state (int state)
{
if (state & TAB_STATE_NEW_HILIGHT)
return nickseen_list;
if (state & TAB_STATE_NEW_MSG)
return newmsg_list;
if (state & TAB_STATE_NEW_DATA)
return newdata_list;
return plain_list;
}
static void
set_window_urgency (GtkWidget *win, gboolean set)
{
@@ -714,7 +726,10 @@ mg_set_access_icon (session_gui *gui, GdkPixbuf *pix, gboolean away)
{
if (gui->op_xpm)
{
if (pix == gtk_image_get_pixbuf (GTK_IMAGE (gui->op_xpm))) /* no change? */
/* no change? (the pref check makes sure a live settings
change can still remove the icon) */
if (pix == gtk_image_get_pixbuf (GTK_IMAGE (gui->op_xpm)) &&
prefs.hex_gui_input_icon)
{
mg_set_myself_away (gui, away);
return;
@@ -3596,6 +3611,48 @@ mg_create_topicbar (session *sess, GtkWidget *box)
mg_create_chanmodebuttons (gui, bbox);
}
/* re-apply the multi-line topic and inline mode button preferences to an
* already built topic bar (see mg_create_topicbar () for the layout) */
static void
mg_update_topicbar_layout (session_gui *gui)
{
GtkWidget *topic = gui->topic_entry;
GtkWidget *vbox = gui->topic_bar;
GtkWidget *topic_scroll;
GtkWidget *hbox;
GtkWidget *mode_hbox;
GtkWidget *desired_parent;
GtkWidget *current_parent;
gboolean wrap;
if (!topic || !GTK_IS_TEXT_VIEW (topic) || !vbox || !gui->topicbutton_box)
return;
wrap = prefs.hex_gui_topicbar_multiline && !prefs.hex_gui_mode_buttons_inline;
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (topic),
wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE);
topic_scroll = gtk_widget_get_parent (topic);
hbox = topic_scroll ? gtk_widget_get_parent (topic_scroll) : NULL;
mode_hbox = gtk_widget_get_parent (gui->topicbutton_box);
if (mode_hbox && hbox)
{
desired_parent = prefs.hex_gui_mode_buttons_inline ? hbox : vbox;
current_parent = gtk_widget_get_parent (mode_hbox);
if (current_parent && current_parent != desired_parent)
{
g_object_ref (mode_hbox);
gtk_container_remove (GTK_CONTAINER (current_parent), mode_hbox);
gtk_box_pack_start (GTK_BOX (desired_parent), mode_hbox, 0, 0, 0);
g_object_unref (mode_hbox);
}
}
mg_topicbar_update_height (topic);
}
/* check if a word is clickable */
static int
@@ -4004,8 +4061,12 @@ mg_create_userlist (session_gui *gui, GtkWidget *box)
gtk_widget_set_margin_end (gui->namelistinfo, 0);
gtk_widget_set_hexpand (gui->namelistinfo, TRUE);
gtk_widget_set_halign (gui->namelistinfo, GTK_ALIGN_FILL);
/* always pack it so the preference can be toggled without a
restart; visibility is controlled explicitly */
gtk_widget_set_no_show_all (gui->namelistinfo, TRUE);
gtk_box_pack_start (GTK_BOX (vbox), gui->namelistinfo, 0, 0, 0);
if (prefs.hex_gui_ulist_count)
gtk_box_pack_start (GTK_BOX (vbox), gui->namelistinfo, 0, 0, 0);
gtk_widget_show (gui->namelistinfo);
gui->user_tree = ulist = userlist_create (vbox);
@@ -5233,6 +5294,10 @@ mg_apply_setup (void)
sess = list->data;
gtk_xtext_set_time_stamp (sess->res->buffer, prefs.hex_stamp_text);
((xtext_buffer *)sess->res->buffer)->needs_recalc = TRUE;
/* the attribute lists were just recreated, so re-apply the
color matching each tab's activity state */
if (sess->res->tab)
chan_set_color (sess->res->tab, mg_attr_list_for_tab_state (sess->tab_state));
if (!sess->gui->is_tab || !done_main)
mg_place_userlist_and_chanview (sess->gui);
if (sess->gui->is_tab)
@@ -5275,6 +5340,49 @@ fe_buttons_update (session *sess)
gtk_widget_hide (sess->gui->button_box);
}
/* tab label for a session that has no joined channel: the channel we are
* waiting to join in parentheses, or "<none>". tbuf must hold CHANLEN+6. */
static void
mg_tab_pending_name (session *sess, char *tbuf)
{
if (sess->waitchannel[0])
{
if (prefs.hex_gui_tab_trunc > 2 && g_utf8_strlen (sess->waitchannel, -1) > prefs.hex_gui_tab_trunc)
{
/* truncate long channel names */
tbuf[0] = '(';
strcpy (tbuf + 1, sess->waitchannel);
g_utf8_offset_to_pointer(tbuf, prefs.hex_gui_tab_trunc)[0] = 0;
strcat (tbuf, "..)");
} else
{
sprintf (tbuf, "(%s)", sess->waitchannel);
}
}
else
strcpy (tbuf, _("<none>"));
}
/* re-apply the tab label truncation preference to a session's tab */
static void
mg_retrunc_tab (session *sess)
{
char tbuf[CHANLEN+6];
if (!sess->res->tab)
return;
if (sess->channel[0])
chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc);
else
{
mg_tab_pending_name (sess, tbuf);
chan_rename (sess->res->tab, tbuf, prefs.hex_gui_tab_trunc);
}
}
void
fe_clear_channel (session *sess)
{
@@ -5283,22 +5391,7 @@ fe_clear_channel (session *sess)
if (sess->gui->is_tab)
{
if (sess->waitchannel[0])
{
if (prefs.hex_gui_tab_trunc > 2 && g_utf8_strlen (sess->waitchannel, -1) > prefs.hex_gui_tab_trunc)
{
/* truncate long channel names */
tbuf[0] = '(';
strcpy (tbuf + 1, sess->waitchannel);
g_utf8_offset_to_pointer(tbuf, prefs.hex_gui_tab_trunc)[0] = 0;
strcat (tbuf, "..)");
} else
{
sprintf (tbuf, "(%s)", sess->waitchannel);
}
}
else
strcpy (tbuf, _("<none>"));
mg_tab_pending_name (sess, tbuf);
chan_rename (sess->res->tab, tbuf, prefs.hex_gui_tab_trunc);
}
@@ -5422,6 +5515,110 @@ fe_set_channel (session *sess)
chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc);
}
static void
mg_apply_live_prefs_to_gui (session_gui *gui, session *sess, const mg_live_prefs *changes)
{
if (changes->meters)
mg_update_meters (gui);
if (changes->ulist_columns && gui->user_tree)
userlist_rebuild_columns (gui->user_tree);
if (changes->ulist_count && gui->namelistinfo)
{
if (prefs.hex_gui_ulist_count)
gtk_widget_show (gui->namelistinfo);
else
gtk_widget_hide (gui->namelistinfo);
}
if (changes->input_box && gui->nick_box)
{
if (prefs.hex_gui_input_nick)
gtk_widget_show (gui->nick_box);
else
gtk_widget_hide (gui->nick_box);
}
/* refresh the access icon; re-filling the user list rows can
clear it as a side effect, so restore it here too */
if ((changes->input_box || changes->ulist_rows) && sess && is_session (sess))
{
if (sess->type == SESS_DIALOG)
mg_set_access_icon (gui, NULL, sess->server->is_away);
else
mg_set_access_icon (gui, get_user_icon (sess->server, sess->me), sess->server->is_away);
}
if (changes->topic_bar)
mg_update_topicbar_layout (gui);
if (changes->transparency && gui->window)
gtk_widget_set_opacity (gui->window, (prefs.hex_gui_transparency / 255.));
}
/* apply preference changes that used to require a restart. called by
* setup.c after the new prefs have been committed. */
void
mg_apply_live_prefs (const mg_live_prefs *changes)
{
GSList *list;
session *sess;
int done_main = FALSE;
if (!changes)
return;
/* the tab bar / channel tree lives in the shared tab window */
if (mg_gui && mg_gui->chanview)
{
if (changes->tab_resort)
{
chanview_set_sorted (mg_gui->chanview, prefs.hex_gui_tab_sort);
chanview_resort (mg_gui->chanview);
}
if (changes->tab_trunc)
chanview_set_trunc_len (mg_gui->chanview, prefs.hex_gui_tab_trunc);
if (changes->chanview || changes->tab_resort)
{
chanview_set_use_icons (mg_gui->chanview,
prefs.hex_gui_tab_icons && (pix_tree_channel || pix_tree_dialog ||
pix_tree_server || pix_tree_util));
/* rebuild the tab bar / tree widgets from the model */
chanview_set_impl (mg_gui->chanview, prefs.hex_gui_tab_layout);
}
}
list = sess_list;
while (list)
{
sess = list->data;
if (changes->tab_trunc)
mg_retrunc_tab (sess);
if (changes->ulist_sort)
userlist_apply_sort (sess);
if (changes->ulist_rows)
userlist_refill (sess);
if (sess->gui->is_tab)
{
/* only apply to the shared tab window once, using the
front-most session for session-dependent bits */
if (!done_main)
{
done_main = TRUE;
mg_apply_live_prefs_to_gui (sess->gui, current_tab, changes);
}
}
else
mg_apply_live_prefs_to_gui (sess->gui, sess, changes);
list = list->next;
}
}
void
fe_set_typing (session *sess, const char *nick, const char *state)
{

View File

@@ -37,6 +37,24 @@ GtkWidget *mg_create_generic_tab (char *name, char *title, int force_toplevel, i
void mg_set_title (GtkWidget *button, char *title);
void mg_set_access_icon (session_gui *gui, GdkPixbuf *pix, gboolean away);
void mg_apply_setup (void);
/* which parts of the GUI need a live rebuild after a preferences change */
typedef struct
{
unsigned int chanview:1; /* rebuild the tab bar / channel tree */
unsigned int tab_resort:1; /* re-sort channel tabs */
unsigned int tab_trunc:1; /* re-truncate tab labels */
unsigned int ulist_columns:1; /* rebuild user list columns */
unsigned int ulist_rows:1; /* re-fill user list rows */
unsigned int ulist_sort:1; /* re-apply user list sort order */
unsigned int ulist_count:1; /* user count label visibility */
unsigned int meters:1; /* rebuild lag/throttle meters */
unsigned int input_box:1; /* nick box visibility and access icon */
unsigned int topic_bar:1; /* topic wrap mode and mode button placement */
unsigned int transparency:1; /* main window opacity */
} mg_live_prefs;
void mg_apply_live_prefs (const mg_live_prefs *changes);
void mg_apply_session_font_prefs (session_gui *gui);
void mg_close_sess (session *);
void mg_tab_close (session *sess);

View File

@@ -1045,28 +1045,30 @@ setup_create_spin (GtkWidget *table, int row, const setting *set)
return wid;
}
static guint setup_trans_apply_tag = 0;
/* the window opacity the dialog opened with, so cancelling can undo the
* live preview done by the slider */
static int setup_transparency_snapshot;
static gint
setup_apply_trans (int *tag)
setup_apply_trans (gpointer userdata)
{
prefs.hex_gui_transparency = setup_prefs.hex_gui_transparency;
gtk_widget_set_opacity (current_sess->gui->window,
(prefs.hex_gui_transparency / 255.));
if (current_sess)
gtk_widget_set_opacity (current_sess->gui->window,
(prefs.hex_gui_transparency / 255.));
*tag = 0;
setup_trans_apply_tag = 0;
return 0;
}
static void
setup_hscale_cb (GtkRange *wid, const setting *set)
{
static int tag = 0;
setup_set_int (&setup_prefs, set, (int) gtk_range_get_value (wid));
if (tag == 0)
{
tag = g_idle_add ((GSourceFunc) setup_apply_trans, &tag);
}
if (setup_trans_apply_tag == 0)
setup_trans_apply_tag = g_idle_add ((GSourceFunc) setup_apply_trans, NULL);
}
static void
@@ -1091,7 +1093,8 @@ setup_create_hscale (GtkWidget *table, int row, const setting *set)
#ifndef WIN32 /* Windows always supports this */
/* Only used for transparency currently */
if (!gdk_screen_is_composited (gtk_widget_get_screen (current_sess->gui->window)))
if (current_sess &&
!gdk_screen_is_composited (gtk_widget_get_screen (current_sess->gui->window)))
gtk_widget_set_sensitive (wid, FALSE);
#endif
}
@@ -2293,7 +2296,9 @@ setup_apply (struct zoitechatprefs *pr)
ThemeChangedEvent event;
struct zoitechatprefs old_prefs = prefs;
int old_dark_mode = prefs.hex_gui_dark_mode;
mg_live_prefs live_changes;
memset (&live_changes, 0, sizeof (live_changes));
#define DIFF(a) (pr->a != prefs.a)
@@ -2301,40 +2306,49 @@ setup_apply (struct zoitechatprefs *pr)
if (DIFF (hex_gui_lang))
noapply = TRUE;
#endif
if (DIFF (hex_gui_compact))
noapply = TRUE;
if (DIFF (hex_gui_input_icon))
noapply = TRUE;
if (DIFF (hex_gui_input_nick))
noapply = TRUE;
if (DIFF (hex_gui_lagometer))
noapply = TRUE;
if (DIFF (hex_gui_mode_buttons_inline))
noapply = TRUE;
if (DIFF (hex_gui_tab_icons))
noapply = TRUE;
if (DIFF (hex_gui_tab_closebuttons))
noapply = TRUE;
/* server tabs are only created when a connection is opened, so
existing connections keep their current tabs until a restart;
the channel tree indentation is still re-applied below */
if (DIFF (hex_gui_tab_server))
{
noapply = TRUE;
if (DIFF (hex_gui_tab_small))
noapply = TRUE;
live_changes.chanview = TRUE;
}
if (DIFF (hex_gui_compact))
{
live_changes.chanview = TRUE;
live_changes.ulist_columns = TRUE;
}
if (DIFF (hex_gui_input_icon) || DIFF (hex_gui_input_nick))
live_changes.input_box = TRUE;
if (DIFF (hex_gui_lagometer) || DIFF (hex_gui_throttlemeter))
live_changes.meters = TRUE;
if (DIFF (hex_gui_mode_buttons_inline) || DIFF (hex_gui_topicbar_multiline))
live_changes.topic_bar = TRUE;
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
DIFF (hex_gui_tab_small))
live_changes.chanview = TRUE;
if (DIFF (hex_gui_tab_sort))
noapply = TRUE;
live_changes.tab_resort = TRUE;
if (DIFF (hex_gui_tab_trunc))
noapply = TRUE;
if (DIFF (hex_gui_throttlemeter))
noapply = TRUE;
if (DIFF (hex_gui_topicbar_multiline))
noapply = TRUE;
live_changes.tab_trunc = TRUE;
if (DIFF (hex_gui_ulist_count))
noapply = TRUE;
live_changes.ulist_count = TRUE;
if (DIFF (hex_gui_ulist_icons))
noapply = TRUE;
live_changes.ulist_rows = TRUE;
if (DIFF (hex_gui_ulist_show_hosts))
noapply = TRUE;
{
live_changes.ulist_columns = TRUE;
live_changes.ulist_rows = TRUE; /* hostnames may be stale */
}
if (DIFF (hex_gui_ulist_sort))
noapply = TRUE;
live_changes.ulist_sort = TRUE;
/* compare against the pre-dialog value: the slider previews the
opacity live on the current window only, so OK still needs to
apply it to the other windows */
if (pr->hex_gui_transparency != setup_transparency_snapshot)
live_changes.transparency = TRUE;
if ((pr->hex_gui_tab_pos == 5 || pr->hex_gui_tab_pos == 6) &&
pr->hex_gui_tab_layout == 2 && pr->hex_gui_tab_pos != prefs.hex_gui_tab_pos)
@@ -2378,6 +2392,8 @@ setup_apply (struct zoitechatprefs *pr)
theme_manager_dispatch_setup_apply (&event);
mg_apply_live_prefs (&live_changes);
if (noapply)
fe_message (_("Some settings were changed that require a"
" restart to take full effect."), FE_MSG_WARN);
@@ -2410,6 +2426,9 @@ setup_ok_cb (GtkWidget *but, GtkWidget *win)
if (save_result.success)
{
theme_preferences_stage_commit ();
/* the previewed values were accepted, so the close handler
must not revert them */
setup_transparency_snapshot = prefs.hex_gui_transparency;
gtk_widget_destroy (win);
return;
}
@@ -2472,6 +2491,24 @@ setup_close_cb (GtkWidget *win, GtkWidget **swin)
{
*swin = NULL;
/* drop any pending transparency preview and undo the previewed
opacity when the dialog is cancelled; after OK the snapshot
was updated so this is a no-op */
if (setup_trans_apply_tag)
{
g_source_remove (setup_trans_apply_tag);
setup_trans_apply_tag = 0;
}
if (prefs.hex_gui_transparency != setup_transparency_snapshot)
{
mg_live_prefs revert_changes;
prefs.hex_gui_transparency = setup_transparency_snapshot;
memset (&revert_changes, 0, sizeof (revert_changes));
revert_changes.transparency = TRUE;
mg_apply_live_prefs (&revert_changes);
}
theme_preferences_stage_discard ();
if (font_dialog)
@@ -2493,6 +2530,7 @@ setup_open (void)
memcpy (&setup_prefs, &prefs, sizeof (prefs));
color_change = FALSE;
setup_transparency_snapshot = prefs.hex_gui_transparency;
theme_preferences_stage_begin ();
setup_window = setup_window_open ();

View File

@@ -106,6 +106,11 @@ typedef struct
gboolean staged_dirty[THEME_TOKEN_COUNT];
GdkRGBA snapshot[THEME_TOKEN_COUNT];
GdkRGBA staged[THEME_TOKEN_COUNT];
/* the GTK3 theme selection is previewed live from the combo, so
* remember what was active when the dialog opened in order to
* restore it when the dialog is cancelled */
char gtk3_theme_snapshot[sizeof ((struct zoitechatprefs *) 0)->hex_gui_gtk3_theme];
int gtk3_variant_snapshot;
} theme_preferences_stage_state;
static theme_preferences_stage_state theme_preferences_stage;
@@ -204,6 +209,11 @@ theme_preferences_stage_begin (void)
memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage));
theme_preferences_stage.active = TRUE;
g_strlcpy (theme_preferences_stage.gtk3_theme_snapshot,
prefs.hex_gui_gtk3_theme,
sizeof (theme_preferences_stage.gtk3_theme_snapshot));
theme_preferences_stage.gtk3_variant_snapshot = prefs.hex_gui_gtk3_variant;
for (token = THEME_TOKEN_MIRC_0; token < THEME_TOKEN_COUNT; token++)
{
GdkRGBA rgba;
@@ -237,12 +247,36 @@ theme_preferences_stage_commit (void)
memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage));
}
/* undo a live GTK3 theme preview: put the selection that was active when
* the dialog opened back and re-apply it */
static void
theme_preferences_stage_restore_gtk3 (void)
{
if (g_strcmp0 (prefs.hex_gui_gtk3_theme, theme_preferences_stage.gtk3_theme_snapshot) == 0 &&
prefs.hex_gui_gtk3_variant == theme_preferences_stage.gtk3_variant_snapshot)
return;
g_strlcpy (prefs.hex_gui_gtk3_theme,
theme_preferences_stage.gtk3_theme_snapshot,
sizeof (prefs.hex_gui_gtk3_theme));
prefs.hex_gui_gtk3_variant = theme_preferences_stage.gtk3_variant_snapshot;
theme_gtk3_apply_current (NULL);
theme_manager_dispatch_changed (THEME_CHANGED_REASON_THEME_PACK |
THEME_CHANGED_REASON_PALETTE |
THEME_CHANGED_REASON_WIDGET_STYLE |
THEME_CHANGED_REASON_USERLIST |
THEME_CHANGED_REASON_MODE);
}
void
theme_preferences_stage_discard (void)
{
if (!theme_preferences_stage.active)
return;
/* restore the GTK3 theme first so the color snapshot below wins */
theme_preferences_stage_restore_gtk3 ();
theme_preferences_stage_sync_runtime_to_snapshot ();
memset (&theme_preferences_stage, 0, sizeof (theme_preferences_stage));
}

View File

@@ -796,16 +796,12 @@ userlist_store_color (GtkListStore *store, GtkTreeIter *iter, ThemeSemanticToken
}
}
GtkListStore *
userlist_create_model (session *sess)
static void
userlist_model_set_sort (GtkListStore *store, session *sess)
{
GtkListStore *store;
GtkTreeIterCompareFunc cmp_func;
GtkSortType sort_type;
store = gtk_list_store_new (6, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_POINTER, THEME_GTK_COLOR_TYPE);
switch (prefs.hex_gui_ulist_sort)
{
case 0:
@@ -825,18 +821,77 @@ userlist_create_model (session *sess)
sort_type = GTK_SORT_DESCENDING;
break;
default:
/* No sorting */
/* No sorting; rows keep their current order and new rows
go wherever they are inserted */
gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(store),
GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING);
gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE(store), NULL, NULL, NULL);
return store;
return;
}
gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE(store), cmp_func, sess, NULL);
gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(store),
GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, sort_type);
}
GtkListStore *
userlist_create_model (session *sess)
{
GtkListStore *store;
store = gtk_list_store_new (6, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_POINTER, THEME_GTK_COLOR_TYPE);
userlist_model_set_sort (store, sess);
return store;
}
void
userlist_apply_sort (session *sess)
{
if (!sess->res || !sess->res->user_model)
return;
userlist_model_set_sort (sess->res->user_model, sess);
}
/* remove and re-insert every row, so per-row content that depends on
* preferences (mode icons vs. text prefixes, hostnames) is rebuilt */
void
userlist_refill (session *sess)
{
GtkTreeModel *model;
GtkTreeIter iter;
GSList *users = NULL;
GSList *it;
struct User *user;
if (!sess->res || !sess->res->user_model)
return;
model = GTK_TREE_MODEL (sess->res->user_model);
if (gtk_tree_model_get_iter_first (model, &iter))
{
do
{
gtk_tree_model_get (model, &iter, COL_USER, &user, -1);
if (user)
users = g_slist_prepend (users, user);
}
while (gtk_tree_model_iter_next (model, &iter));
}
fe_userlist_clear (sess);
/* fe_userlist_insert() prepends, so inserting in reverse display
order restores the on-screen order for unsorted lists; sorted
models position the rows themselves */
for (it = users; it; it = it->next)
fe_userlist_insert (sess, it->data, FALSE);
g_slist_free (users);
}
static void
userlist_add_columns (GtkTreeView * treeview)
{
@@ -906,6 +961,23 @@ userlist_add_columns (GtkTreeView * treeview)
}
}
void
userlist_rebuild_columns (GtkWidget *treeview)
{
GList *columns;
GList *it;
if (!treeview)
return;
columns = gtk_tree_view_get_columns (GTK_TREE_VIEW (treeview));
for (it = columns; it; it = it->next)
gtk_tree_view_remove_column (GTK_TREE_VIEW (treeview), it->data);
g_list_free (columns);
userlist_add_columns (GTK_TREE_VIEW (treeview));
}
static gint
userlist_click_cb (GtkWidget *widget, GdkEventButton *event, gpointer userdata)
{

View File

@@ -24,6 +24,9 @@ void userlist_set_value (GtkWidget *treeview, gfloat val);
gfloat userlist_get_value (GtkWidget *treeview);
GtkWidget *userlist_create (GtkWidget *box);
GtkListStore *userlist_create_model (session *sess);
void userlist_apply_sort (session *sess);
void userlist_refill (session *sess);
void userlist_rebuild_columns (GtkWidget *treeview);
void userlist_show (session *sess);
void userlist_select (session *sess, char *name);
void fe_userlist_set_typing (session *sess, const char *nick, const char *state);

View File

@@ -373,10 +373,10 @@ begin
idpClearFiles;
if WizardIsComponentSelected('themes\windows10') then
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip'));
idpAddFile('https://dl.zoitechat.org/themes/GTK3Themes/Windows-10-3.2.1.zip', ExpandConstant('{tmp}\Windows-10-3.2.1.zip'));
if WizardIsComponentSelected('themes\windows10dark') then
idpAddFile('https://dl.zoitechat.zoite.net/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip'));
idpAddFile('https://dl.zoitechat.org/themes/GTK3Themes/Windows-10-Dark-3.2.1-dark.zip', ExpandConstant('{tmp}\Windows-10-Dark-3.2.1-dark.zip'));
if not WizardIsTaskSelected('portable') then
begin

View File

@@ -1,6 +1,6 @@
param ([string] $templateFilename, [string] $outputFilename)
$versionParts = Select-String -Path "${env:SOLUTIONDIR}meson.build" -Pattern " version: '([^']+)',$" | Select-Object -First 1 | %{ $_.Matches[0].Groups[1].Value.Split('.') }
$versionParts = (Get-Content "${env:SOLUTIONDIR}VERSION" -Raw).Trim().Split('.')
[string[]] $contents = Get-Content $templateFilename -Encoding UTF8 | %{
while ($_ -match '^(.*?)<#=(.*?)#>(.*?)$') {

View File

@@ -1 +0,0 @@
2.18.3