$ git clone http://tcclient.ion.nu/tc_client.git
commit 3b28271ef7910cf382fb498094c8ec636a9fb065
Author: Alicia <...>
Date:   Thu Sep 29 02:23:11 2016 +0200

    tc_client-gtk: reimplemented automatic scrolling using a state to keep track of whether the GtkTextView is scrolled to the bottom and using gtk_text_view_scroll_to_mark() to scroll.

diff --git a/ChangeLog b/ChangeLog
index ce0fbe7..7e3ab65 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -9,6 +9,7 @@ tc_client-gtk: use the camera's own copy of its ID for g_timeout_add() rather th
 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: reimplemented automatic scrolling using a state to keep track of whether the GtkTextView is scrolled to the bottom and using gtk_text_view_scroll_to_mark() to scroll.
 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/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index 8e4581f..eaa5776 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -69,8 +69,6 @@ struct viddata
   int audiopipe;
   SwrContext* swrctx;
 #endif
-  GtkTextBuffer* buffer;
-  GtkAdjustment* scroll;
 };
 struct viddata* data;
 
@@ -80,6 +78,7 @@ const char* channel=0;
 const char* mycolor=0;
 char* nickname=0;
 char frombuild=0; // Running from the build directory
+struct chatview* mainchat;
 #define TC_CLIENT (frombuild?"./tc_client":"tc_client")
 #ifdef _WIN32
   PROCESS_INFORMATION coreprocess={.hProcess=0};
@@ -87,19 +86,16 @@ char frombuild=0; // Running from the build directory
 
 void printchat(const char* text, const char* color, unsigned int offset, const char* pm)
 {
-  GtkAdjustment* scroll;
-  GtkTextBuffer* buffer;
+  struct chatview* chatview;
   struct user* user;
   if(pm && (user=finduser(pm)))
   {
-    pm_open(pm, 0, data->scroll);
-    scroll=user->pm_scroll;
-    buffer=user->pm_buffer;
+    pm_open(pm, 0);
+    chatview=user->pm_chatview;
   }else{
-    scroll=data->scroll;
-    buffer=data->buffer;
+    chatview=mainchat;
   }
-  char bottom=autoscroll_before(scroll);
+  GtkTextBuffer* buffer=gtk_text_view_get_buffer(chatview->textview);
   // Insert new content
   GtkTextIter end;
   gtk_text_buffer_get_end_iter(buffer, &end);
@@ -146,7 +142,7 @@ void printchat(const char* text, const char* color, unsigned int offset, const c
     gtk_text_buffer_apply_tag_by_name(buffer, "nickname", &start, &end);
   }
   buffer_updatesize(buffer);
-  if(bottom){autoscroll_after(scroll);}
+  chatview_autoscroll(chatview);
 }
 
 unsigned int cameventsource=0;
@@ -615,24 +611,36 @@ void togglecam(GtkCheckMenuItem* item, struct viddata* data)
 
 gboolean handleresize(GtkWidget* widget, GdkEventConfigure* event, struct viddata* data)
 {
-  char bottom=autoscroll_before(data->scroll);
   if(event->width!=gtk_widget_get_allocated_width(cambox))
   {
     updatescaling(event->width, 0, 0);
   }
-#ifndef _WIN32 // For some reason scrolling as a response to resizing freezes windows
-  if(bottom){autoscroll_after(data->scroll);}
-#endif
+  // Fix scrolling
+  chatview_autoscroll(mainchat);
+  unsigned int i;
+  for(i=0; i<usercount; ++i)
+  {
+    if(userlist[i].pm_chatview)
+    {
+      chatview_autoscroll(userlist[i].pm_chatview);
+    }
+  }
   return 0;
 }
 
 void handleresizepane(GObject* obj, GParamSpec* spec, struct viddata* data)
 {
-  char bottom=autoscroll_before(data->scroll);
   updatescaling(0, gtk_paned_get_position(GTK_PANED(obj)), 0);
-#ifndef _WIN32
-  if(bottom){autoscroll_after(data->scroll);}
-#endif
+  // Fix scrolling
+  chatview_autoscroll(mainchat);
+  unsigned int i;
+  for(i=0; i<usercount; ++i)
+  {
+    if(userlist[i].pm_chatview)
+    {
+      chatview_autoscroll(userlist[i].pm_chatview);
+    }
+  }
 }
 
 gboolean inputkeys(GtkWidget* widget, GdkEventKey* event, void* data)
