]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-chat.c
cdddf2d6c46379d5f0b43b8b4e419fb4005d145e
[empathy.git] / libempathy-gtk / empathy-chat.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2002-2007 Imendio AB
4  * Copyright (C) 2007-2008 Collabora Ltd.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public
17  * License along with this program; if not, write to the
18  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  * 
21  * Authors: Mikael Hallendal <micke@imendio.com>
22  *          Richard Hult <richard@imendio.com>
23  *          Martyn Russell <martyn@imendio.com>
24  *          Geert-Jan Van den Bogaerde <geertjan@gnome.org>
25  *          Xavier Claessens <xclaesse@gmail.com>
26  */
27
28 #include <config.h>
29
30 #include <string.h>
31 #include <stdlib.h>
32
33 #include <gdk/gdkkeysyms.h>
34 #include <glib/gi18n.h>
35 #include <gtk/gtk.h>
36
37 #include <libmissioncontrol/mission-control.h>
38 #include <telepathy-glib/util.h>
39
40 #include <libempathy/empathy-log-manager.h>
41 #include <libempathy/empathy-contact-list.h>
42 #include <libempathy/empathy-debug.h>
43 #include <libempathy/empathy-utils.h>
44
45 #include "empathy-chat.h"
46 #include "empathy-conf.h"
47 #include "empathy-spell.h"
48 #include "empathy-spell-dialog.h"
49 #include "empathy-contact-list-store.h"
50 #include "empathy-contact-list-view.h"
51 #include "empathy-ui-utils.h"
52
53 #define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), EMPATHY_TYPE_CHAT, EmpathyChatPriv))
54
55 #define DEBUG_DOMAIN "Chat"
56
57 #define CHAT_DIR_CREATE_MODE  (S_IRUSR | S_IWUSR | S_IXUSR)
58 #define CHAT_FILE_CREATE_MODE (S_IRUSR | S_IWUSR)
59 #define IS_ENTER(v) (v == GDK_Return || v == GDK_ISO_Enter || v == GDK_KP_Enter)
60 #define MAX_INPUT_HEIGHT 150
61 #define COMPOSING_STOP_TIMEOUT 5
62
63 struct _EmpathyChatPriv {
64         EmpathyTpChat     *tp_chat;
65         McAccount         *account;
66         gchar             *id;
67         gchar             *name;
68         gchar             *subject;
69         EmpathyContact    *remote_contact;
70
71         EmpathyLogManager *log_manager;
72         MissionControl    *mc;
73         GSList            *sent_messages;
74         gint               sent_messages_index;
75         GList             *compositors;
76         GList             *backlog_messages;
77         GCompletion       *completion;
78         guint              composing_stop_timeout_id;
79         guint              block_events_timeout_id;
80         TpHandleType       handle_type;
81         gpointer           token;
82         gint               contacts_width;
83
84         GtkWidget         *widget;
85         GtkWidget         *hpaned;
86         GtkWidget         *vbox_left;
87         GtkWidget         *scrolled_window_chat;
88         GtkWidget         *scrolled_window_input;
89         GtkWidget         *scrolled_window_contacts;
90         GtkWidget         *hbox_topic;
91         GtkWidget         *label_topic;
92         EmpathyContactListView *view;
93         EmpathyContactListStore *store;
94
95         /* Used to automatically shrink a window that has temporarily
96          * grown due to long input. 
97          */
98         gint               padding_height;
99         gint               default_window_height;
100         gint               last_input_height;
101         gboolean           vscroll_visible;
102         gboolean           is_first_char;
103 };
104
105 static void empathy_chat_class_init (EmpathyChatClass *klass);
106 static void empathy_chat_init       (EmpathyChat      *chat);
107
108 enum {
109         COMPOSING,
110         NEW_MESSAGE,
111         LAST_SIGNAL
112 };
113
114 enum {
115         PROP_0,
116         PROP_TP_CHAT,
117         PROP_ACCOUNT,
118         PROP_ID,
119         PROP_NAME,
120         PROP_SUBJECT,
121         PROP_REMOTE_CONTACT,
122 };
123
124 static guint signals[LAST_SIGNAL] = { 0 };
125
126 G_DEFINE_TYPE (EmpathyChat, empathy_chat, GTK_TYPE_BIN);
127
128 static void
129 chat_get_property (GObject    *object,
130                    guint       param_id,
131                    GValue     *value,
132                    GParamSpec *pspec)
133 {
134         EmpathyChatPriv *priv = GET_PRIV (object);
135
136         switch (param_id) {
137         case PROP_TP_CHAT:
138                 g_value_set_object (value, priv->tp_chat);
139                 break;
140         case PROP_ACCOUNT:
141                 g_value_set_object (value, priv->account);
142                 break;
143         case PROP_NAME:
144                 g_value_set_string (value, priv->name);
145                 break;
146         case PROP_ID:
147                 g_value_set_string (value, priv->id);
148                 break;
149         case PROP_SUBJECT:
150                 g_value_set_string (value, priv->subject);
151                 break;
152         case PROP_REMOTE_CONTACT:
153                 g_value_set_object (value, priv->remote_contact);
154                 break;
155         default:
156                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
157                 break;
158         };
159 }
160
161 static void
162 chat_set_property (GObject      *object,
163                    guint         param_id,
164                    const GValue *value,
165                    GParamSpec   *pspec)
166 {
167         EmpathyChat *chat = EMPATHY_CHAT (object);
168
169         switch (param_id) {
170         case PROP_TP_CHAT:
171                 empathy_chat_set_tp_chat (chat, EMPATHY_TP_CHAT (g_value_get_object (value)));
172                 break;
173         default:
174                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
175                 break;
176         };
177 }
178
179 static void
180 chat_status_changed_cb (MissionControl           *mc,
181                         TpConnectionStatus        status,
182                         McPresence                presence,
183                         TpConnectionStatusReason  reason,
184                         const gchar              *unique_name,
185                         EmpathyChat              *chat)
186 {
187         EmpathyChatPriv *priv = GET_PRIV (chat);
188         McAccount       *account;
189
190         account = mc_account_lookup (unique_name);
191
192         if (status == TP_CONNECTION_STATUS_CONNECTED && !priv->tp_chat &&
193             empathy_account_equal (account, priv->account) &&
194             priv->handle_type != 0) {
195                 empathy_debug (DEBUG_DOMAIN,
196                                "Account reconnected, request a new Text channel");
197                 mission_control_request_channel_with_string_handle (mc,
198                                                                     priv->account,
199                                                                     TP_IFACE_CHANNEL_TYPE_TEXT,
200                                                                     priv->id,
201                                                                     priv->handle_type,
202                                                                     NULL, NULL);
203         }
204
205         g_object_unref (account);
206 }
207
208 static void
209 chat_composing_remove_timeout (EmpathyChat *chat)
210 {
211         EmpathyChatPriv *priv;
212
213         priv = GET_PRIV (chat);
214
215         if (priv->composing_stop_timeout_id) {
216                 g_source_remove (priv->composing_stop_timeout_id);
217                 priv->composing_stop_timeout_id = 0;
218         }
219 }
220
221 static gboolean
222 chat_composing_stop_timeout_cb (EmpathyChat *chat)
223 {
224         EmpathyChatPriv *priv;
225
226         priv = GET_PRIV (chat);
227
228         priv->composing_stop_timeout_id = 0;
229         empathy_tp_chat_set_state (priv->tp_chat,
230                                    TP_CHANNEL_CHAT_STATE_PAUSED);
231
232         return FALSE;
233 }
234
235 static void
236 chat_composing_start (EmpathyChat *chat)
237 {
238         EmpathyChatPriv *priv;
239
240         priv = GET_PRIV (chat);
241
242         if (priv->composing_stop_timeout_id) {
243                 /* Just restart the timeout */
244                 chat_composing_remove_timeout (chat);
245         } else {
246                 empathy_tp_chat_set_state (priv->tp_chat,
247                                            TP_CHANNEL_CHAT_STATE_COMPOSING);
248         }
249
250         priv->composing_stop_timeout_id = g_timeout_add_seconds (
251                 COMPOSING_STOP_TIMEOUT,
252                 (GSourceFunc) chat_composing_stop_timeout_cb,
253                 chat);
254 }
255
256 static void
257 chat_composing_stop (EmpathyChat *chat)
258 {
259         EmpathyChatPriv *priv;
260
261         priv = GET_PRIV (chat);
262
263         chat_composing_remove_timeout (chat);
264         empathy_tp_chat_set_state (priv->tp_chat,
265                                    TP_CHANNEL_CHAT_STATE_ACTIVE);
266 }
267
268 static void
269 chat_destroy_cb (EmpathyTpChat *tp_chat,
270                  EmpathyChat    *chat)
271 {
272         EmpathyChatPriv *priv;
273
274         priv = GET_PRIV (chat);
275
276         if (priv->tp_chat) {
277                 g_object_unref (priv->tp_chat);
278                 priv->tp_chat = NULL;
279                 g_object_notify (G_OBJECT (chat), "tp-chat");
280         }
281
282         empathy_chat_view_append_event (chat->view, _("Disconnected"));
283         gtk_widget_set_sensitive (chat->input_text_view, FALSE);
284 }
285
286 static void 
287 chat_sent_message_add (EmpathyChat  *chat,
288                        const gchar *str)
289 {
290         EmpathyChatPriv *priv;
291         GSList         *list;
292         GSList         *item;
293
294         priv = GET_PRIV (chat);
295
296         /* Save the sent message in our repeat buffer */
297         list = priv->sent_messages;
298         
299         /* Remove any other occurances of this msg */
300         while ((item = g_slist_find_custom (list, str, (GCompareFunc) strcmp)) != NULL) {
301                 list = g_slist_remove_link (list, item);
302                 g_free (item->data);
303                 g_slist_free1 (item);
304         }
305
306         /* Trim the list to the last 10 items */
307         while (g_slist_length (list) > 10) {
308                 item = g_slist_last (list);
309                 if (item) {
310                         list = g_slist_remove_link (list, item);
311                         g_free (item->data);
312                         g_slist_free1 (item);
313                 }
314         }
315
316         /* Add new message */
317         list = g_slist_prepend (list, g_strdup (str));
318
319         /* Set list and reset the index */
320         priv->sent_messages = list;
321         priv->sent_messages_index = -1;
322 }
323
324 static const gchar *
325 chat_sent_message_get_next (EmpathyChat *chat)
326 {
327         EmpathyChatPriv *priv;
328         gint            max;
329         
330         priv = GET_PRIV (chat);
331
332         if (!priv->sent_messages) {
333                 empathy_debug (DEBUG_DOMAIN, 
334                               "No sent messages, next message is NULL");
335                 return NULL;
336         }
337
338         max = g_slist_length (priv->sent_messages) - 1;
339
340         if (priv->sent_messages_index < max) {
341                 priv->sent_messages_index++;
342         }
343         
344         empathy_debug (DEBUG_DOMAIN, 
345                       "Returning next message index:%d",
346                       priv->sent_messages_index);
347
348         return g_slist_nth_data (priv->sent_messages, priv->sent_messages_index);
349 }
350
351 static const gchar *
352 chat_sent_message_get_last (EmpathyChat *chat)
353 {
354         EmpathyChatPriv *priv;
355
356         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
357
358         priv = GET_PRIV (chat);
359         
360         if (!priv->sent_messages) {
361                 empathy_debug (DEBUG_DOMAIN, 
362                               "No sent messages, last message is NULL");
363                 return NULL;
364         }
365
366         if (priv->sent_messages_index >= 0) {
367                 priv->sent_messages_index--;
368         }
369
370         empathy_debug (DEBUG_DOMAIN, 
371                       "Returning last message index:%d",
372                       priv->sent_messages_index);
373
374         return g_slist_nth_data (priv->sent_messages, priv->sent_messages_index);
375 }
376
377 static void
378 chat_send (EmpathyChat  *chat,
379            const gchar *msg)
380 {
381         EmpathyChatPriv *priv;
382         EmpathyMessage  *message;
383
384         priv = GET_PRIV (chat);
385
386         if (G_STR_EMPTY (msg)) {
387                 return;
388         }
389
390         chat_sent_message_add (chat, msg);
391
392         if (g_str_has_prefix (msg, "/clear")) {
393                 empathy_chat_view_clear (chat->view);
394                 return;
395         }
396
397         message = empathy_message_new (msg);
398
399         empathy_tp_chat_send (priv->tp_chat, message);
400
401         g_object_unref (message);
402 }
403
404 static void
405 chat_input_text_view_send (EmpathyChat *chat)
406 {
407         EmpathyChatPriv *priv;
408         GtkTextBuffer  *buffer;
409         GtkTextIter     start, end;
410         gchar          *msg;
411
412         priv = GET_PRIV (chat);
413
414         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
415
416         gtk_text_buffer_get_bounds (buffer, &start, &end);
417         msg = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
418
419         /* clear the input field */
420         gtk_text_buffer_set_text (buffer, "", -1);
421
422         chat_send (chat, msg);
423         g_free (msg);
424
425         priv->is_first_char = TRUE;
426 }
427
428 static void
429 chat_state_changed_cb (EmpathyTpChat      *tp_chat,
430                        EmpathyContact     *contact,
431                        TpChannelChatState  state,
432                        EmpathyChat        *chat)
433 {
434         EmpathyChatPriv *priv;
435         GList          *l;
436         gboolean        was_composing;
437
438         priv = GET_PRIV (chat);
439
440         if (empathy_contact_is_user (contact)) {
441                 /* We don't care about our own chat state */
442                 return;
443         }
444
445         was_composing = (priv->compositors != NULL);
446
447         /* Find the contact in the list. After that l is the list elem or NULL */
448         for (l = priv->compositors; l; l = l->next) {
449                 if (contact == l->data) {
450                         break;
451                 }
452         }
453
454         switch (state) {
455         case TP_CHANNEL_CHAT_STATE_GONE:
456         case TP_CHANNEL_CHAT_STATE_INACTIVE:
457         case TP_CHANNEL_CHAT_STATE_PAUSED:
458         case TP_CHANNEL_CHAT_STATE_ACTIVE:
459                 /* Contact is not composing */
460                 if (l) {
461                         priv->compositors = g_list_remove_link (priv->compositors, l);
462                         g_object_unref (l->data);
463                         g_list_free1 (l);
464                 }
465                 break;
466         case TP_CHANNEL_CHAT_STATE_COMPOSING:
467                 /* Contact is composing */
468                 if (!l) {
469                         priv->compositors = g_list_prepend (priv->compositors,
470                                                             g_object_ref (contact));
471                 }
472                 break;
473         default:
474                 g_assert_not_reached ();
475         }
476
477         empathy_debug (DEBUG_DOMAIN, "Was composing: %s now composing: %s",
478                       was_composing ? "yes" : "no",
479                       priv->compositors ? "yes" : "no");
480
481         if ((was_composing && !priv->compositors) ||
482             (!was_composing && priv->compositors)) {
483                 /* Composing state changed */
484                 g_signal_emit (chat, signals[COMPOSING], 0,
485                                priv->compositors != NULL);
486         }
487 }
488
489 static void
490 chat_message_received_cb (EmpathyTpChat  *tp_chat,
491                           EmpathyMessage *message,
492                           EmpathyChat    *chat)
493 {
494         EmpathyChatPriv *priv;
495         EmpathyContact  *sender;
496         const gchar     *body;
497
498         priv = GET_PRIV (chat);
499
500         sender = empathy_message_get_sender (message);
501         body = empathy_message_get_body (message);
502         while (priv->backlog_messages) {
503                 EmpathyMessage *log_message;
504                 EmpathyContact *log_sender;
505                 const gchar    *log_body;
506
507                 log_message = priv->backlog_messages->data;
508                 log_sender = empathy_message_get_sender (log_message);
509                 log_body = empathy_message_get_body (log_message);
510
511                 priv->backlog_messages = g_list_remove (priv->backlog_messages,
512                                                         log_message);
513
514                 if (empathy_contact_equal (sender, log_sender) &&
515                     !tp_strdiff (body, log_body)) {
516                         /* The message we received is already displayed because
517                          * some jabber chatrooms sends us back logs and we
518                          * already displayed it from localy logged messages. */
519                         empathy_debug (DEBUG_DOMAIN, "Skipping message because "
520                                        "it is already displayed from logged "
521                                        "messages");
522                         g_object_unref (log_message);
523                         return;
524                 }
525                 g_object_unref (log_message);
526         }
527
528         empathy_debug (DEBUG_DOMAIN, "Appending new message from %s (%d)",
529                        empathy_contact_get_name (sender),
530                        empathy_contact_get_handle (sender));
531
532         empathy_log_manager_add_message (priv->log_manager,
533                                          empathy_chat_get_id (chat),
534                                          FALSE,
535                                          message);
536
537         empathy_chat_view_append_message (chat->view, message);
538
539         /* We received a message so the contact is no more composing */
540         chat_state_changed_cb (tp_chat, sender,
541                                TP_CHANNEL_CHAT_STATE_ACTIVE,
542                                chat);
543
544         g_signal_emit (chat, signals[NEW_MESSAGE], 0, message);
545 }
546
547 static void
548 chat_send_error_cb (EmpathyTpChat          *tp_chat,
549                     EmpathyMessage         *message,
550                     TpChannelTextSendError  error_code,
551                     EmpathyChat            *chat)
552 {
553         const gchar *error;
554         gchar       *str;
555
556         switch (error_code) {
557         case TP_CHANNEL_TEXT_SEND_ERROR_OFFLINE:
558                 error = _("offline");
559                 break;
560         case TP_CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT:
561                 error = _("invalid contact");
562                 break;
563         case TP_CHANNEL_TEXT_SEND_ERROR_PERMISSION_DENIED:
564                 error = _("permission denied");
565                 break;
566         case TP_CHANNEL_TEXT_SEND_ERROR_TOO_LONG:
567                 error = _("too long message");
568                 break;
569         case TP_CHANNEL_TEXT_SEND_ERROR_NOT_IMPLEMENTED:
570                 error = _("not implemented");
571                 break;
572         default:
573                 error = _("unknown");
574                 break;
575         }
576
577         str = g_strdup_printf (_("Error sending message '%s': %s"),
578                                empathy_message_get_body (message),
579                                error);
580         empathy_chat_view_append_event (chat->view, str);
581         g_free (str);
582 }
583
584 static void
585 chat_property_changed_cb (EmpathyTpChat *tp_chat,
586                           const gchar   *name,
587                           GValue        *value,
588                           EmpathyChat   *chat)
589 {
590         EmpathyChatPriv *priv = GET_PRIV (chat);
591
592         if (!tp_strdiff (name, "subject")) {
593                 g_free (priv->subject);
594                 priv->subject = g_value_dup_string (value);
595                 g_object_notify (G_OBJECT (chat), "subject");
596
597                 if (G_STR_EMPTY (priv->subject)) {
598                         gtk_widget_hide (priv->hbox_topic);
599                 } else {
600                         gtk_label_set_text (GTK_LABEL (priv->label_topic), priv->subject);
601                         gtk_widget_show (priv->hbox_topic);
602                 }
603                 if (priv->block_events_timeout_id == 0) {
604                         gchar *str;
605
606                         if (!G_STR_EMPTY (priv->subject)) {
607                                 str = g_strdup_printf (_("Topic set to: %s"), priv->subject);
608                         } else {
609                                 str = g_strdup (_("No topic defined"));
610                         }
611                         empathy_chat_view_append_event (EMPATHY_CHAT (chat)->view, str);
612                         g_free (str);
613                 }
614         }
615         else if (!tp_strdiff (name, "name")) {
616                 g_free (priv->name);
617                 priv->name = g_value_dup_string (value);
618                 g_object_notify (G_OBJECT (chat), "name");
619         }
620 }
621
622 static gboolean
623 chat_get_is_command (const gchar *str)
624 {
625         g_return_val_if_fail (str != NULL, FALSE);
626
627         if (str[0] != '/') {
628                 return FALSE;
629         }
630
631         if (g_str_has_prefix (str, "/me")) {
632                 return TRUE;
633         }
634         else if (g_str_has_prefix (str, "/nick")) {
635                 return TRUE;
636         }
637         else if (g_str_has_prefix (str, "/topic")) {
638                 return TRUE;
639         }
640
641         return FALSE;
642 }
643
644 static void
645 chat_input_text_buffer_changed_cb (GtkTextBuffer *buffer,
646                                    EmpathyChat    *chat)
647 {
648         EmpathyChatPriv *priv;
649         GtkTextIter     start, end;
650         gchar          *str;
651         gboolean        spell_checker = FALSE;
652
653         priv = GET_PRIV (chat);
654
655         if (gtk_text_buffer_get_char_count (buffer) == 0) {
656                 chat_composing_stop (chat);
657         } else {
658                 chat_composing_start (chat);
659         }
660
661         empathy_conf_get_bool (empathy_conf_get (),
662                               EMPATHY_PREFS_CHAT_SPELL_CHECKER_ENABLED,
663                               &spell_checker);
664
665         if (priv->is_first_char) {
666                 GtkRequisition  req;
667                 gint            window_height;
668                 GtkWindow      *dialog;
669                 GtkAllocation  *allocation;
670
671                 /* Save the window's size */
672                 dialog = empathy_get_toplevel_window (GTK_WIDGET (chat));
673                 if (dialog) {
674                         gtk_window_get_size (GTK_WINDOW (dialog), NULL, &window_height);
675                         gtk_widget_size_request (chat->input_text_view, &req);
676                         allocation = &GTK_WIDGET (chat->view)->allocation;
677
678                         priv->default_window_height = window_height;
679                         priv->last_input_height = req.height;
680                         priv->padding_height = window_height - req.height - allocation->height;
681                 }
682
683                 priv->is_first_char = FALSE;
684         }
685
686         gtk_text_buffer_get_start_iter (buffer, &start);
687
688         if (!spell_checker) {
689                 gtk_text_buffer_get_end_iter (buffer, &end);
690                 gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
691                 return;
692         }
693
694         if (!empathy_spell_supported ()) {
695                 return;
696         }
697
698         /* NOTE: this is really inefficient, we shouldn't have to
699            reiterate the whole buffer each time and check each work
700            every time. */
701         while (TRUE) {
702                 gboolean correct = FALSE;
703
704                 /* if at start */
705                 if (gtk_text_iter_is_start (&start)) {
706                         end = start;
707
708                         if (!gtk_text_iter_forward_word_end (&end)) {
709                                 /* no whole word yet */
710                                 break;
711                         }
712                 } else {
713                         if (!gtk_text_iter_forward_word_end (&end)) {
714                                 /* must be the end of the buffer */
715                                 break;
716                         }
717
718                         start = end;
719                         gtk_text_iter_backward_word_start (&start);
720                 }
721
722                 str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
723
724                 /* spell check string */
725                 if (!chat_get_is_command (str)) {
726                         correct = empathy_spell_check (str);
727                 } else {
728                         correct = TRUE;
729                 }
730
731                 if (!correct) {
732                         gtk_text_buffer_apply_tag_by_name (buffer, "misspelled", &start, &end);
733                 } else {
734                         gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
735                 }
736
737                 g_free (str);
738
739                 /* set start iter to the end iters position */
740                 start = end;
741         }
742 }
743
744 static gboolean
745 chat_input_key_press_event_cb (GtkWidget   *widget,
746                                GdkEventKey *event,
747                                EmpathyChat *chat)
748 {
749         EmpathyChatPriv *priv;
750         GtkAdjustment  *adj;
751         gdouble         val;
752         GtkWidget      *text_view_sw;
753
754         priv = GET_PRIV (chat);
755
756         /* Catch ctrl+up/down so we can traverse messages we sent */
757         if ((event->state & GDK_CONTROL_MASK) && 
758             (event->keyval == GDK_Up || 
759              event->keyval == GDK_Down)) {
760                 GtkTextBuffer *buffer;
761                 const gchar   *str;
762
763                 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
764
765                 if (event->keyval == GDK_Up) {
766                         str = chat_sent_message_get_next (chat);
767                 } else {
768                         str = chat_sent_message_get_last (chat);
769                 }
770
771                 g_signal_handlers_block_by_func (buffer, 
772                                                  chat_input_text_buffer_changed_cb,
773                                                  chat);
774                 gtk_text_buffer_set_text (buffer, str ? str : "", -1);
775                 g_signal_handlers_unblock_by_func (buffer, 
776                                                    chat_input_text_buffer_changed_cb,
777                                                    chat);
778
779                 return TRUE;    
780         }
781
782         /* Catch enter but not ctrl/shift-enter */
783         if (IS_ENTER (event->keyval) &&
784             !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
785                 GtkTextView *view;
786
787                 /* This is to make sure that kinput2 gets the enter. And if
788                  * it's handled there we shouldn't send on it. This is because
789                  * kinput2 uses Enter to commit letters. See:
790                  * http://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=104299
791                  */
792
793                 view = GTK_TEXT_VIEW (chat->input_text_view);
794                 if (gtk_im_context_filter_keypress (view->im_context, event)) {
795                         GTK_TEXT_VIEW (chat->input_text_view)->need_im_reset = TRUE;
796                         return TRUE;
797                 }
798
799                 chat_input_text_view_send (chat);
800                 return TRUE;
801         }
802
803         text_view_sw = gtk_widget_get_parent (GTK_WIDGET (chat->view));
804
805         if (IS_ENTER (event->keyval) &&
806             (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
807                 /* Newline for shift/control-enter. */
808                 return FALSE;
809         }
810         if (!(event->state & GDK_CONTROL_MASK) &&
811             event->keyval == GDK_Page_Up) {
812                 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
813                 gtk_adjustment_set_value (adj, adj->value - adj->page_size);
814                 return TRUE;
815         }
816         if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK &&
817             event->keyval == GDK_Page_Down) {
818                 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
819                 val = MIN (adj->value + adj->page_size, adj->upper - adj->page_size);
820                 gtk_adjustment_set_value (adj, val);
821                 return TRUE;
822         }
823         if (!(event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) &&
824             event->keyval == GDK_Tab) {
825                 GtkTextBuffer *buffer;
826                 GtkTextIter    start, current;
827                 gchar         *nick, *completed;
828                 GList         *list, *completed_list;
829                 gboolean       is_start_of_buffer;
830
831                 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (EMPATHY_CHAT (chat)->input_text_view));
832                 gtk_text_buffer_get_iter_at_mark (buffer, &current, gtk_text_buffer_get_insert (buffer));
833
834                 /* Get the start of the nick to complete. */
835                 gtk_text_buffer_get_iter_at_mark (buffer, &start, gtk_text_buffer_get_insert (buffer));
836                 gtk_text_iter_backward_word_start (&start);
837                 is_start_of_buffer = gtk_text_iter_is_start (&start);
838
839                 list = empathy_contact_list_get_members (EMPATHY_CONTACT_LIST (priv->tp_chat));
840                 g_completion_add_items (priv->completion, list);
841
842                 nick = gtk_text_buffer_get_text (buffer, &start, &current, FALSE);
843                 completed_list = g_completion_complete (priv->completion,
844                                                         nick,
845                                                         &completed);
846
847                 g_free (nick);
848
849                 if (completed) {
850                         guint        len;
851                         const gchar *text;
852                         gchar       *complete_char = NULL;
853
854                         gtk_text_buffer_delete (buffer, &start, &current);
855
856                         len = g_list_length (completed_list);
857
858                         if (len == 1) {
859                                 /* If we only have one hit, use that text
860                                  * instead of the text in completed since the
861                                  * completed text will use the typed string
862                                  * which might be cased all wrong.
863                                  * Fixes #120876
864                                  * */
865                                 text = empathy_contact_get_name (completed_list->data);
866                         } else {
867                                 text = completed;
868                         }
869
870                         gtk_text_buffer_insert_at_cursor (buffer, text, strlen (text));
871
872                         if (len == 1 && is_start_of_buffer &&
873                             empathy_conf_get_string (empathy_conf_get (),
874                                                      EMPATHY_PREFS_CHAT_NICK_COMPLETION_CHAR,
875                                                      &complete_char) &&
876                             complete_char != NULL) {
877                                 gtk_text_buffer_insert_at_cursor (buffer,
878                                                                   complete_char,
879                                                                   strlen (complete_char));
880                                 gtk_text_buffer_insert_at_cursor (buffer, " ", 1);
881                                 g_free (complete_char);
882                         }
883
884                         g_free (completed);
885                 }
886
887                 g_completion_clear_items (priv->completion);
888
889                 g_list_foreach (list, (GFunc) g_object_unref, NULL);
890                 g_list_free (list);
891
892                 return TRUE;
893         }
894
895         return FALSE;
896 }
897
898 static gboolean
899 chat_text_view_focus_in_event_cb (GtkWidget  *widget,
900                                   GdkEvent   *event,
901                                   EmpathyChat *chat)
902 {
903         gtk_widget_grab_focus (chat->input_text_view);
904
905         return TRUE;
906 }
907
908 typedef struct {
909         GtkWindow *window;
910         gint       width;
911         gint       height;
912 } ChangeSizeData;
913
914 static gboolean
915 chat_change_size_in_idle_cb (ChangeSizeData *data)
916 {
917         gtk_window_resize (data->window, data->width, data->height);
918
919         return FALSE;
920 }
921
922 static void
923 chat_text_view_scroll_hide_cb (GtkWidget  *widget,
924                                EmpathyChat *chat)
925 {
926         EmpathyChatPriv *priv;
927         GtkWidget      *sw;
928
929         priv = GET_PRIV (chat);
930
931         priv->vscroll_visible = FALSE;
932         g_signal_handlers_disconnect_by_func (widget,
933                                               chat_text_view_scroll_hide_cb,
934                                               chat);
935
936         sw = gtk_widget_get_parent (chat->input_text_view);
937         gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
938                                         GTK_POLICY_NEVER,
939                                         GTK_POLICY_NEVER);
940         g_object_set (sw, "height-request", -1, NULL);
941 }
942
943 static void
944 chat_text_view_size_allocate_cb (GtkWidget     *widget,
945                                  GtkAllocation *allocation,
946                                  EmpathyChat    *chat)
947 {
948         EmpathyChatPriv *priv;
949         gint            width;
950         GtkWindow      *dialog;
951         ChangeSizeData *data;
952         gint            window_height;
953         gint            new_height;
954         GtkAllocation  *view_allocation;
955         gint            current_height;
956         gint            diff;
957         GtkWidget      *sw;
958
959         priv = GET_PRIV (chat);
960
961         if (priv->default_window_height <= 0) {
962                 return;
963         }
964
965         sw = gtk_widget_get_parent (widget);
966         if (sw->allocation.height >= MAX_INPUT_HEIGHT && !priv->vscroll_visible) {
967                 GtkWidget *vscroll;
968
969                 priv->vscroll_visible = TRUE;
970                 gtk_widget_set_size_request (sw, sw->allocation.width, MAX_INPUT_HEIGHT);
971                 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
972                                                 GTK_POLICY_NEVER,
973                                                 GTK_POLICY_AUTOMATIC);
974                 vscroll = gtk_scrolled_window_get_vscrollbar (GTK_SCROLLED_WINDOW (sw));
975                 g_signal_connect (vscroll, "hide",
976                                   G_CALLBACK (chat_text_view_scroll_hide_cb),
977                                   chat);
978         }
979
980         if (priv->last_input_height <= allocation->height) {
981                 priv->last_input_height = allocation->height;
982                 return;
983         }
984
985         diff = priv->last_input_height - allocation->height;
986         priv->last_input_height = allocation->height;
987
988         view_allocation = &GTK_WIDGET (chat->view)->allocation;
989
990         dialog = empathy_get_toplevel_window (GTK_WIDGET (widget));
991         gtk_window_get_size (dialog, NULL, &current_height);
992
993         new_height = view_allocation->height + priv->padding_height + allocation->height - diff;
994
995         if (new_height <= priv->default_window_height) {
996                 window_height = priv->default_window_height;
997         } else {
998                 window_height = new_height;
999         }
1000
1001         if (current_height <= window_height) {
1002                 return;
1003         }
1004
1005         /* Restore the window's size */
1006         gtk_window_get_size (dialog, &width, NULL);
1007
1008         data = g_new0 (ChangeSizeData, 1);
1009         data->window = dialog;
1010         data->width  = width;
1011         data->height = window_height;
1012
1013         g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
1014                          (GSourceFunc) chat_change_size_in_idle_cb,
1015                          data, g_free);
1016 }
1017
1018 static void
1019 chat_text_view_realize_cb (GtkWidget  *widget,
1020                            EmpathyChat *chat)
1021 {
1022         empathy_debug (DEBUG_DOMAIN, "Setting focus to the input text view");
1023         gtk_widget_grab_focus (widget);
1024 }
1025
1026 static void
1027 chat_insert_smiley_activate_cb (GtkWidget   *menuitem,
1028                                 EmpathyChat *chat)
1029 {
1030         GtkTextBuffer *buffer;
1031         GtkTextIter    iter;
1032         const gchar   *smiley;
1033
1034         smiley = g_object_get_data (G_OBJECT (menuitem), "smiley_text");
1035
1036         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1037
1038         gtk_text_buffer_get_end_iter (buffer, &iter);
1039         gtk_text_buffer_insert (buffer, &iter, smiley, -1);
1040
1041         gtk_text_buffer_get_end_iter (buffer, &iter);
1042         gtk_text_buffer_insert (buffer, &iter, " ", -1);
1043 }
1044
1045 typedef struct {
1046         EmpathyChat  *chat;
1047         gchar       *word;
1048
1049         GtkTextIter  start;
1050         GtkTextIter  end;
1051 } EmpathyChatSpell;
1052
1053 static EmpathyChatSpell *
1054 chat_spell_new (EmpathyChat  *chat,
1055                 const gchar *word,
1056                 GtkTextIter  start,
1057                 GtkTextIter  end)
1058 {
1059         EmpathyChatSpell *chat_spell;
1060
1061         chat_spell = g_slice_new0 (EmpathyChatSpell);
1062
1063         chat_spell->chat = g_object_ref (chat);
1064         chat_spell->word = g_strdup (word);
1065         chat_spell->start = start;
1066         chat_spell->end = end;
1067
1068         return chat_spell;
1069 }
1070
1071 static void
1072 chat_spell_free (EmpathyChatSpell *chat_spell)
1073 {
1074         g_object_unref (chat_spell->chat);
1075         g_free (chat_spell->word);
1076         g_slice_free (EmpathyChatSpell, chat_spell);
1077 }
1078
1079 static void
1080 chat_text_check_word_spelling_cb (GtkMenuItem     *menuitem,
1081                                   EmpathyChatSpell *chat_spell)
1082 {
1083         empathy_spell_dialog_show (chat_spell->chat,
1084                                   chat_spell->start,
1085                                   chat_spell->end,
1086                                   chat_spell->word);
1087 }
1088
1089 static void
1090 chat_text_populate_popup_cb (GtkTextView *view,
1091                              GtkMenu     *menu,
1092                              EmpathyChat  *chat)
1093 {
1094         EmpathyChatPriv  *priv;
1095         GtkTextBuffer   *buffer;
1096         GtkTextTagTable *table;
1097         GtkTextTag      *tag;
1098         gint             x, y;
1099         GtkTextIter      iter, start, end;
1100         GtkWidget       *item;
1101         gchar           *str = NULL;
1102         EmpathyChatSpell *chat_spell;
1103         GtkWidget       *smiley_menu;
1104
1105         priv = GET_PRIV (chat);
1106
1107         /* Add the emoticon menu. */
1108         item = gtk_separator_menu_item_new ();
1109         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1110         gtk_widget_show (item);
1111
1112         item = gtk_menu_item_new_with_mnemonic (_("Insert Smiley"));
1113         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1114         gtk_widget_show (item);
1115
1116         smiley_menu = empathy_chat_view_get_smiley_menu (
1117                 G_CALLBACK (chat_insert_smiley_activate_cb),
1118                 chat);
1119         gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), smiley_menu);
1120
1121         /* Add the spell check menu item. */
1122         buffer = gtk_text_view_get_buffer (view);
1123         table = gtk_text_buffer_get_tag_table (buffer);
1124
1125         tag = gtk_text_tag_table_lookup (table, "misspelled");
1126
1127         gtk_widget_get_pointer (GTK_WIDGET (view), &x, &y);
1128
1129         gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
1130                                                GTK_TEXT_WINDOW_WIDGET,
1131                                                x, y,
1132                                                &x, &y);
1133
1134         gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y);
1135
1136         start = end = iter;
1137
1138         if (gtk_text_iter_backward_to_tag_toggle (&start, tag) &&
1139             gtk_text_iter_forward_to_tag_toggle (&end, tag)) {
1140
1141                 str = gtk_text_buffer_get_text (buffer,
1142                                                 &start, &end, FALSE);
1143         }
1144
1145         if (G_STR_EMPTY (str)) {
1146                 return;
1147         }
1148
1149         chat_spell = chat_spell_new (chat, str, start, end);
1150
1151         g_object_set_data_full (G_OBJECT (menu),
1152                                 "chat_spell", chat_spell,
1153                                 (GDestroyNotify) chat_spell_free);
1154
1155         item = gtk_separator_menu_item_new ();
1156         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1157         gtk_widget_show (item);
1158
1159         item = gtk_menu_item_new_with_mnemonic (_("_Check Word Spelling..."));
1160         g_signal_connect (item,
1161                           "activate",
1162                           G_CALLBACK (chat_text_check_word_spelling_cb),
1163                           chat_spell);
1164         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1165         gtk_widget_show (item);
1166 }
1167
1168 static void
1169 chat_add_logs (EmpathyChat *chat)
1170 {
1171         EmpathyChatPriv *priv;
1172         GList          *messages, *l;
1173         guint           num_messages;
1174         guint           i;
1175
1176         priv = GET_PRIV (chat);
1177
1178         /* Turn off scrolling temporarily */
1179         empathy_chat_view_scroll (chat->view, FALSE);
1180
1181         /* Add messages from last conversation */
1182         messages = empathy_log_manager_get_last_messages (priv->log_manager,
1183                                                           priv->account,
1184                                                           empathy_chat_get_id (chat),
1185                                                           FALSE);
1186         num_messages  = g_list_length (messages);
1187
1188         /* Only keep the 10 last messages */
1189         for (i = 0; num_messages - i > 10; i++) {
1190                 EmpathyMessage *message;
1191
1192                 message = messages->data;
1193                 messages = g_list_remove (messages, message);
1194                 g_object_unref (message);
1195         }
1196
1197         for (l = messages; l; l = l->next) {
1198                 empathy_chat_view_append_message (chat->view, l->data);
1199         }
1200         priv->backlog_messages = messages;
1201
1202         /* Turn back on scrolling */
1203         empathy_chat_view_scroll (chat->view, TRUE);
1204 }
1205
1206 static gint
1207 chat_contacts_completion_func (const gchar *s1,
1208                                const gchar *s2,
1209                                gsize        n)
1210 {
1211         gchar *tmp, *nick1, *nick2;
1212         gint   ret;
1213
1214         if (s1 == s2) {
1215                 return 0;
1216         }
1217         if (!s1 || !s2) {
1218                 return s1 ? -1 : +1;
1219         }
1220
1221         tmp = g_utf8_normalize (s1, -1, G_NORMALIZE_DEFAULT);
1222         nick1 = g_utf8_casefold (tmp, -1);
1223         g_free (tmp);
1224
1225         tmp = g_utf8_normalize (s2, -1, G_NORMALIZE_DEFAULT);
1226         nick2 = g_utf8_casefold (tmp, -1);
1227         g_free (tmp);
1228
1229         ret = strncmp (nick1, nick2, n);
1230
1231         g_free (nick1);
1232         g_free (nick2);
1233
1234         return ret;
1235 }
1236
1237 static void
1238 chat_members_changed_cb (EmpathyTpChat  *tp_chat,
1239                          EmpathyContact *contact,
1240                          EmpathyContact *actor,
1241                          guint           reason,
1242                          gchar          *message,
1243                          gboolean        is_member,
1244                          EmpathyChat    *chat)
1245 {
1246         EmpathyChatPriv *priv = GET_PRIV (chat);
1247
1248         if (priv->block_events_timeout_id == 0) {
1249                 gchar *str;
1250
1251                 empathy_contact_run_until_ready (contact,
1252                                                  EMPATHY_CONTACT_READY_NAME,
1253                                                  NULL);
1254
1255                 if (is_member) {
1256                         str = g_strdup_printf (_("%s has joined the room"),
1257                                                empathy_contact_get_name (contact));
1258                 } else {
1259                         str = g_strdup_printf (_("%s has left the room"),
1260                                                empathy_contact_get_name (contact));
1261                 }
1262                 empathy_chat_view_append_event (chat->view, str);
1263                 g_free (str);
1264         }
1265 }
1266
1267 static void
1268 chat_set_show_contacts (EmpathyChat *chat, gboolean show)
1269 {
1270         EmpathyChatPriv *priv = GET_PRIV (chat);
1271
1272         if (!priv->scrolled_window_contacts ||
1273             GTK_WIDGET_VISIBLE (priv->scrolled_window_contacts) == show) {
1274                 return;
1275         }
1276
1277         if (show) {
1278                 gtk_widget_show (priv->scrolled_window_contacts);
1279                 gtk_paned_set_position (GTK_PANED (priv->hpaned),
1280                                         priv->contacts_width);
1281         } else {
1282                 priv->contacts_width = gtk_paned_get_position (GTK_PANED (priv->hpaned));
1283                 gtk_widget_hide (priv->scrolled_window_contacts);
1284         }
1285 }
1286
1287 static void
1288 chat_remote_contact_changed_cb (EmpathyChat *chat)
1289 {
1290         EmpathyChatPriv *priv = GET_PRIV (chat);
1291
1292         if (priv->remote_contact) {
1293                 g_object_unref (priv->remote_contact);
1294                 priv->remote_contact = NULL;
1295         }
1296
1297         priv->remote_contact = empathy_tp_chat_get_remote_contact (priv->tp_chat);
1298         if (priv->remote_contact) {
1299                 g_object_ref (priv->remote_contact);
1300         }
1301
1302         chat_set_show_contacts (chat, priv->remote_contact == NULL);
1303
1304         g_object_notify (G_OBJECT (chat), "remote-contact");
1305 }
1306
1307 static void
1308 chat_create_ui (EmpathyChat *chat)
1309 {
1310         EmpathyChatPriv *priv = GET_PRIV (chat);
1311         GladeXML        *glade;
1312         GList           *list = NULL; 
1313         gchar           *filename;
1314         GtkTextBuffer   *buffer;
1315
1316         filename = empathy_file_lookup ("empathy-chat.glade",
1317                                         "libempathy-gtk");
1318         glade = empathy_glade_get_file (filename,
1319                                         "chat_widget",
1320                                         NULL,
1321                                         "chat_widget", &priv->widget,
1322                                         "hpaned", &priv->hpaned,
1323                                         "vbox_left", &priv->vbox_left,
1324                                         "scrolled_window_chat", &priv->scrolled_window_chat,
1325                                         "scrolled_window_input", &priv->scrolled_window_input,
1326                                         "hbox_topic", &priv->hbox_topic,
1327                                         "label_topic", &priv->label_topic,
1328                                         "scrolled_window_contacts", &priv->scrolled_window_contacts,
1329                                         NULL);
1330         g_free (filename);
1331         g_object_unref (glade);
1332
1333         /* Add message GtkTextView. */
1334         chat->view = empathy_chat_view_new ();
1335         g_signal_connect (chat->view, "focus_in_event",
1336                           G_CALLBACK (chat_text_view_focus_in_event_cb),
1337                           chat);
1338         gtk_container_add (GTK_CONTAINER (priv->scrolled_window_chat),
1339                            GTK_WIDGET (chat->view));
1340         gtk_widget_show (GTK_WIDGET (chat->view));
1341
1342         /* Add input GtkTextView */
1343         chat->input_text_view = g_object_new (GTK_TYPE_TEXT_VIEW,
1344                                               "pixels-above-lines", 2,
1345                                               "pixels-below-lines", 2,
1346                                               "pixels-inside-wrap", 1,
1347                                               "right-margin", 2,
1348                                               "left-margin", 2,
1349                                               "wrap-mode", GTK_WRAP_WORD_CHAR,
1350                                               NULL);
1351         g_signal_connect (chat->input_text_view, "key_press_event",
1352                           G_CALLBACK (chat_input_key_press_event_cb),
1353                           chat);
1354         g_signal_connect (chat->input_text_view, "size_allocate",
1355                           G_CALLBACK (chat_text_view_size_allocate_cb),
1356                           chat);
1357         g_signal_connect (chat->input_text_view, "realize",
1358                           G_CALLBACK (chat_text_view_realize_cb),
1359                           chat);
1360         g_signal_connect (chat->input_text_view, "populate_popup",
1361                           G_CALLBACK (chat_text_populate_popup_cb),
1362                           chat);
1363         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1364         g_signal_connect (buffer, "changed",
1365                           G_CALLBACK (chat_input_text_buffer_changed_cb),
1366                           chat);
1367         gtk_text_buffer_create_tag (buffer, "misspelled",
1368                                     "underline", PANGO_UNDERLINE_ERROR,
1369                                     NULL);
1370         gtk_container_add (GTK_CONTAINER (priv->scrolled_window_input),
1371                            chat->input_text_view);
1372         gtk_widget_show (chat->input_text_view);
1373
1374         /* Create contact list */
1375         priv->store = empathy_contact_list_store_new (EMPATHY_CONTACT_LIST (priv->tp_chat));
1376         priv->view = empathy_contact_list_view_new (priv->store,
1377                                                     EMPATHY_CONTACT_LIST_FEATURE_CONTACT_CHAT |
1378                                                     EMPATHY_CONTACT_LIST_FEATURE_CONTACT_CALL |
1379                                                     EMPATHY_CONTACT_LIST_FEATURE_CONTACT_LOG |
1380                                                     EMPATHY_CONTACT_LIST_FEATURE_CONTACT_FT |
1381                                                     EMPATHY_CONTACT_LIST_FEATURE_CONTACT_INVITE |
1382                                                     EMPATHY_CONTACT_LIST_FEATURE_CONTACT_INFO);
1383         gtk_container_add (GTK_CONTAINER (priv->scrolled_window_contacts),
1384                            GTK_WIDGET (priv->view));
1385         gtk_widget_show (GTK_WIDGET (priv->view));
1386
1387         /* Initialy hide the topic, will be shown if not empty */
1388         gtk_widget_hide (priv->hbox_topic);
1389
1390         /* Show/Hide contact list */
1391         chat_set_show_contacts (chat, priv->remote_contact == NULL);
1392
1393         /* Set widget focus order */
1394         list = g_list_append (NULL, priv->scrolled_window_input);
1395         gtk_container_set_focus_chain (GTK_CONTAINER (priv->vbox_left), list);
1396         g_list_free (list);
1397
1398         list = g_list_append (NULL, priv->vbox_left);
1399         list = g_list_append (list, priv->scrolled_window_contacts);
1400         gtk_container_set_focus_chain (GTK_CONTAINER (priv->hpaned), list);
1401         g_list_free (list);
1402
1403         list = g_list_append (NULL, priv->hpaned);
1404         list = g_list_append (list, priv->hbox_topic);
1405         gtk_container_set_focus_chain (GTK_CONTAINER (priv->widget), list);
1406         g_list_free (list);
1407
1408         /* Add the main widget in the chat widget */
1409         gtk_container_add (GTK_CONTAINER (chat), priv->widget);
1410 }
1411
1412 static void
1413 chat_size_request (GtkWidget      *widget,
1414                    GtkRequisition *requisition)
1415 {
1416   GtkBin *bin = GTK_BIN (widget);
1417
1418   requisition->width = GTK_CONTAINER (widget)->border_width * 2;
1419   requisition->height = GTK_CONTAINER (widget)->border_width * 2;
1420
1421   if (bin->child && GTK_WIDGET_VISIBLE (bin->child))
1422     {
1423       GtkRequisition child_requisition;
1424       
1425       gtk_widget_size_request (bin->child, &child_requisition);
1426
1427       requisition->width += child_requisition.width;
1428       requisition->height += child_requisition.height;
1429     }
1430 }
1431
1432 static void
1433 chat_size_allocate (GtkWidget     *widget,
1434                     GtkAllocation *allocation)
1435 {
1436   GtkBin *bin = GTK_BIN (widget);
1437   GtkAllocation child_allocation;
1438   
1439   widget->allocation = *allocation;
1440
1441   if (bin->child && GTK_WIDGET_VISIBLE (bin->child))
1442     {
1443       child_allocation.x = allocation->x + GTK_CONTAINER (widget)->border_width;
1444       child_allocation.y = allocation->y + GTK_CONTAINER (widget)->border_width;
1445       child_allocation.width = MAX (allocation->width - GTK_CONTAINER (widget)->border_width * 2, 0);
1446       child_allocation.height = MAX (allocation->height - GTK_CONTAINER (widget)->border_width * 2, 0);
1447
1448       gtk_widget_size_allocate (bin->child, &child_allocation);
1449     }
1450 }
1451
1452 static void
1453 chat_finalize (GObject *object)
1454 {
1455         EmpathyChat     *chat;
1456         EmpathyChatPriv *priv;
1457
1458         chat = EMPATHY_CHAT (object);
1459         priv = GET_PRIV (chat);
1460
1461         empathy_debug (DEBUG_DOMAIN, "Finalized: %p", object);
1462
1463         g_slist_foreach (priv->sent_messages, (GFunc) g_free, NULL);
1464         g_slist_free (priv->sent_messages);
1465
1466         g_list_foreach (priv->compositors, (GFunc) g_object_unref, NULL);
1467         g_list_free (priv->compositors);
1468
1469         g_list_foreach (priv->backlog_messages, (GFunc) g_object_unref, NULL);
1470         g_list_free (priv->backlog_messages);
1471
1472         chat_composing_remove_timeout (chat);
1473
1474         empathy_disconnect_account_status_changed (priv->token);
1475         g_object_unref (priv->mc);
1476         g_object_unref (priv->log_manager);
1477         g_object_unref (priv->store);
1478
1479         if (priv->tp_chat) {
1480                 g_object_unref (priv->tp_chat);
1481         }
1482         if (priv->account) {
1483                 g_object_unref (priv->account);
1484         }
1485         if (priv->remote_contact) {
1486                 g_object_unref (priv->remote_contact);
1487         }
1488
1489         if (priv->block_events_timeout_id) {
1490                 g_source_remove (priv->block_events_timeout_id);
1491         }
1492
1493         g_free (priv->id);
1494         g_free (priv->name);
1495         g_free (priv->subject);
1496
1497         G_OBJECT_CLASS (empathy_chat_parent_class)->finalize (object);
1498 }
1499
1500 static void
1501 chat_constructed (GObject *object)
1502 {
1503         EmpathyChat *chat = EMPATHY_CHAT (object);
1504
1505         chat_create_ui (chat);
1506         chat_add_logs (chat);
1507 }
1508
1509 static void
1510 empathy_chat_class_init (EmpathyChatClass *klass)
1511 {
1512         GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1513         GObjectClass   *object_class = G_OBJECT_CLASS (klass);
1514
1515         object_class->finalize = chat_finalize;
1516         object_class->get_property = chat_get_property;
1517         object_class->set_property = chat_set_property;
1518         object_class->constructed = chat_constructed;
1519
1520         widget_class->size_request = chat_size_request;
1521         widget_class->size_allocate = chat_size_allocate;
1522
1523         g_object_class_install_property (object_class,
1524                                          PROP_TP_CHAT,
1525                                          g_param_spec_object ("tp-chat",
1526                                                               "Empathy tp chat",
1527                                                               "The tp chat object",
1528                                                               EMPATHY_TYPE_TP_CHAT,
1529                                                               G_PARAM_CONSTRUCT |
1530                                                               G_PARAM_READWRITE));
1531         g_object_class_install_property (object_class,
1532                                          PROP_ACCOUNT,
1533                                          g_param_spec_object ("account",
1534                                                               "Account of the chat",
1535                                                               "The account of the chat",
1536                                                               MC_TYPE_ACCOUNT,
1537                                                               G_PARAM_READABLE));
1538         g_object_class_install_property (object_class,
1539                                          PROP_ID,
1540                                          g_param_spec_string ("id",
1541                                                               "Chat's id",
1542                                                               "The id of the chat",
1543                                                               NULL,
1544                                                               G_PARAM_READABLE));
1545         g_object_class_install_property (object_class,
1546                                          PROP_NAME,
1547                                          g_param_spec_string ("name",
1548                                                               "Chat's name",
1549                                                               "The name of the chat",
1550                                                               NULL,
1551                                                               G_PARAM_READABLE));
1552         g_object_class_install_property (object_class,
1553                                          PROP_SUBJECT,
1554                                          g_param_spec_string ("subject",
1555                                                               "Chat's subject",
1556                                                               "The subject or topic of the chat",
1557                                                               NULL,
1558                                                               G_PARAM_READABLE));
1559         g_object_class_install_property (object_class,
1560                                          PROP_REMOTE_CONTACT,
1561                                          g_param_spec_object ("remote-contact",
1562                                                               "The remote contact",
1563                                                               "The remote contact is any",
1564                                                               EMPATHY_TYPE_CONTACT,
1565                                                               G_PARAM_READABLE));
1566
1567         signals[COMPOSING] =
1568                 g_signal_new ("composing",
1569                               G_OBJECT_CLASS_TYPE (object_class),
1570                               G_SIGNAL_RUN_LAST,
1571                               0,
1572                               NULL, NULL,
1573                               g_cclosure_marshal_VOID__BOOLEAN,
1574                               G_TYPE_NONE,
1575                               1, G_TYPE_BOOLEAN);
1576
1577         signals[NEW_MESSAGE] =
1578                 g_signal_new ("new-message",
1579                               G_OBJECT_CLASS_TYPE (object_class),
1580                               G_SIGNAL_RUN_LAST,
1581                               0,
1582                               NULL, NULL,
1583                               g_cclosure_marshal_VOID__OBJECT,
1584                               G_TYPE_NONE,
1585                               1, EMPATHY_TYPE_MESSAGE);
1586
1587         g_type_class_add_private (object_class, sizeof (EmpathyChatPriv));
1588 }
1589
1590 static gboolean
1591 chat_block_events_timeout_cb (gpointer data)
1592 {
1593         EmpathyChatPriv *priv = GET_PRIV (data);
1594
1595         priv->block_events_timeout_id = 0;
1596
1597         return FALSE;
1598 }
1599
1600 static void
1601 empathy_chat_init (EmpathyChat *chat)
1602 {
1603         EmpathyChatPriv *priv = GET_PRIV (chat);
1604
1605         priv->is_first_char = TRUE;
1606         priv->log_manager = empathy_log_manager_new ();
1607         priv->default_window_height = -1;
1608         priv->vscroll_visible = FALSE;
1609         priv->sent_messages = NULL;
1610         priv->sent_messages_index = -1;
1611         priv->mc = empathy_mission_control_new ();
1612
1613         priv->token = empathy_connect_to_account_status_changed (priv->mc,
1614                                                    G_CALLBACK (chat_status_changed_cb),
1615                                                    chat, NULL);
1616
1617         /* Block events for some time to avoid having "has come online" or
1618          * "joined" messages. */
1619         priv->block_events_timeout_id =
1620                 g_timeout_add_seconds (1, chat_block_events_timeout_cb, chat);
1621
1622         /* Add nick name completion */
1623         priv->completion = g_completion_new ((GCompletionFunc) empathy_contact_get_name);
1624         g_completion_set_compare (priv->completion, chat_contacts_completion_func);
1625 }
1626
1627 EmpathyChat *
1628 empathy_chat_new (EmpathyTpChat *tp_chat)
1629 {
1630         return g_object_new (EMPATHY_TYPE_CHAT, "tp-chat", tp_chat, NULL);
1631 }
1632
1633 EmpathyTpChat *
1634 empathy_chat_get_tp_chat (EmpathyChat *chat)
1635 {
1636         EmpathyChatPriv *priv = GET_PRIV (chat);
1637
1638         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
1639
1640         return priv->tp_chat;
1641 }
1642
1643 void
1644 empathy_chat_set_tp_chat (EmpathyChat   *chat,
1645                           EmpathyTpChat *tp_chat)
1646 {
1647         EmpathyChatPriv *priv;
1648
1649         g_return_if_fail (EMPATHY_IS_CHAT (chat));
1650         g_return_if_fail (EMPATHY_IS_TP_CHAT (tp_chat));
1651         g_return_if_fail (empathy_tp_chat_is_ready (tp_chat));
1652
1653         priv = GET_PRIV (chat);
1654
1655         if (tp_chat == priv->tp_chat) {
1656                 return;
1657         }
1658
1659         if (priv->tp_chat) {
1660                 g_object_unref (priv->tp_chat);
1661         }
1662         if (priv->account) {
1663                 g_object_unref (priv->account);
1664         }
1665         g_free (priv->id);
1666
1667         priv->tp_chat = g_object_ref (tp_chat);
1668         priv->id = g_strdup (empathy_tp_chat_get_id (tp_chat));
1669         priv->account = g_object_ref (empathy_tp_chat_get_account (tp_chat));
1670
1671         g_signal_connect (tp_chat, "message-received",
1672                           G_CALLBACK (chat_message_received_cb),
1673                           chat);
1674         g_signal_connect (tp_chat, "send-error",
1675                           G_CALLBACK (chat_send_error_cb),
1676                           chat);
1677         g_signal_connect (tp_chat, "chat-state-changed",
1678                           G_CALLBACK (chat_state_changed_cb),
1679                           chat);
1680         g_signal_connect (tp_chat, "property-changed",
1681                           G_CALLBACK (chat_property_changed_cb),
1682                           chat);
1683         g_signal_connect (tp_chat, "members-changed",
1684                           G_CALLBACK (chat_members_changed_cb),
1685                           chat);
1686         g_signal_connect_swapped (tp_chat, "notify::remote-contact",
1687                                   G_CALLBACK (chat_remote_contact_changed_cb),
1688                                   chat);
1689         g_signal_connect (tp_chat, "destroy",
1690                           G_CALLBACK (chat_destroy_cb),
1691                           chat);
1692
1693         chat_remote_contact_changed_cb (chat);
1694
1695         if (chat->input_text_view) {
1696                 gtk_widget_set_sensitive (chat->input_text_view, TRUE);
1697                 if (priv->block_events_timeout_id == 0) {
1698                         empathy_chat_view_append_event (chat->view, _("Connected"));
1699                 }
1700         }
1701
1702         g_object_notify (G_OBJECT (chat), "tp-chat");
1703         g_object_notify (G_OBJECT (chat), "id");
1704         g_object_notify (G_OBJECT (chat), "account");
1705 }
1706
1707 McAccount *
1708 empathy_chat_get_account (EmpathyChat *chat)
1709 {
1710         EmpathyChatPriv *priv = GET_PRIV (chat);
1711
1712         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
1713
1714         return priv->account;
1715 }
1716
1717 const gchar *
1718 empathy_chat_get_id (EmpathyChat *chat)
1719 {
1720         EmpathyChatPriv *priv = GET_PRIV (chat);
1721
1722         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
1723
1724         return priv->id;
1725 }
1726
1727 const gchar *
1728 empathy_chat_get_name (EmpathyChat *chat)
1729 {
1730         EmpathyChatPriv *priv = GET_PRIV (chat);
1731
1732         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
1733
1734         return priv->name;
1735 }
1736
1737 const gchar *
1738 empathy_chat_get_subject (EmpathyChat *chat)
1739 {
1740         EmpathyChatPriv *priv = GET_PRIV (chat);
1741
1742         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
1743
1744         return priv->subject;
1745 }
1746
1747 EmpathyContact *
1748 empathy_chat_get_remote_contact (EmpathyChat *chat)
1749 {
1750         EmpathyChatPriv *priv = GET_PRIV (chat);
1751
1752         g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
1753
1754         return priv->remote_contact;
1755 }
1756
1757 void
1758 empathy_chat_clear (EmpathyChat *chat)
1759 {
1760         g_return_if_fail (EMPATHY_IS_CHAT (chat));
1761
1762         empathy_chat_view_clear (chat->view);
1763 }
1764
1765 void
1766 empathy_chat_scroll_down (EmpathyChat *chat)
1767 {
1768         g_return_if_fail (EMPATHY_IS_CHAT (chat));
1769
1770         empathy_chat_view_scroll_down (chat->view);
1771 }
1772
1773 void
1774 empathy_chat_cut (EmpathyChat *chat)
1775 {
1776         GtkTextBuffer *buffer;
1777
1778         g_return_if_fail (EMPATHY_IS_CHAT (chat));
1779
1780         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1781         if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) {
1782                 GtkClipboard *clipboard;
1783
1784                 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1785
1786                 gtk_text_buffer_cut_clipboard (buffer, clipboard, TRUE);
1787         }
1788 }
1789
1790 void
1791 empathy_chat_copy (EmpathyChat *chat)
1792 {
1793         GtkTextBuffer *buffer;
1794
1795         g_return_if_fail (EMPATHY_IS_CHAT (chat));
1796
1797         if (empathy_chat_view_get_selection_bounds (chat->view, NULL, NULL)) {
1798                 empathy_chat_view_copy_clipboard (chat->view);
1799                 return;
1800         }
1801
1802         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1803         if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) {
1804                 GtkClipboard *clipboard;
1805
1806                 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1807
1808                 gtk_text_buffer_copy_clipboard (buffer, clipboard);
1809         }
1810 }
1811
1812 void
1813 empathy_chat_paste (EmpathyChat *chat)
1814 {
1815         GtkTextBuffer *buffer;
1816         GtkClipboard  *clipboard;
1817
1818         g_return_if_fail (EMPATHY_IS_CHAT (chat));
1819
1820         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1821         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1822
1823         gtk_text_buffer_paste_clipboard (buffer, clipboard, NULL, TRUE);
1824 }
1825
1826 void
1827 empathy_chat_correct_word (EmpathyChat  *chat,
1828                           GtkTextIter  start,
1829                           GtkTextIter  end,
1830                           const gchar *new_word)
1831 {
1832         GtkTextBuffer *buffer;
1833
1834         g_return_if_fail (chat != NULL);
1835         g_return_if_fail (new_word != NULL);
1836
1837         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1838
1839         gtk_text_buffer_delete (buffer, &start, &end);
1840         gtk_text_buffer_insert (buffer, &start,
1841                                 new_word,
1842                                 -1);
1843 }
1844