From d5df5e6b07d60d4ac8f43213e719d3331a8b5eb6 Mon Sep 17 00:00:00 2001
From: Florian Zeitz <florob@babelmonkeys.de>
Date: Sun, 23 Dec 2012 02:18:27 +0100
Subject: [PATCH] Add some simple chat UI

---
 data/gtk/chat.ui             | 207 +++++++++++++++++++++++++++++++++++
 src/gui/gtk/JubGtkChatUI.h   |  20 ++++
 src/gui/gtk/JubGtkChatUI.m   | 143 ++++++++++++++++++++++++
 src/gui/gtk/JubGtkRosterUI.h |   1 +
 src/gui/gtk/JubGtkRosterUI.m |  30 +++++
 src/gui/gtk/Makefile         |   1 +
 6 files changed, 402 insertions(+)
 create mode 100644 data/gtk/chat.ui
 create mode 100644 src/gui/gtk/JubGtkChatUI.h
 create mode 100644 src/gui/gtk/JubGtkChatUI.m

diff --git a/data/gtk/chat.ui b/data/gtk/chat.ui
new file mode 100644
index 0000000..8c352cd
--- /dev/null
+++ b/data/gtk/chat.ui
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkWindow" id="ChatWindow">
+    <property name="can_focus">False</property>
+    <property name="default_width">400</property>
+    <property name="default_height">200</property>
+    <child>
+      <object class="GtkBox" id="box1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkMenuBar" id="menubar1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkMenuItem" id="menuitem1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">_Datei</property>
+                <property name="use_underline">True</property>
+                <child type="submenu">
+                  <object class="GtkMenu" id="menu1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem1">
+                        <property name="label">gtk-new</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem2">
+                        <property name="label">gtk-open</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem3">
+                        <property name="label">gtk-save</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem4">
+                        <property name="label">gtk-save-as</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkSeparatorMenuItem" id="separatormenuitem1">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem5">
+                        <property name="label">gtk-quit</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuItem" id="menuitem2">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">_Bearbeiten</property>
+                <property name="use_underline">True</property>
+                <child type="submenu">
+                  <object class="GtkMenu" id="menu2">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem6">
+                        <property name="label">gtk-cut</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem7">
+                        <property name="label">gtk-copy</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem8">
+                        <property name="label">gtk-paste</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem9">
+                        <property name="label">gtk-delete</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuItem" id="menuitem3">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">_Ansicht</property>
+                <property name="use_underline">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuItem" id="menuitem4">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">_Hilfe</property>
+                <property name="use_underline">True</property>
+                <child type="submenu">
+                  <object class="GtkMenu" id="menu3">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkImageMenuItem" id="imagemenuitem10">
+                        <property name="label">gtk-about</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="scrolledwindow1">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hscrollbar_policy">never</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkTextView" id="ChatTextView">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="editable">False</property>
+                <property name="wrap_mode">word</property>
+                <property name="cursor_visible">False</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEntry" id="ChatEntry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">●</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/gui/gtk/JubGtkChatUI.h b/src/gui/gtk/JubGtkChatUI.h
new file mode 100644
index 0000000..5c0addb
--- /dev/null
+++ b/src/gui/gtk/JubGtkChatUI.h
@@ -0,0 +1,20 @@
+#import <ObjFW/ObjFW.h>
+#import <ObjXMPP/ObjXMPP.h>
+#include <gtk/gtk.h>
+
+typedef void (^jub_send_block_t)(OFString *);
+
+@interface JubGtkChatUI: OFObject
+{
+	GtkWidget *chat_window;
+	GtkTextBuffer *chat_buffer;
+	jub_send_block_t sendBlock;
+	BOOL bufferEmpty;
+}
+
+- initWithTitle: (OFString*)title
+      sendBlock: (jub_send_block_t)sendBlock_;
+
+- (void)addMessage: (OFString*)text
+	    sender: (OFString*)sender;
+@end
diff --git a/src/gui/gtk/JubGtkChatUI.m b/src/gui/gtk/JubGtkChatUI.m
new file mode 100644
index 0000000..1dd1155
--- /dev/null
+++ b/src/gui/gtk/JubGtkChatUI.m
@@ -0,0 +1,143 @@
+#import "JubGtkChatUI.h"
+
+struct show_chat_params {
+	GtkWidget *window;
+	OFString *title;
+};
+
+static gboolean show_chat(gpointer data)
+{
+	struct show_chat_params *params = data;
+
+	gtk_window_set_title(GTK_WINDOW(params->window),
+	    [params->title UTF8String]);
+	gtk_widget_show(params->window);
+
+	g_object_unref(params->window);
+	[params->title release];
+	free(params);
+
+	return FALSE;
+}
+
+struct add_text_params {
+	OFString *name;
+	OFString *text;
+	GtkTextBuffer *buffer;
+	BOOL first;
+};
+
+static gboolean add_text(gpointer data)
+{
+	GtkTextIter endIter;
+	struct add_text_params *params = data;
+
+	if (OF_LIKELY(!params->first))
+		gtk_text_buffer_insert_at_cursor(params->buffer, "\n", 1);
+
+	gtk_text_buffer_get_end_iter(params->buffer, &endIter);
+	gtk_text_buffer_insert_with_tags_by_name(params->buffer, &endIter,
+	    [params->name UTF8String], [params->name UTF8StringLength], "bold",
+	    NULL);
+	gtk_text_buffer_get_end_iter(params->buffer, &endIter);
+	gtk_text_buffer_insert_with_tags_by_name(params->buffer, &endIter,
+	    ": ", 2, "bold", NULL);
+
+	gtk_text_buffer_insert_at_cursor(params->buffer,
+	    [params->text UTF8String], [params->text UTF8StringLength]);
+
+	[params->name release];
+	[params->text release];
+	g_object_unref(params->buffer);
+	free(params);
+
+	return FALSE;
+}
+
+static gboolean call_send_block(GtkEntry *entry, GdkEventKey *event,
+    gpointer data)
+{
+	if (event->keyval != GDK_KEY_Return) return TRUE;
+
+	jub_send_block_t block = data;
+	OFString *text =
+	    [[OFString alloc] initWithUTF8String: gtk_entry_get_text(entry)];
+	gtk_entry_set_text(entry, "");
+	block(text);
+	[text release];
+
+	return TRUE;
+}
+
+static gboolean add_tags(gpointer data) {
+	gtk_text_buffer_create_tag(data, "bold", "weight",
+	    PANGO_WEIGHT_BOLD, NULL);
+
+	return FALSE;
+}
+
+@implementation JubGtkChatUI
+- initWithTitle: (OFString*)title
+      sendBlock: (jub_send_block_t)sendBlock_
+{
+	self = [super init];
+
+	@try {
+		GtkTextView *chat_view;
+		GtkEntry *chat_entry;
+		GtkBuilder *builder = gtk_builder_new();
+
+		sendBlock = [sendBlock_ copy];
+		bufferEmpty = YES;
+
+		gtk_builder_add_from_file(builder, "data/gtk/chat.ui", NULL);
+
+		chat_window = GTK_WIDGET(
+		    gtk_builder_get_object(builder, "ChatWindow"));
+
+		chat_view = GTK_TEXT_VIEW(
+		    gtk_builder_get_object(builder, "ChatTextView"));
+
+		chat_entry = GTK_ENTRY(
+		    gtk_builder_get_object(builder, "ChatEntry"));
+
+		g_signal_connect(chat_entry, "key_release_event",
+		    G_CALLBACK(call_send_block), sendBlock);
+
+		chat_buffer = gtk_text_view_get_buffer(chat_view);
+		g_idle_add(add_tags, chat_buffer);
+
+		struct show_chat_params *params = malloc(sizeof(*params));
+		params->window = g_object_ref(chat_window);
+		params->title = [title retain];
+		g_idle_add(show_chat, params);
+
+		g_object_unref(builder);
+	} @catch (id e) {
+		[self release];
+		@throw e;
+	}
+
+	return self;
+}
+
+- (void)dealloc
+{
+	gtk_widget_destroy(chat_window);
+	[sendBlock release];
+
+	[super dealloc];
+}
+
+- (void)addMessage: (OFString*)text
+	    sender: (OFString*)sender;
+{
+	struct add_text_params *params = malloc(sizeof(*params));
+	params->name = [sender retain];
+	params->text = [text retain];
+	params->buffer = g_object_ref(chat_buffer);
+	params->first = bufferEmpty;
+	g_idle_add(add_text, params);
+	if (OF_UNLIKELY(bufferEmpty)) bufferEmpty = NO;
+}
+@end
diff --git a/src/gui/gtk/JubGtkRosterUI.h b/src/gui/gtk/JubGtkRosterUI.h
index 3244ad5..9bb0878 100644
--- a/src/gui/gtk/JubGtkRosterUI.h
+++ b/src/gui/gtk/JubGtkRosterUI.h
@@ -8,6 +8,7 @@
 	GtkTreeModelFilter *roster_filter;
 	OFMapTable *groupMap;
 	OFMutableDictionary *contactMap;