@@ -702,7 +710,7 @@ void sendmessage(GtkEntry* entry, void* x)
   char* pm=0;
   if(!strncmp(msg, "/pm ", 4))
   {
-    pm_open(&msg[4], 1, data->scroll);
+    pm_open(&msg[4], 1);
     gtk_entry_set_text(entry, "");
     sendingmsg=0;
     return;
@@ -886,7 +894,7 @@ void captcha_done(GtkWidget* button, void* x)
 int main(int argc, char** argv)
 {
   if(!strncmp(argv[0], "./", 2)){frombuild=1;}
-  struct viddata datax={0,0,0,0,0};
+  struct viddata datax={0,0,0};
   data=&datax;
   avcodec_register_all();
   data->vdecoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
@@ -977,18 +985,11 @@ int main(int argc, char** argv)
   cambox=GTK_WIDGET(gtk_builder_get_object(gui, "cambox"));
   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);
+  mainchat=chatview_new(GTK_TEXT_VIEW(chatview));
   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);
-
   GtkWidget* panes=GTK_WIDGET(gtk_builder_get_object(gui, "vpaned"));
   g_signal_connect(panes, "notify::position", G_CALLBACK(handleresizepane), data);
   gtk_paned_set_wide_handle(GTK_PANED(panes), 1);
diff --git a/utilities/gtk/gui.c b/utilities/gtk/gui.c
index d51534a..0303173 100644
--- a/utilities/gtk/gui.c
+++ b/utilities/gtk/gui.c
@@ -34,23 +34,6 @@ extern gboolean gui_greenscreen_preview(void* x);
 GdkCursor* gui_cursor_text;
 GdkCursor* gui_cursor_link;
 
-char autoscroll_before(GtkAdjustment* scroll)
-{
-  // Figure out if we're at the bottom and should autoscroll with new content
-  int upper=gtk_adjustment_get_upper(scroll);
-  int size=gtk_adjustment_get_page_size(scroll);
-  int value=gtk_adjustment_get_value(scroll);
-  return (value+size+20>=upper);
-}
-
-void autoscroll_after(GtkAdjustment* scroll)
-{
-  while(gtk_events_pending()){gtk_main_iteration();} // Make sure the textview's new size affects scroll's "upper" value first
-  int upper=gtk_adjustment_get_upper(scroll);
-  int size=gtk_adjustment_get_page_size(scroll);
-  gtk_adjustment_set_value(scroll, upper-size);
-}
-
 void settings_reset(GtkBuilder* gui)
 {
   // Font
@@ -336,56 +319,41 @@ void pm_close(GtkButton* btn, GtkWidget* tab)
   gtk_widget_destroy(tab);
   struct user* user=user_find_by_tab(tab);
   if(!user){return;}
+  free(user->pm_chatview);
   user->pm_tab=0;
   user->pm_tablabel=0;
-  user->pm_buffer=0;
-  user->pm_scroll=0;
+  user->pm_chatview=0;
   user->pm_highlight=0;
 }
 
-void pm_open(const char* nick, char select, GtkAdjustment* scroll)
+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(!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_chatview=chatview_new(0);
+    user->pm_tab=user->pm_chatview->scrolledwindow;
+    user->pm_tablabel=gtk_label_new(nick);
+    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);
+    gtk_notebook_append_page(tabs, user->pm_tab, tabbox);
+    gtk_widget_show_all(user->pm_tab);
+    gtk_widget_show_all(tabbox);
   }
-  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));
-  user->pm_tablabel=gtk_label_new(nick);
-  buffer_setup_colors(user->pm_buffer);
-  gtk_container_add(GTK_CONTAINER(user->pm_tab), textview);
-  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(textview), 0);
-  gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(textview), 0);
-  gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(textview), GTK_WRAP_CHAR);
-  gtk_widget_show_all(user->pm_tab);
-  gtk_widget_show_all(tabbox);
   if(select)
   {
+    int num=gtk_notebook_page_num(tabs, user->pm_tab);
     gtk_notebook_set_current_page(tabs, num);
   }
