$ git clone http://tcclient.ion.nu/tc_client.git
commit 972a0a57410f02abae88d6ead4459dd9261ec9fd
Author: Alicia <...>
Date: Fri Jun 12 00:15:22 2015 +0200
tc_client-gtk: added tabs for PMs.
diff --git a/ChangeLog b/ChangeLog
index 5bfe5f1..0277357 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,7 @@ tc_client-gtk: redesigned the startup window to better support frequenting multi
tc_client-gtk and camviewer: added compatibility code to build on windows (for now the tc_client core still needs to be built with cygwin for windows)
tc_client-gtk: made the options for notifications and youtube videos work for windows.
tc_client-gtk: if there are no saved channels, show a placeholder text instructing how to add a channel.
+tc_client-gtk: added tabs for PMs.
0.32:
Added an 'install' make target, adjusted utilities to run tc_client from PATH unless they were run from the build directory (i.e. './<executable>')
Provide feedback on the /ban command.
diff --git a/gtkgui.glade b/gtkgui.glade
index e817f1b..b0f0785 100644
--- a/gtkgui.glade
+++ b/gtkgui.glade
@@ -413,17 +413,35 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
- <object class="GtkScrolledWindow" id="chatscroll">
+ <object class="GtkNotebook" id="tabs">
<property name="visible">True</property>
<property name="can_focus">True</property>
+ <property name="scrollable">True</property>
+ <property name="enable_popup">True</property>
<child>
- <object class="GtkTextView" id="chatview">
+ <object class="GtkScrolledWindow" id="chatscroll">
<property name="visible">True</property>
<property name="can_focus">True</property>
- <property name="editable">False</property>
- <property name="wrap_mode">char</property>
- <property name="cursor_visible">False</property>
+ <child>
+ <object class="GtkTextView" id="chatview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="editable">False</property>
+ <property name="wrap_mode">char</property>
+ <property name="cursor_visible">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label17">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Main</property>
</object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
</child>
</object>
<packing>
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index aaa616d..c95ba35 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -73,7 +73,7 @@ struct viddata
int audiopipe;
SwrContext* swrctx;
#endif
- GtkTextBuffer* buffer; // TODO: struct buffer array, for PMs
+ GtkTextBuffer* buffer;
GtkAdjustment* scroll;
};
struct viddata* data;
@@ -125,34 +125,58 @@ void updatescaling(struct viddata* data, unsigned int width, unsigned int height
}
}
-void printchat(struct viddata* data, const char* text)
+void printchat(const char* text, const char* pm)
{
- char bottom=autoscroll_before(data->scroll);
+ GtkAdjustment* scroll;
+ GtkTextBuffer* buffer;
+ struct user* user;
+ if(pm && (user=finduser(pm)))
+ {
+ pm_open(pm, 0);
+ scroll=user->pm_scroll;
+ buffer=user->pm_buffer;
+ }else{
+ scroll=data->scroll;
+ buffer=data->buffer;
+ }
+ char bottom=autoscroll_before(scroll);
// Insert new content
GtkTextIter end;
- gtk_text_buffer_get_end_iter(data->buffer, &end);
- gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
- gtk_text_buffer_insert(data->buffer, &end, text, -1);
- if(bottom){autoscroll_after(data->scroll);}
+ gtk_text_buffer_get_end_iter(buffer, &end);
+ gtk_text_buffer_insert(buffer, &end, "\n", -1);
+ gtk_text_buffer_insert(buffer, &end, text, -1);
+ if(bottom){autoscroll_after(scroll);}
}
-void printchat_color(struct viddata* data, const char* text, const char* color, unsigned int offset)
+void printchat_color(const char* text, const char* color, unsigned int offset, const char* pm)
{
- char bottom=autoscroll_before(data->scroll);
+ GtkAdjustment* scroll;
+ GtkTextBuffer* buffer;
+ struct user* user;
+ if(pm && (user=finduser(pm)))
+ {
+ pm_open(pm, 0);
+ scroll=user->pm_scroll;
+ buffer=user->pm_buffer;
+ }else{
+ scroll=data->scroll;
+ buffer=data->buffer;
+ }
+ char bottom=autoscroll_before(scroll);
// Insert new content
GtkTextIter end;
- gtk_text_buffer_get_end_iter(data->buffer, &end);
- gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
+ 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(data->buffer, &end, text, -1);
+ gtk_text_buffer_insert(buffer, &end, text, -1);
// Set color if there was one
if(color)
{
GtkTextIter start;
- gtk_text_buffer_get_iter_at_offset(data->buffer, &start, startnum+offset);
- gtk_text_buffer_apply_tag_by_name(data->buffer, color, &start, &end);
+ gtk_text_buffer_get_iter_at_offset(buffer, &start, startnum+offset);
+ gtk_text_buffer_apply_tag_by_name(buffer, color, &start, &end);
}
- if(bottom){autoscroll_after(data->scroll);}
+ if(bottom){autoscroll_after(scroll);}
}
char buf[1024];
@@ -169,7 +193,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
buf[i]=0;
if(!strncmp(buf, "Currently online: ", 18))
{
- printchat(data, buf);
+ printchat(buf, 0);
char* next=&buf[16];
while(next)
{
@@ -190,7 +214,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
// Start streams once we're properly connected
if(!strncmp(buf, "Currently on cam: ", 18))
{
- printchat(data, buf);
+ printchat(buf, 0);
char* next=&buf[16];
while(next)
{
@@ -210,7 +234,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
}
if(buf[0]=='/') // For the /help text
{
- printchat(data, buf);
+ printchat(buf, 0);
return 1;
}
// Remove escape codes and pick up the text color while we're at it
@@ -227,12 +251,12 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
// Timestamped events
if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
{
+ char* pm=0;
char* nick=&buf[8];
space=strchr(nick, ' ');
if(!space){return 1;}
if(space[-1]==':')
{
-// TODO: handle /msg (PMs)
if(config_get_bool("soundradio_cmd"))
{
#ifdef _WIN32
@@ -285,11 +309,23 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
#endif
}
}
+ // Handle incoming PMs
+ else if(!strncmp(space, " /msg ", 6))
+ {
+ char* msg=strchr(&space[6], ' ');
+ if(msg)
+ {
+ memmove(space, msg, strlen(msg)+1);
+ char* end=strchr(nick, ':');
+ pm=strndup(nick, end-nick);
+ }
+ }
}
-// TODO: handle logging PMs
- if(config_get_bool("enable_logging")){logger_write(buf, channel, 0);}
+ if(config_get_bool("enable_logging")){logger_write(buf, channel, pm);}
// Insert new content
- printchat_color(data, buf, color, 8);
+ printchat_color(buf, color, 8, pm);
+ pm_highlight(pm);
+ free(pm);
if(space[-1]!=':') // Not a message
{
if(!strcmp(space, " entered the channel"))
@@ -327,14 +363,14 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
}
if(!strcmp(buf, "Changed color") || !strncmp(buf, "Current color: ", 15))
{
- printchat_color(data, buf, color, 0);
+ printchat_color(buf, color, 0, 0);
free((void*)mycolor);
mycolor=color;
return 1;
}
if(!strncmp(buf, "Color ", 6))
{
- printchat_color(data, buf, color, 0);
+ printchat_color(buf, color, 0, 0);
}
free(color);
if(space && !strcmp(space, " is a moderator."))
@@ -709,6 +745,30 @@ gboolean inputkeys(GtkWidget* widget, GdkEventKey* event, void* data)
void sendmessage(GtkEntry* entry, struct viddata* data)
{
const char* msg=gtk_entry_get_text(entry);
+ char* pm=0;
+ if(!strncmp(msg, "/pm ", 4))
+ {
+ pm_open(&msg[4], 1);
+ gtk_entry_set_text(entry, "");
+ return;
+ }
+ else if(msg[0]!='/') // If we're in a PM tab, send messages as PMs
+ {
+ GtkNotebook* tabs=GTK_NOTEBOOK(gtk_builder_get_object(gui, "tabs"));
+ int num=gtk_notebook_get_current_page(tabs);
+ if(num>0)
+ {
+ GtkWidget* page=gtk_notebook_get_nth_page(tabs, num);
+ struct user* user=user_find_by_tab(page);
+ if(!user) // Person we were PMing with left
+ {
+ gtk_entry_set_text(entry, "");
+ return;
+ }
+ pm=strdup(user->nick);
+ dprintf(tc_client_in[1], "/msg %s ", pm);
+ }
+ }
dprintf(tc_client_in[1], "%s\n", msg);
// Don't print commands
if(!strcmp(msg, "/help") ||
@@ -733,14 +793,36 @@ void sendmessage(GtkEntry* entry, struct viddata* data)
gtk_entry_set_text(entry, "");
return;
}
+ if(!strncmp(msg, "/msg ", 5))
+ {
+ const char* end=strchr(&msg[5], ' ');
+ if(end)
+ {
+ pm=strndup(&msg[5], end-&msg[5]);
+ struct user* user=finduser(pm);
+ if(!user)
+ {
+ gtk_entry_set_text(entry, "");
+ printchat("No such user", 0);
+ free(pm);
+ return;
+ }
+ }
+ }
char text[strlen("[00:00] ")+strlen(nickname)+strlen(": ")+strlen(msg)+1];
time_t timestamp=time(0);
struct tm* t=localtime(×tamp);
sprintf(text, "[%02i:%02i] ", t->tm_hour, t->tm_min);
- sprintf(&text[8], "%s: %s", nickname, msg);
- if(config_get_bool("enable_logging")){logger_write(text, channel, 0);}
- printchat_color(data, text, mycolor, 8);
+ if(pm && msg[0]=='/')
+ {
+ sprintf(&text[8], "%s: %s", nickname, &msg[6+strlen(pm)]);
+ }else{
+ sprintf(&text[8], "%s: %s", nickname, msg);
+ }
+ if(config_get_bool("enable_logging")){logger_write(text, channel, pm);}
+ printchat_color(text, mycolor, 8, pm);
gtk_entry_set_text(entry, "");
+ free(pm);
}
void startsession(GtkButton* button, void* x)
@@ -899,23 +981,7 @@ int main(int argc, char** argv)
data->scroll=gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(gtk_builder_get_object(gui, "chatscroll")));
data->buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(chatview));
- #define colormap(code, color) gtk_text_buffer_create_tag(data->buffer, code, "foreground", color, (char*)0)
- colormap("[31", "#821615");
- colormap("[31;1", "#c53332");
- colormap("[33", "#a08f23");
- //colormap("[33", "#a78901");
- colormap("[33;1", "#919104");
- colormap("[32;1", "#7bb224");
- //colormap("[32;1", "#7db257");
- colormap("[32", "#487d21");
- colormap("[36", "#00a990");
- colormap("[34;1", "#32a5d9");
- //colormap("[34;1", "#1d82eb");
- colormap("[34", "#1965b6");
- colormap("[35", "#5c1a7a");
- colormap("[35;1", "#9d5bb5");
- //colormap("[35;1", "#c356a3");
- //colormap("[35;1", "#b9807f");
+ buffer_setup_colors(data->buffer);
GtkWidget* panes=GTK_WIDGET(gtk_builder_get_object(gui, "vpaned"));
g_signal_connect(panes, "notify::position", G_CALLBACK(handleresizepane), data);
@@ -954,6 +1020,9 @@ int main(int argc, char** argv)
item=GTK_WIDGET(gtk_builder_get_object(gui, "start_menu_add"));
struct channelopts cc_add={-1,1};
g_signal_connect(item, "activate", G_CALLBACK(channeldialog), &cc_add);
+ // Connect signal for tab changing (to un-highlight)
+ item=GTK_WIDGET(gtk_builder_get_object(gui, "tabs"));
+ g_signal_connect(item, "switch-page", G_CALLBACK(pm_select), 0);
// Populate saved channels
GtkWidget* startbox=GTK_WIDGET(gtk_builder_get_object(gui, "startbox"));
int channelcount=config_get_int("channelcount");
diff --git a/utilities/gtk/gui.c b/utilities/gtk/gui.c
index 0b8dbfd..867b025 100644
--- a/utilities/gtk/gui.c
+++ b/utilities/gtk/gui.c
@@ -22,6 +22,7 @@
#include "config.h"
#include "logging.h"
#include "compat.h"
+#include "userlist.h"
extern void startsession(GtkButton* button, void* x);
GtkBuilder* gui;
@@ -299,3 +300,99 @@ void channeldialog(GtkButton* button, struct channelopts* opts)
g_signal_connect(obj, "clicked", G_CALLBACK(deletechannel), (void*)(intptr_t)opts->channel_id);
}
}
+
+void pm_close(GtkButton* btn, GtkWidget* tab)
+{
+ gtk_widget_destroy(tab);
+ struct user* user=user_find_by_tab(tab);
+ if(!user){return;}
+ user->pm_tab=0;
+ user->pm_tablabel=0;
+ user->pm_buffer=0;
+ user->pm_scroll=0;
+ user->pm_highlight=0;
+}
+
+void pm_open(const char* nick, char select)
+{
+ struct user* user=finduser(nick);
+ if(!user){return;}
+ GtkNotebook* tabs=GTK_NOTEBOOK(gtk_builder_get_object(gui, "tabs"));
+ if(user->pm_tab)
+ {
+ if(!select){return;}
+ int num=gtk_notebook_page_num(tabs, user->pm_tab);
+ gtk_notebook_set_current_page(tabs, num);
+ return;
+ }
+ user->pm_tab=gtk_text_view_new();
+ user->pm_buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(user->pm_tab));
+ user->pm_scroll=gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(user->pm_tab));
+ user->pm_tablabel=gtk_label_new(nick);
+ buffer_setup_colors(user->pm_buffer);
+ GtkWidget* tabbox=gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+#if GTK_MAJOR_VERSION<3 || (GTK_MAJOR_VERSION==3 && GTK_MINOR_VERSION<10)
+ GtkWidget* closebtn=gtk_button_new_from_icon_name("gtk-close", GTK_ICON_SIZE_BUTTON);
+#else
+ GtkWidget* closebtn=gtk_button_new_from_icon_name("window-close", GTK_ICON_SIZE_BUTTON);
+#endif
+ g_signal_connect(closebtn, "clicked", G_CALLBACK(pm_close), user->pm_tab);
+ gtk_box_pack_start(GTK_BOX(tabbox), user->pm_tablabel, 1, 1, 0);
+ gtk_box_pack_start(GTK_BOX(tabbox), closebtn, 0, 0, 0);
+ int num=gtk_notebook_append_page(tabs, user->pm_tab, tabbox);
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(user->pm_tab), 0);
+ gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(user->pm_tab), 0);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(user->pm_tab), GTK_WRAP_CHAR);
+ gtk_widget_show_all(user->pm_tab);
+ gtk_widget_show_all(tabbox);
+ if(select)
+ {
+ gtk_notebook_set_current_page(tabs, num);
+ }
+}
+
+void pm_highlight(const char* nick)
+{
+ if(!nick){return;}
+ struct user* user=finduser(nick);
+ if(!user || !user->pm_tablabel){return;}
+ // Only highlight tabs we're not on
+ GtkNotebook* tabs=GTK_NOTEBOOK(gtk_builder_get_object(gui, "tabs"));
+ GtkWidget* page=gtk_notebook_get_nth_page(tabs, gtk_notebook_get_current_page(tabs));
+ if(page==user->pm_tab){return;}
+ char* markup=g_markup_printf_escaped("<span color=\"red\">%s</span>", user->nick);
+ gtk_label_set_markup(GTK_LABEL(user->pm_tablabel), markup);
+ g_free(markup);
+ user->pm_highlight=1;
+}
+
+char pm_select(GtkNotebook* tabs, GtkWidget* tab, int num, void* x)
+{
+ struct user* user=user_find_by_tab(tab);
+ if(!user){return 0;}
+ // Reset highlighting
+ gtk_label_set_text(GTK_LABEL(user->pm_tablabel), user->nick);
+ user->pm_highlight=0;
+ return 0;
+}
+
+void buffer_setup_colors(GtkTextBuffer* buffer)
+{
+ #define colormap(code, color) gtk_text_buffer_create_tag(buffer, code, "foreground", color, (char*)0)
+ colormap("[31", "#821615");
+ colormap("[31;1", "#c53332");
+ colormap("[33", "#a08f23");
+ //colormap("[33", "#a78901");
+ colormap("[33;1", "#919104");
+ colormap("[32;1", "#7bb224");
+ //colormap("[32;1", "#7db257");
+ colormap("[32", "#487d21");
+ colormap("[36", "#00a990");
+ colormap("[34;1", "#32a5d9");
+ //colormap("[34;1", "#1d82eb");
+ colormap("[34", "#1965b6");
+ colormap("[35", "#5c1a7a");
+ colormap("[35;1", "#9d5bb5");
+ //colormap("[35;1", "#c356a3");
+ //colormap("[35;1", "#b9807f");
+}
diff --git a/utilities/gtk/gui.h b/utilities/gtk/gui.h
index 971d680..aa646b2 100644
--- a/utilities/gtk/gui.h
+++ b/utilities/gtk/gui.h
@@ -32,5 +32,9 @@ extern void toggle_logging(GtkToggleButton* button, GtkBuilder* gui);
extern void toggle_youtubecmd(GtkToggleButton* button, GtkBuilder* gui);
extern void deletechannel(GtkButton* button, void* x);
extern void channeldialog(GtkButton* button, struct channelopts* opts);
+extern void pm_open(const char* nick, char select);
+extern void pm_highlight(const char* nick);
+extern char pm_select(GtkNotebook* tabs, GtkWidget* tab, int num, void* x);
+extern void buffer_setup_colors(GtkTextBuffer* buffer);
extern GtkBuilder* gui;
diff --git a/utilities/gtk/userlist.c b/utilities/gtk/userlist.c
index 89d1df5..0aac809 100644
--- a/utilities/gtk/userlist.c
+++ b/utilities/gtk/userlist.c
@@ -17,6 +17,7 @@
#include <stdlib.h>
#include <string.h>
#include <gtk/gtk.h>
+#include "gui.h"
#include "userlist.h"
struct user* userlist=0;
@@ -33,6 +34,16 @@ struct user* finduser(const char* nick)
return 0;
}
+struct user* user_find_by_tab(GtkWidget* tab)
+{
+ unsigned int i;
+ for(i=0; i<usercount; ++i)
+ {
+ if(userlist[i].pm_tab==tab){return &userlist[i];}
+ }
+ return 0;
+}
+
struct user* adduser(const char* nick)
{
struct user* user=finduser(nick);
@@ -41,6 +52,11 @@ struct user* adduser(const char* nick)
userlist=realloc(userlist, sizeof(struct user)*usercount);
userlist[usercount-1].nick=strdup(nick);
userlist[usercount-1].label=gtk_label_new(nick); // TODO: some kind of menubutton for actions?
+ userlist[usercount-1].pm_tab=0;
+ userlist[usercount-1].pm_tablabel=0;
+ userlist[usercount-1].pm_buffer=0;
+ userlist[usercount-1].pm_scroll=0;
+ userlist[usercount-1].pm_highlight=0;
#if GTK_MAJOR_VERSION>=3
gtk_widget_set_halign(userlist[usercount-1].label, GTK_ALIGN_START);
#endif
@@ -65,6 +81,15 @@ void renameuser(const char* old, const char* newnick)
}else{
gtk_label_set_text(GTK_LABEL(user->label), newnick);
}
+ if(user->pm_tablabel)
+ {
+ if(user->pm_highlight)
+ {
+ pm_highlight(newnick);
+ }else{
+ gtk_label_set_text(GTK_LABEL(user->pm_tablabel), newnick);
+ }
+ }
}
void removeuser(const char* nick)
diff --git a/utilities/gtk/userlist.h b/utilities/gtk/userlist.h
index 98b886e..eb229c6 100644
--- a/utilities/gtk/userlist.h
+++ b/utilities/gtk/userlist.h
@@ -20,6 +20,11 @@ struct user
GtkWidget* label;
// unsigned int id; // hm, tc_client doesn't share IDs other than in guestnicks, this might be useful for a ban-after-they-left situation
char ismod;
+ GtkWidget* pm_tab;
+ GtkWidget* pm_tablabel;
+ GtkTextBuffer* pm_buffer;
+ GtkAdjustment* pm_scroll;
+ char pm_highlight;
};
extern struct user* userlist;
@@ -27,6 +32,7 @@ extern unsigned int usercount;
extern GtkWidget* userlistwidget;
extern struct user* finduser(const char* nick);
+extern struct user* user_find_by_tab(GtkWidget* tab);
extern struct user* adduser(const char* nick);
extern void renameuser(const char* old, const char* newnick);
extern void removeuser(const char* nick);