3 Commits

Author SHA1 Message Date
8c1b920b27 support inline images in chat. 2026-07-20 14:33:20 -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
23 changed files with 2919 additions and 84 deletions

View File

@@ -28,6 +28,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

View File

@@ -564,6 +564,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},
@@ -776,6 +777,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;

View File

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

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

@@ -49,6 +49,7 @@
#include "chanlist.h"
#include "joind.h"
#include "xtext.h"
#include "inline-image.h"
#include "theme/theme-gtk.h"
#include "menu.h"
#include "notifygui.h"
@@ -943,7 +944,13 @@ void
fe_print_text (struct session *sess, char *text, time_t stamp,
gboolean no_activity)
{
PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp);
/* image links become a "[view image]" placeholder with a hidden URL */
char *filtered = inline_image_filter_text (text);
PrintTextRaw (sess->res->buffer,
(unsigned char *)(filtered ? filtered : text),
prefs.hex_text_indent, stamp);
g_free (filtered);
if (no_activity || !sess->gui->is_tab)
return;

View File

@@ -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" />
@@ -102,6 +103,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" />

View File

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

1667
src/fe-gtk/inline-image.c Normal file

File diff suppressed because it is too large Load Diff

45
src/fe-gtk/inline-image.h Normal file
View File

@@ -0,0 +1,45 @@
/* ZoiteChat
* Copyright (C) 2026 ZoiteChat contributors.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#ifndef ZOITECHAT_INLINE_IMAGE_H
#define ZOITECHAT_INLINE_IMAGE_H
#include "xtext.h"
/* does this look like a direct http(s) link to an image file? */
gboolean inline_image_is_image_url (const char *url);
/* show the image behind 'url' inline below 'ent', or hide it again if it is
* already shown. Errors are reported to 'sess'. */
void inline_image_toggle (session *sess, GtkXText *xtext, textentry *ent,
const char *url);
/* replace image links in a message with a clickable placeholder label that
* hides the URL. Returns a newly allocated string, or NULL if the text has
* no image links (or the feature is off). */
char *inline_image_filter_text (const char *text);
/* pop up a window with a larger version of the entry's inline image */
void inline_image_show_viewer (GtkXText *xtext, textentry *ent);
/* can downloads go through the proxy configured for ZoiteChat (or is no
* proxy configured)? FALSE means loading media would have to bypass the
* user's proxy, which is never done. */
gboolean inline_image_proxy_usable (void);
#endif

View File

@@ -59,6 +59,7 @@
#include "pixmaps.h"
#include "plugin-tray.h"
#include "xtext.h"
#include "inline-image.h"
#include "sexy-spell-entry.h"
#include "gtkutil.h"
@@ -569,6 +570,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 +727,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 +3612,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
@@ -3630,7 +3688,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;
}
@@ -3642,6 +3706,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);
}
}
@@ -3670,7 +3748,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;
@@ -4004,8 +4083,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 +5316,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 +5362,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 +5413,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 +5537,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

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

View File

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

View File

@@ -23,6 +23,7 @@ zoitechat_gtk_sources = [
'gtkutil.c',
'icon-resolver.c',
'ignoregui.c',
'inline-image.c',
'joind.c',
'menu.c',
'maingui.c',

View File

@@ -673,6 +673,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}
};
@@ -1045,28 +1050,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 +1098,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 +2301,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 +2311,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 +2397,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 +2431,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 +2496,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 +2535,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

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

View File

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

View File

@@ -78,6 +78,7 @@
<Gtk3Immodules Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\immodules\**\*" />
<Gtk3PrintBackends Include="$(DepsRoot)\lib\gtk-3.0\3.0.0\printbackends\**\*" />
<GSettingsSchemas Include="$(DepsRoot)\share\glib-2.0\schemas\*" />
<GioModules Include="$(DepsRoot)\lib\gio\modules\**\*" />
<GdkPixbufLoaders Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders\**\*" />
<GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" />
@@ -96,6 +97,7 @@
<Copy SourceFiles="@(GdkPixbufLoaders)" DestinationFiles="@(GdkPixbufLoaders->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GdkPixbufLoaderCache)" DestinationFiles="@(GdkPixbufLoaderCache->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GSettingsSchemas)" DestinationFiles="@(GSettingsSchemas->'$(ZoiteChatRel)\share\glib-2.0\schemas\%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GioModules)" DestinationFiles="@(GioModules->'$(ZoiteChatRel)\lib\gio\modules\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" />
<Copy SourceFiles="$(WinSparklePath)\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\WinSparkle" />

View File

@@ -151,6 +151,7 @@ Source: "tiff.dll"; DestDir: "{app}"; Flags: ignoreversion; Components: libs
Source: "plugins\hcnotifications-winrt.dll"; DestDir: "{app}\plugins"; Flags: ignoreversion; Components: libs
Source: "lib\enchant\*"; DestDir: "{app}\lib\enchant"; Flags: ignoreversion; Components: libs
Source: "lib\gio\modules\*"; DestDir: "{app}\lib\gio\modules"; Flags: ignoreversion recursesubdirs; Components: libs
Source: "girepository-2.0-0.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua
Source: "lua51.dll"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist; Components: langs\lua