-  if(bottom){autoscroll_after(scroll);}
 }
 
 void pm_highlight(const char* nick)
@@ -418,36 +386,6 @@ char pm_select(GtkNotebook* tabs, GtkWidget* tab, int num, void* x)
   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");
-  colormap("timestamp", "#808080");
-  gtk_text_buffer_create_tag(buffer, "nickname", "weight", PANGO_WEIGHT_BOLD, "weight-set", TRUE, (char*)0);
-  // Set size if it's set in config
-  if(config_get_set("fontsize"))
-  {
-    gtk_text_buffer_create_tag(buffer, "size", "size-points", config_get_double("fontsize"), "size-set", TRUE, (char*)0);
-  }else{
-    gtk_text_buffer_create_tag(buffer, "size", "size-set", FALSE, (char*)0);
-  }
-}
-
 void buffer_updatesize(GtkTextBuffer* buffer)
 {
   GtkTextIter start, end;
@@ -468,11 +406,12 @@ void fontsize_set(double size)
   unsigned int i;
   for(i=0; i<usercount; ++i)
   {
-    if(!userlist[i].pm_buffer){continue;}
-    table=gtk_text_buffer_get_tag_table(userlist[i].pm_buffer);
+    if(!userlist[i].pm_chatview){continue;}
+    buffer=gtk_text_view_get_buffer(userlist[i].pm_chatview->textview);
+    table=gtk_text_buffer_get_tag_table(buffer);
     tag=gtk_text_tag_table_lookup(table, "size");
     g_object_set(tag, "size-points", size, "size-set", TRUE, (char*)0);
-    buffer_updatesize(userlist[i].pm_buffer);
+    buffer_updatesize(buffer);
   }
 }
 
@@ -767,3 +706,68 @@ void gui_link_menu_copy(GtkWidget* menuitem, void* x)
   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);
 }
+
+void chatview_scrolled(GtkAdjustment* adj, struct chatview* cv)
+{
+  double value=gtk_adjustment_get_value(adj);
+  double upper=gtk_adjustment_get_upper(adj);
+  double pagesize=gtk_adjustment_get_page_size(adj);
+  char bottom=(value+pagesize>=upper);
+  // To keep smooth scrolling from messing up autoscroll,
+  // don't change state if we're already marked as being at the
+  // bottom and scrolling down.
+  if(bottom || value<cv->oldscrollposition)
+  {
+    cv->atbottom=bottom;
+    cv->oldscrollposition=value;
+  }
+}
+
+struct chatview* chatview_new(GtkTextView* existing_textview)
+{
+  struct chatview* this=malloc(sizeof(struct chatview));
+  if(existing_textview)
+  {
+    this->textview=existing_textview;
+    this->scrolledwindow=gtk_widget_get_parent(GTK_WIDGET(this->textview));
+  }else{
+    this->textview=GTK_TEXT_VIEW(gtk_text_view_new());
+    this->scrolledwindow=gtk_scrolled_window_new(0, 0);
+    gtk_container_add(GTK_CONTAINER(this->scrolledwindow), GTK_WIDGET(this->textview));
+  }
+  this->atbottom=1;
+  gtk_text_view_set_editable(this->textview, 0);
+  gtk_text_view_set_cursor_visible(this->textview, 0);
+  gtk_text_view_set_wrap_mode(this->textview, GTK_WRAP_CHAR);
+  GtkAdjustment* scroll=gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(this->scrolledwindow));
+  g_signal_connect(this->textview, "button-release-event", G_CALLBACK(gui_click_link), 0);
+  g_signal_connect(this->textview, "button-press-event", G_CALLBACK(gui_rightclick_link), 0);
+  g_signal_connect(this->textview, "motion-notify-event", G_CALLBACK(gui_hover_link), 0);
+  g_signal_connect(scroll, "value-changed", G_CALLBACK(chatview_scrolled), this);
+
+  // Set up the buffer
+  GtkTextBuffer* buffer=gtk_text_view_get_buffer(this->textview);
+  gtk_text_buffer_create_tag(buffer, "timestamp", "foreground", "#808080", (char*)0);
+  gtk_text_buffer_create_tag(buffer, "nickname", "weight", PANGO_WEIGHT_BOLD, "weight-set", TRUE, (char*)0);
+  // Set size if it's set in config
+  if(config_get_set("fontsize"))
+  {
+    gtk_text_buffer_create_tag(buffer, "size", "size-points", config_get_double("fontsize"), "size-set", TRUE, (char*)0);
+  }else{
+    gtk_text_buffer_create_tag(buffer, "size", "size-set", FALSE, (char*)0);
+  }
+  // And en 'end' mark for scrolling
+  GtkTextIter end;
+  gtk_text_buffer_get_end_iter(buffer, &end);
+  gtk_text_buffer_create_mark(buffer, "end", &end, 0);
+
+  return this;
+}
+
+void chatview_autoscroll(struct chatview* cv)
+{
+  if(!cv->atbottom){return;}
+  GtkTextBuffer* buffer=gtk_text_view_get_buffer(cv->textview);
+  GtkTextMark* mark=gtk_text_buffer_get_mark(buffer, "end");
+  gtk_text_view_scroll_to_mark(cv->textview, mark, 0, 0, 0, 0);
+}
diff --git a/utilities/gtk/gui.h b/utilities/gtk/gui.h
index 33af3ef..e0071fb 100644
--- a/utilities/gtk/gui.h
+++ b/utilities/gtk/gui.h
@@ -22,8 +22,15 @@ struct channelopts
   char save;
 };
 