+	OFMutableDictionary *chatMap;
 	OFCountedSet *presences;
 	GtkBuilder *builder;
 }
diff --git a/src/gui/gtk/JubGtkRosterUI.m b/src/gui/gtk/JubGtkRosterUI.m
index acdffb6..9a2da29 100644
--- a/src/gui/gtk/JubGtkRosterUI.m
+++ b/src/gui/gtk/JubGtkRosterUI.m
@@ -1,5 +1,6 @@
 #import "JubGtkRosterUI.h"
 #import "JubGObjectMap.h"
+#import "JubGtkChatUI.h"
 
 static gboolean filter_roster_by_presence(GtkTreeModel *model,
     GtkTreeIter *iter, gpointer data)
@@ -38,6 +39,7 @@ static gboolean filter_roster_by_presence(GtkTreeModel *model,
 		    initWithKeyFunctions: keyFunctions
 			  valueFunctions: rowRefFunctions];
 		contactMap = [[OFMutableDictionary alloc] init];
+		chatMap = [[OFMutableDictionary alloc] init];
 		presences = [[OFCountedSet alloc] init];
 
 		builder = g_object_ref(builder_);
@@ -94,6 +96,34 @@ static gboolean refilter_roster(gpointer data)
 	g_idle_add(refilter_roster, roster_filter);
 }
 
