$ git clone http://tcclient.ion.nu/tc_client.git
commit 8d80721e21a97197d43baed05adc107906ee7e9b
Author: Alicia <...>
Date: Tue Sep 27 23:28:21 2016 +0200
tc_client-gtk: turn URLs mentioned in chat into clickable links.
diff --git a/ChangeLog b/ChangeLog
index 95fc458..ce0fbe7 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -8,6 +8,7 @@ tc_client-gtk: changed camera input for broadcasting from using a thread to usin
tc_client-gtk: use the camera's own copy of its ID for g_timeout_add() rather than what camera_new() was called with. And in configure, include libavutil/mem.h if we fall back on av_freep()
tc_client-gtk: cleaned up leftover windows compatibility code that is no longer necessary.
tc_client-gtk: changed the greenscreen postprocessor to use libcamera for the background, allowing you to use either another camera or an image through the virtual "Image" camera as background.
+tc_client-gtk: turn URLs mentioned in chat into clickable links.
tc_client-gtk and camviewer: updated to libavcodec's avcodec_{send,receive}_{frame,packet} API.
libcamera(v4l2): cache the frame and if there is no data to be read, return the cache instead of blocking.
0.39:
diff --git a/gtkgui.glade b/gtkgui.glade
index 141560f..a56a5cf 100644
--- a/gtkgui.glade
+++ b/gtkgui.glade
@@ -1535,4 +1535,24 @@
</object>
</child>
</object>
+ <object class="GtkMenu" id="link_menu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="link_menu_open">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Open Link</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="link_menu_copy">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Copy Link</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ </object>
</interface>
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index 90691e7..8e4581f 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -85,7 +85,7 @@ char frombuild=0; // Running from the build directory
PROCESS_INFORMATION coreprocess={.hProcess=0};
#endif
-void printchat(const char* text, const char* pm)
+void printchat(const char* text, const char* color, unsigned int offset, const char* pm)
{
GtkAdjustment* scroll;
GtkTextBuffer* buffer;
@@ -104,32 +104,21 @@ void printchat(const char* text, const char* pm)
GtkTextIter end;
gtk_text_buffer_get_end_iter(buffer, &end);
gtk_text_buffer_insert(buffer, &end, "\n", -1);
- gtk_text_buffer_insert(buffer, &end, text, -1);
- buffer_updatesize(buffer);
- if(bottom){autoscroll_after(scroll);}
-}
-
-void printchat_color(const char* text, const char* color, unsigned int offset, const char* pm)
-{
- GtkAdjustment* scroll;
- GtkTextBuffer* buffer;
- struct user* user;
- if(pm && (user=finduser(pm)))
+ int startnum=gtk_text_iter_get_offset(&end);
+ // Insert links and regular text separately
+ const char* linktext=text;
+ const char* link;
+ while((link=strstr(linktext, "://")))
{
- pm_open(pm, 0, data->scroll);
- scroll=user->pm_scroll;
- buffer=user->pm_buffer;
- }else{
- scroll=data->scroll;
- buffer=data->buffer;
+ while(link>linktext && link[-1]!=' '){link=&link[-1];}
+ unsigned int linklen;
+ for(linklen=0; link[linklen] && link[linklen]!=' '; ++linklen);
+ if(linklen<6){continue;}
+ gtk_text_buffer_insert(buffer, &end, linktext, link-linktext);
+ gui_insert_link(buffer, &end, link, linklen);
+ linktext=&link[linklen];
}
- char bottom=autoscroll_before(scroll);
- // Insert new content
- GtkTextIter end;
- gtk_text_buffer_get_end_iter(buffer, &end);
- gtk_text_buffer_insert(buffer, &end, "\n", -1);
- int startnum=gtk_text_iter_get_offset(&end);
- gtk_text_buffer_insert(buffer, &end, text, -1);
+ gtk_text_buffer_insert(buffer, &end, linktext, -1);
GtkTextIter start;
// Set color if there was one
if(color)
@@ -267,7 +256,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
}
if(!strncmp(buf, "Currently online: ", 18))
{
- printchat(buf, 0);
+ printchat(buf, 0, 0, 0);
char* next=&buf[16];
while(next)
{
@@ -298,7 +287,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
// Start streams once we're properly connected
if(!strncmp(buf, "Currently on cam: ", 18))
{
- printchat(buf, 0);
+ printchat(buf, 0, 0, 0);
if(!config_get_bool("autoopencams") && config_get_set("autoopencams")){return 1;}
char* next=&buf[16];
while(next)
@@ -319,7 +308,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
}
if(buf[0]=='/') // For the /help text
{
- printchat(buf, 0);
+ printchat(buf, 0, 0, 0);
return 1;
}
char* color=0;
@@ -427,7 +416,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
}
if(config_get_bool("enable_logging")){logger_write(buf, channel, pm);}
// Insert new content
- printchat_color(buf, color, 8, pm);
+ printchat(buf, color, 8, pm);
pm_highlight(pm);
free(pm);
if(space[-1]!=':') // Not a message
@@ -471,14 +460,14 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
}
if(!strcmp(buf, "Changed color") || !strncmp(buf, "Current color: ", 15))
{
- printchat_color(buf, color, 0, 0);
+ printchat(buf, color, 0, 0);
free((void*)mycolor);
mycolor=color;
return 1;
}
if(!strncmp(buf, "Color ", 6))
{
- printchat_color(buf, color, 0, 0);
+ printchat(buf, color, 0, 0);
}
free(color);
if(space && !strcmp(space, " is a moderator."))
@@ -502,7 +491,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
// Start a stream when someone cams up
if(space && !strcmp(space, " cammed up"))
{
- printchat(buf, 0);
+ printchat(buf, 0, 0, 0);
if(config_get_bool("autoopencams") || !config_get_set("autoopencams"))
{
space[0]=0;
@@ -561,12 +550,12 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
if(!strncmp(buf, "Room topic: ", 12) ||
(space && (!strcmp(space, " is not logged in") || !strncmp(space, " is logged in as ", 17))))
{
- printchat(buf, 0);
+ printchat(buf, 0, 0, 0);
return 1;
}
if(!strcmp(buf, "Server disconnected"))
{
- printchat(buf, 0);
+ printchat(buf, 0, 0, 0);
if(camout_cam)
{
cam_close(camout_cam);
@@ -773,7 +762,7 @@ void sendmessage(GtkEntry* entry, void* x)
if(!user)
{
gtk_entry_set_text(entry, "");
- printchat("No such user", 0);
+ printchat("No such user", 0, 0, 0);
free(pm);
sendingmsg=0;
return;
@@ -791,7 +780,7 @@ void sendmessage(GtkEntry* entry, void* x)
sprintf(&text[8], "%s: %s", nickname, msg);
}
if(config_get_bool("enable_logging")){logger_write(text, channel, pm);}
- printchat_color(text, mycolor, 8, pm);
+ printchat(text, mycolor, 8, pm);
gtk_entry_set_text(entry, "");
free(pm);
sendingmsg=0;
@@ -989,6 +978,13 @@ int main(int argc, char** argv)
userlistwidget=GTK_WIDGET(gtk_builder_get_object(gui, "userlistbox"));
GtkWidget* chatview=GTK_WIDGET(gtk_builder_get_object(gui, "chatview"));
data->scroll=gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(gtk_builder_get_object(gui, "chatscroll")));
+ // Set up handling of links in chat
+ g_signal_connect(chatview, "button-release-event", G_CALLBACK(gui_click_link), 0);
+ g_signal_connect(chatview, "button-press-event", G_CALLBACK(gui_rightclick_link), 0);
+ g_signal_connect(chatview, "motion-notify-event", G_CALLBACK(gui_hover_link), 0);
+ GdkDisplay* display=gtk_widget_get_display(chatview);
+ gui_cursor_text=gdk_cursor_new_from_name(display, "text");
+ gui_cursor_link=gdk_cursor_new_from_name(display, "pointer");
data->buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(chatview));
buffer_setup_colors(data->buffer);
@@ -1022,9 +1018,9 @@ int main(int argc, char** argv)
// Start window and channel password window signals
GtkWidget* button=GTK_WIDGET(gtk_builder_get_object(gui, "channelpasswordbutton"));
- g_signal_connect(button, "clicked", G_CALLBACK(startsession), (void*)-1); // &data);
+ g_signal_connect(button, "clicked", G_CALLBACK(startsession), (void*)-1);
button=GTK_WIDGET(gtk_builder_get_object(gui, "channelpassword"));
- g_signal_connect(button, "activate", G_CALLBACK(startsession), (void*)-1); // &data);
+ g_signal_connect(button, "activate", G_CALLBACK(startsession), (void*)-1);
GtkWidget* startwindow=GTK_WIDGET(gtk_builder_get_object(gui, "startwindow"));
// Connect signal for quick connect
item=GTK_WIDGET(gtk_builder_get_object(gui, "start_menu_connect"));
@@ -1039,6 +1035,9 @@ int main(int argc, char** argv)
g_signal_connect(item, "switch-page", G_CALLBACK(pm_select), 0);
// Connect signal for captcha
g_signal_connect(gtk_builder_get_object(gui, "captcha_done"), "clicked", G_CALLBACK(captcha_done), 0);
+ // Connect signals for link menus
+ g_signal_connect(gtk_builder_get_object(gui, "link_menu_open"), "activate", G_CALLBACK(gui_link_menu_open), 0);
+ g_signal_connect(gtk_builder_get_object(gui, "link_menu_copy"), "activate", G_CALLBACK(gui_link_menu_copy), 0);
// Connect signals for camera postprocessing
g_signal_connect(gtk_builder_get_object(gui, "cam_menu_colors"), "activate", G_CALLBACK(gui_show_camcolors), 0);
g_signal_connect(gtk_builder_get_object(gui, "camcolors_min_brightness"), "value-changed", G_CALLBACK(camcolors_adjust_min), 0);
diff --git a/utilities/gtk/gui.c b/utilities/gtk/gui.c
index ca4cbe8..d51534a 100644
--- a/utilities/gtk/gui.c
+++ b/utilities/gtk/gui.c
@@ -1,6 +1,6 @@
/*
tc_client-gtk, a graphical user interface for tc_client
- Copyright (C) 2015 alicia@ion.nu
+ Copyright (C) 2015-2016 alicia@ion.nu
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
@@ -31,6 +31,8 @@ GtkBuilder* gui;
GtkWidget* gui_greenscreen_preview_img=0;
unsigned int gui_greenscreen_preview_event=0;
extern gboolean gui_greenscreen_preview(void* x);
+GdkCursor* gui_cursor_text;
+GdkCursor* gui_cursor_link;
char autoscroll_before(GtkAdjustment* scroll)
{
@@ -355,6 +357,9 @@ void pm_open(const char* nick, char select, GtkAdjustment* scroll)
}
char bottom=autoscroll_before(scroll); // If PM tabs (with close buttons) are taller we need to make sure pushing down the chat field doesn't make it stop scrolling
GtkWidget* textview=gtk_text_view_new();
+ g_signal_connect(textview, "button-release-event", G_CALLBACK(gui_click_link), 0);
+ g_signal_connect(textview, "button-press-event", G_CALLBACK(gui_rightclick_link), 0);
+ g_signal_connect(textview, "motion-notify-event", G_CALLBACK(gui_hover_link), 0);
user->pm_tab=gtk_scrolled_window_new(0, 0);
user->pm_buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
user->pm_scroll=gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(user->pm_tab));
@@ -677,3 +682,88 @@ void gui_set_greenscreen_tolerance(GtkAdjustment* adjustment, void* x)
if(!cam){return;}
cam->postproc.greenscreen_tolerance[(intptr_t)x]=gtk_adjustment_get_value(adjustment);
}
+
+void gui_insert_link(GtkTextBuffer* buffer, GtkTextIter* iter, const char* url, int length)
+{
+ int startnum=gtk_text_iter_get_offset(iter);
+ gtk_text_buffer_insert(buffer, iter, url, length);
+ // Make it look like a link and store the URL for later
+ GtkTextTag* tag=gtk_text_buffer_create_tag(buffer, 0, "underline", PANGO_UNDERLINE_SINGLE, (char*)0);
+ g_object_set_data(G_OBJECT(tag), "url", strndup(url, length));
+ GtkTextIter start;
+ gtk_text_buffer_get_iter_at_offset(buffer, &start, startnum);
+ gtk_text_buffer_apply_tag(buffer, tag, &start, iter);
+}
+
+const char* gui_find_link(GtkTextView* textview, int xpos, int ypos)
+{
+ int x;
+ int y;
+ gtk_text_view_window_to_buffer_coords(textview, GTK_TEXT_WINDOW_TEXT, xpos, ypos, &x, &y);
+ GtkTextIter iter;
+ gtk_text_view_get_iter_at_location(textview, &iter, x, y);
+ const char* url=0;
+ GSList* tags=gtk_text_iter_get_tags(&iter);
+ GSList* i;
+ for(i=tags; i; i=g_slist_next(i))
+ {
+ url=g_object_get_data(G_OBJECT(i->data), "url");
+ if(url){break;}
+ }
+ g_slist_free(tags);
+ return url;
+}
+
+gboolean gui_click_link(GtkTextView* textview, GdkEventButton* event, void* data)
+{
+ const char* url=gui_find_link(textview, event->x, event->y);
+ if(url && event->button!=3) // Not right-click, just open it
+ {
+ gtk_show_uri(gtk_widget_get_screen(GTK_WIDGET(textview)), url, gtk_get_current_event_time(), 0);
+ return 1;
+ }
+ return 0;
+}
+
+static const char* gui_link_menu_url=0;
+gboolean gui_rightclick_link(GtkTextView* textview, GdkEventButton* event, void* data)
+{
+ const char* url=gui_find_link(textview, event->x, event->y);
+ if(url && event->button==3)
+ {
+ // Show a menu with options to either open the link or copy it
+ gui_link_menu_url=url;
+ GtkMenu* menu=GTK_MENU(gtk_builder_get_object(gui, "link_menu"));
+ gtk_menu_popup(menu, 0, 0, 0, 0, event->button, event->time);
+ return 1;
+ }
+ return 0;
+}
+
+gboolean gui_hover_link(GtkTextView* textview, GdkEventMotion* event, void* data)
+{
+ const char* url=gui_find_link(textview, event->x, event->y);
+ GdkWindow* window=gtk_text_view_get_window(textview, GTK_TEXT_WINDOW_TEXT);
+ GdkCursor* cursor;
+ if(url)
+ {
+ cursor=gui_cursor_link;
+ }else{
+ cursor=gui_cursor_text;
+ }
+ gdk_window_set_cursor(window, cursor);
+ return 0;
+}
+
+void gui_link_menu_open(GtkWidget* menuitem, void* x)
+{
+ if(!gui_link_menu_url){return;}
+ gtk_show_uri(gtk_widget_get_screen(GTK_WIDGET(menuitem)), gui_link_menu_url, gtk_get_current_event_time(), 0);
+}
+
+void gui_link_menu_copy(GtkWidget* menuitem, void* x)
+{
+ if(!gui_link_menu_url){return;}
+ GtkClipboard* clipboard=gtk_clipboard_get_for_display(gtk_widget_get_display(menuitem), GDK_SELECTION_CLIPBOARD);
+ gtk_clipboard_set_text(clipboard, gui_link_menu_url, -1);
+}
diff --git a/utilities/gtk/gui.h b/utilities/gtk/gui.h
index 3b63f62..33af3ef 100644
--- a/utilities/gtk/gui.h
+++ b/utilities/gtk/gui.h
@@ -1,6 +1,6 @@
/*
tc_client-gtk, a graphical user interface for tc_client
- Copyright (C) 2015 alicia@ion.nu
+ Copyright (C) 2015-2016 alicia@ion.nu
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
@@ -48,5 +48,13 @@ extern void gui_hide_cam(GtkMenuItem* menuitem, void* x);
extern void gui_set_greenscreen_img(GtkButton* button, void* x);
extern void gui_set_greenscreen_color(GtkColorButton* button, void* x);
extern void gui_set_greenscreen_tolerance(GtkAdjustment* adjustment, void* x);
+extern void gui_insert_link(GtkTextBuffer* buffer, GtkTextIter* iter, const char* url, int length);
+extern gboolean gui_click_link(GtkTextView* textview, GdkEventButton* event, void* data);
+extern gboolean gui_rightclick_link(GtkTextView* textview, GdkEventButton* event, void* data);
+extern gboolean gui_hover_link(GtkTextView* textview, GdkEventMotion* event, void* data);
+extern void gui_link_menu_open(GtkWidget* menuitem, void* x);
+extern void gui_link_menu_copy(GtkWidget* menuitem, void* x);
extern GtkBuilder* gui;
+extern GdkCursor* gui_cursor_text;
+extern GdkCursor* gui_cursor_link;