-extern char autoscroll_before(GtkAdjustment* scroll);
-extern void autoscroll_after(GtkAdjustment* scroll);
+struct chatview
+{
+  GtkTextView* textview;
+  GtkWidget* scrolledwindow;
+  char atbottom; // If we're scrolled to the bottom we should stay at the bottom
+                 // unless the user scrolls up
+  double oldscrollposition; // To keep smooth scrolling from messing things up
+};
+
 extern void settings_reset(GtkBuilder* gui);
 extern void showsettings(GtkMenuItem* item, GtkBuilder* gui);
 extern void savesettings(GtkButton* button, GtkBuilder* gui);
@@ -32,10 +39,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, GtkAdjustment* scroll);
+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 void buffer_updatesize(GtkTextBuffer* buffer);
 extern void fontsize_set(double size);
 extern gboolean gui_show_cam_menu(GtkWidget* widget, GdkEventButton* event, const char* id);
@@ -54,6 +60,8 @@ extern gboolean gui_rightclick_link(GtkTextView* textview, GdkEventButton* event
 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 struct chatview* chatview_new(GtkTextView* existing_textview);
+extern void chatview_autoscroll(struct chatview* cv);
 
 extern GtkBuilder* gui;
 extern GdkCursor* gui_cursor_text;
diff --git a/utilities/gtk/userlist.c b/utilities/gtk/userlist.c
index 0aac809..1121749 100644
--- a/utilities/gtk/userlist.c
+++ b/utilities/gtk/userlist.c
@@ -1,6 +1,6 @@
 /*
     camviewer, a sample application to view tinychat cam streams
-    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
@@ -54,8 +54,7 @@ struct user* adduser(const char* 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_chatview=0;
   userlist[usercount-1].pm_highlight=0;
 #if GTK_MAJOR_VERSION>=3
   gtk_widget_set_halign(userlist[usercount-1].label, GTK_ALIGN_START);
diff --git a/utilities/gtk/userlist.h b/utilities/gtk/userlist.h
index eb229c6..0b58dbe 100644
--- a/utilities/gtk/userlist.h
+++ b/utilities/gtk/userlist.h
@@ -1,6 +1,6 @@
 /*
     camviewer, a sample application to view tinychat cam streams
-    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
@@ -18,12 +18,10 @@ struct user
 {
   char* nick;
   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;
+  struct chatview* pm_chatview;
   char pm_highlight;
 };