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