+// FIXME: This needs to move somewhere else
+-  (void)connection: (XMPPConnection*)connection
+  didReceiveMessage: (XMPPMessage*)message
+{
+	JubGtkChatUI *chat = [chatMap objectForKey: [message.from bareJID]];
+	if (chat == nil) {
+		OFString * title = [@"Chat with " stringByAppendingString:
+		    [message.from bareJID]];
+		chat = [JubGtkChatUI alloc];
+		[[chat initWithTitle: title
+			   sendBlock: ^(OFString *text) {
+			XMPPMessage *msg =
+			    [XMPPMessage messageWithType: @"chat"];
+			msg.to = message.from;
+			msg.body = text;
+			[connection sendStanza: msg];
+
+			[chat addMessage: msg.body
+				  sender: [message.to bareJID]];
+		}] autorelease];
+
+		[chatMap setObject: chat
+			    forKey: [message.from bareJID]];
+	}
+	[chat addMessage: message.body
+		  sender: [message.from bareJID]];
+}
+
 /* Roster Delegate methods */
 struct add_roster_item_param {
 	OFString *group;
diff --git a/src/gui/gtk/Makefile b/src/gui/gtk/Makefile
index 25bb2e5..275ebf9 100644
--- a/src/gui/gtk/Makefile
+++ b/src/gui/gtk/Makefile
@@ -1,5 +1,6 @@
 STATIC_LIB_NOINST = gtk.a
 SRCS = JubGtkUI.m	\
+       JubGtkChatUI.m   \
        JubGtkRosterUI.m
 
 include ../../../buildsys.mk
-- 
2.39.5