f67c029be482ac4cf695d05f442d9cd97e0dd144
[empathy.git] / libempathy-gtk / gossip-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 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 #include <glade/glade.h>
37
38 #include <libempathy/empathy-session.h>
39 #include <libempathy/empathy-contact-manager.h>
40 #include <libempathy/gossip-debug.h>
41 #include <libempathy/gossip-utils.h>
42 #include <libempathy/gossip-conf.h>
43
44 #include "gossip-chat.h"
45 #include "gossip-chat-window.h"
46 #include "gossip-geometry.h"
47 #include "gossip-preferences.h"
48 #include "gossip-spell.h"
49 //#include "gossip-spell-dialog.h"
50 #include "gossip-ui-utils.h"
51
52 #define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CHAT, GossipChatPriv))
53
54 #define DEBUG_DOMAIN "Chat"
55
56 #define CHAT_DIR_CREATE_MODE  (S_IRUSR | S_IWUSR | S_IXUSR)
57 #define CHAT_FILE_CREATE_MODE (S_IRUSR | S_IWUSR)
58
59 #define IS_ENTER(v) (v == GDK_Return || v == GDK_ISO_Enter || v == GDK_KP_Enter)
60
61 #define MAX_INPUT_HEIGHT 150
62
63 #define COMPOSING_STOP_TIMEOUT 5
64
65 struct _GossipChatPriv {
66         EmpathyTpChat    *tp_chat;
67         GossipChatWindow *window;
68
69         GtkTooltips      *tooltips;
70         guint             composing_stop_timeout_id;
71         gboolean          sensitive;
72         gchar            *id;
73         /* Used to automatically shrink a window that has temporarily
74          * grown due to long input. 
75          */
76         gint              padding_height;
77         gint              default_window_height;
78         gint              last_input_height;
79         gboolean          vscroll_visible;
80 };
81
82 typedef struct {
83         GossipChat  *chat;
84         gchar       *word;
85
86         GtkTextIter  start;
87         GtkTextIter  end;
88 } GossipChatSpell;
89
90 static void             gossip_chat_class_init            (GossipChatClass *klass);
91 static void             gossip_chat_init                  (GossipChat      *chat);
92 static void             chat_finalize                     (GObject         *object);
93 static void             chat_destroy_cb                   (EmpathyTpChat   *tp_chat,
94                                                            GossipChat      *chat);
95 static void             chat_send                         (GossipChat      *chat,
96                                                            const gchar     *msg);
97 static void             chat_input_text_view_send         (GossipChat      *chat);
98 static void             chat_message_received_cb          (EmpathyTpChat   *tp_chat,
99                                                            GossipMessage   *message,
100                                                            GossipChat      *chat);
101 static gboolean         chat_input_key_press_event_cb     (GtkWidget       *widget,
102                                                            GdkEventKey     *event,
103                                                            GossipChat      *chat);
104 static void             chat_input_text_buffer_changed_cb (GtkTextBuffer   *buffer,
105                                                            GossipChat      *chat);
106 static gboolean         chat_text_view_focus_in_event_cb  (GtkWidget       *widget,
107                                                            GdkEvent        *event,
108                                                            GossipChat      *chat);
109 static void             chat_text_view_scroll_hide_cb     (GtkWidget       *widget,
110                                                            GossipChat      *chat);
111 static void             chat_text_view_size_allocate_cb   (GtkWidget       *widget,
112                                                            GtkAllocation   *allocation,
113                                                            GossipChat      *chat);
114 static void             chat_text_view_realize_cb         (GtkWidget       *widget,
115                                                            GossipChat      *chat);
116 static void             chat_text_populate_popup_cb       (GtkTextView     *view,
117                                                            GtkMenu         *menu,
118                                                            GossipChat      *chat);
119 static void             chat_text_check_word_spelling_cb  (GtkMenuItem     *menuitem,
120                                                            GossipChatSpell *chat_spell);
121 static GossipChatSpell *chat_spell_new                    (GossipChat      *chat,
122                                                            const gchar     *word,
123                                                            GtkTextIter      start,
124                                                            GtkTextIter      end);
125 static void             chat_spell_free                   (GossipChatSpell *chat_spell);
126 static void             chat_composing_start              (GossipChat      *chat);
127 static void             chat_composing_stop               (GossipChat      *chat);
128 static void             chat_composing_remove_timeout     (GossipChat      *chat);
129 static gboolean         chat_composing_stop_timeout_cb    (GossipChat      *chat);
130
131 enum {
132         COMPOSING,
133         NEW_MESSAGE,
134         NAME_CHANGED,
135         STATUS_CHANGED,
136         LAST_SIGNAL
137 };
138
139 static guint chat_signals[LAST_SIGNAL] = { 0 };
140
141 G_DEFINE_TYPE (GossipChat, gossip_chat, G_TYPE_OBJECT);
142
143 static void
144 gossip_chat_class_init (GossipChatClass *klass)
145 {
146         GObjectClass *object_class;
147
148         object_class = G_OBJECT_CLASS (klass);
149
150         object_class->finalize = chat_finalize;
151
152         chat_signals[COMPOSING] =
153                 g_signal_new ("composing",
154                               G_OBJECT_CLASS_TYPE (object_class),
155                               G_SIGNAL_RUN_LAST,
156                               0,
157                               NULL, NULL,
158                               g_cclosure_marshal_VOID__BOOLEAN,
159                               G_TYPE_NONE,
160                               1, G_TYPE_BOOLEAN);
161
162         chat_signals[NEW_MESSAGE] =
163                 g_signal_new ("new-message",
164                               G_OBJECT_CLASS_TYPE (object_class),
165                               G_SIGNAL_RUN_LAST,
166                               0,
167                               NULL, NULL,
168                               g_cclosure_marshal_VOID__OBJECT,
169                               G_TYPE_NONE,
170                               1, GOSSIP_TYPE_MESSAGE);
171
172         chat_signals[NAME_CHANGED] =
173                 g_signal_new ("name-changed",
174                               G_OBJECT_CLASS_TYPE (object_class),
175                               G_SIGNAL_RUN_LAST,
176                               0,
177                               NULL, NULL,
178                               g_cclosure_marshal_VOID__POINTER,
179                               G_TYPE_NONE,
180                               1, G_TYPE_POINTER);
181
182         chat_signals[STATUS_CHANGED] =
183                 g_signal_new ("status-changed",
184                               G_OBJECT_CLASS_TYPE (object_class),
185                               G_SIGNAL_RUN_LAST,
186                               0,
187                               NULL, NULL,
188                               g_cclosure_marshal_VOID__VOID,
189                               G_TYPE_NONE,
190                               0);
191
192         g_type_class_add_private (object_class, sizeof (GossipChatPriv));
193 }
194
195 static void
196 gossip_chat_init (GossipChat *chat)
197 {
198         GossipChatPriv *priv;
199         GtkTextBuffer  *buffer;
200
201         chat->view = gossip_chat_view_new ();
202         chat->input_text_view = gtk_text_view_new ();
203
204         chat->is_first_char = TRUE;
205
206         g_object_set (chat->input_text_view,
207                       "pixels-above-lines", 2,
208                       "pixels-below-lines", 2,
209                       "pixels-inside-wrap", 1,
210                       "right-margin", 2,
211                       "left-margin", 2,
212                       "wrap-mode", GTK_WRAP_WORD_CHAR,
213                       NULL);
214
215         priv = GET_PRIV (chat);
216
217         priv->tooltips = gtk_tooltips_new ();
218
219         priv->default_window_height = -1;
220         priv->vscroll_visible = FALSE;
221         priv->sensitive = TRUE;
222
223         g_signal_connect (chat->input_text_view,
224                           "key_press_event",
225                           G_CALLBACK (chat_input_key_press_event_cb),
226                           chat);
227
228         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
229         g_signal_connect (buffer,
230                           "changed",
231                           G_CALLBACK (chat_input_text_buffer_changed_cb),
232                           chat);
233         g_signal_connect (GOSSIP_CHAT (chat)->view,
234                           "focus_in_event",
235                           G_CALLBACK (chat_text_view_focus_in_event_cb),
236                           chat);
237
238         g_signal_connect (chat->input_text_view,
239                           "size_allocate",
240                           G_CALLBACK (chat_text_view_size_allocate_cb),
241                           chat);
242
243         g_signal_connect (chat->input_text_view,
244                           "realize",
245                           G_CALLBACK (chat_text_view_realize_cb),
246                           chat);
247
248         g_signal_connect (GTK_TEXT_VIEW (chat->input_text_view),
249                           "populate_popup",
250                           G_CALLBACK (chat_text_populate_popup_cb),
251                           chat);
252
253         /* create misspelt words identification tag */
254         gtk_text_buffer_create_tag (buffer,
255                                     "misspelled",
256                                     "underline", PANGO_UNDERLINE_ERROR,
257                                     NULL);
258 }
259
260 static void
261 chat_finalize (GObject *object)
262 {
263         GossipChat     *chat;
264         GossipChatPriv *priv;
265
266         chat = GOSSIP_CHAT (object);
267         priv = GET_PRIV (chat);
268
269         gossip_debug (DEBUG_DOMAIN, "Finalized: %p", object);
270
271         chat_composing_remove_timeout (chat);
272         g_object_unref (GOSSIP_CHAT (object)->account);
273
274         if (priv->tp_chat) {
275                 g_object_unref (priv->tp_chat);
276         }
277
278         G_OBJECT_CLASS (gossip_chat_parent_class)->finalize (object);
279 }
280
281 static void
282 chat_destroy_cb (EmpathyTpChat *tp_chat,
283                  GossipChat    *chat)
284 {
285         GossipChatPriv *priv;
286         GtkWidget      *widget;
287
288         priv = GET_PRIV (chat);
289
290         if (priv->tp_chat) {
291                 g_object_unref (priv->tp_chat);
292                 priv->tp_chat = NULL;
293         }
294
295         gossip_chat_view_append_event (chat->view, _("Disconnected"));
296
297         widget = gossip_chat_get_widget (chat);
298         gtk_widget_set_sensitive (widget, FALSE);
299         priv->sensitive = FALSE;
300 }
301
302 static void
303 chat_send (GossipChat  *chat,
304            const gchar *msg)
305 {
306         GossipChatPriv   *priv;
307         //GossipLogManager *log_manager;
308         GossipMessage    *message;
309         GossipContact    *own_contact;
310
311         priv = GET_PRIV (chat);
312
313         if (msg == NULL || msg[0] == '\0') {
314                 return;
315         }
316
317         if (g_str_has_prefix (msg, "/clear")) {
318                 gossip_chat_view_clear (chat->view);
319                 return;
320         }
321
322         /* FIXME: gossip_app_set_not_away ();*/
323
324         own_contact = gossip_chat_get_own_contact (chat);
325         message = gossip_message_new (msg);
326         gossip_message_set_sender (message, own_contact);
327
328         //FIXME: log_manager = gossip_session_get_log_manager (gossip_app_get_session ());
329         //gossip_log_message_for_contact (log_manager, message, FALSE);
330
331         empathy_tp_chat_send (priv->tp_chat, message);
332
333         g_object_unref (message);
334 }
335
336 static void
337 chat_input_text_view_send (GossipChat *chat)
338 {
339         GossipChatPriv *priv;
340         GtkTextBuffer  *buffer;
341         GtkTextIter     start, end;
342         gchar          *msg;
343
344         priv = GET_PRIV (chat);
345
346         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
347
348         gtk_text_buffer_get_bounds (buffer, &start, &end);
349         msg = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
350
351         /* clear the input field */
352         gtk_text_buffer_set_text (buffer, "", -1);
353
354         chat_send (chat, msg);
355
356         g_free (msg);
357
358         chat->is_first_char = TRUE;
359 }
360
361 static void
362 chat_message_received_cb (EmpathyTpChat *tp_chat,
363                           GossipMessage *message,
364                           GossipChat    *chat)
365 {
366         GossipChatPriv *priv;
367         //GossipLogManager      *log_manager;
368         GossipContact         *sender;
369
370         priv = GET_PRIV (chat);
371
372         sender = gossip_message_get_sender (message);
373         gossip_debug (DEBUG_DOMAIN, "Appending message ('%s')",
374                       gossip_contact_get_name (sender));
375
376 /*FIXME:
377         log_manager = gossip_session_get_log_manager (gossip_app_get_session ());
378         gossip_log_message_for_contact (log_manager, message, TRUE);
379 */
380         gossip_chat_view_append_message (chat->view, message);
381
382         if (gossip_chat_should_play_sound (chat)) {
383                 // FIXME: gossip_sound_play (GOSSIP_SOUND_CHAT);
384         }
385
386         g_signal_emit_by_name (chat, "new-message", message);
387 }
388
389 static gboolean
390 chat_input_key_press_event_cb (GtkWidget   *widget,
391                                GdkEventKey *event,
392                                GossipChat  *chat)
393 {
394         GossipChatPriv *priv;
395         GtkAdjustment  *adj;
396         gdouble         val;
397         GtkWidget      *text_view_sw;
398
399         priv = GET_PRIV (chat);
400
401         if (event->keyval == GDK_Tab && !(event->state & GDK_CONTROL_MASK)) {
402                 return TRUE;
403         }
404
405         /* Catch enter but not ctrl/shift-enter */
406         if (IS_ENTER (event->keyval) && !(event->state & GDK_SHIFT_MASK)) {
407                 GtkTextView *view;
408
409                 /* This is to make sure that kinput2 gets the enter. And if
410                  * it's handled there we shouldn't send on it. This is because
411                  * kinput2 uses Enter to commit letters. See:
412                  * http://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=104299
413                  */
414
415                 view = GTK_TEXT_VIEW (chat->input_text_view);
416                 if (gtk_im_context_filter_keypress (view->im_context, event)) {
417                         GTK_TEXT_VIEW (chat->input_text_view)->need_im_reset = TRUE;
418                         return TRUE;
419                 }
420
421                 chat_input_text_view_send (chat);
422                 return TRUE;
423         }
424
425         text_view_sw = gtk_widget_get_parent (GTK_WIDGET (chat->view));
426         if (IS_ENTER (event->keyval) && (event->state & GDK_SHIFT_MASK)) {
427                 /* Newline for shift-enter. */
428                 return FALSE;
429         }
430         else if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK &&
431                  event->keyval == GDK_Page_Up) {
432                 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
433                 gtk_adjustment_set_value (adj, adj->value - adj->page_size);
434
435                 return TRUE;
436         }
437         else if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK &&
438                  event->keyval == GDK_Page_Down) {
439                 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
440                 val = MIN (adj->value + adj->page_size, adj->upper - adj->page_size);
441                 gtk_adjustment_set_value (adj, val);
442
443                 return TRUE;
444         }
445
446         return FALSE;
447 }
448
449 static gboolean
450 chat_text_view_focus_in_event_cb (GtkWidget  *widget,
451                                   GdkEvent   *event,
452                                   GossipChat *chat)
453 {
454         gtk_widget_grab_focus (chat->input_text_view);
455
456         return TRUE;
457 }
458
459 static void
460 chat_input_text_buffer_changed_cb (GtkTextBuffer *buffer,
461                                    GossipChat    *chat)
462 {
463         GossipChatPriv *priv;
464         GtkTextIter     start, end;
465         gchar          *str;
466         gboolean        spell_checker = FALSE;
467
468         priv = GET_PRIV (chat);
469
470         if (gtk_text_buffer_get_char_count (buffer) == 0) {
471                 chat_composing_stop (chat);
472         } else {
473                 chat_composing_start (chat);
474         }
475
476         gossip_conf_get_bool (gossip_conf_get (),
477                               GOSSIP_PREFS_CHAT_SPELL_CHECKER_ENABLED,
478                               &spell_checker);
479
480         if (chat->is_first_char) {
481                 GtkRequisition  req;
482                 gint            window_height;
483                 GtkWidget      *dialog;
484                 GtkAllocation  *allocation;
485
486                 /* Save the window's size */
487                 dialog = gossip_chat_window_get_dialog (priv->window);
488                 gtk_window_get_size (GTK_WINDOW (dialog),
489                                      NULL, &window_height);
490
491                 gtk_widget_size_request (chat->input_text_view, &req);
492
493                 allocation = &GTK_WIDGET (chat->view)->allocation;
494
495                 priv->default_window_height = window_height;
496                 priv->last_input_height = req.height;
497                 priv->padding_height = window_height - req.height - allocation->height;
498
499                 chat->is_first_char = FALSE;
500         }
501
502         gtk_text_buffer_get_start_iter (buffer, &start);
503
504         if (!spell_checker) {
505                 gtk_text_buffer_get_end_iter (buffer, &end);
506                 gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
507                 return;
508         }
509
510         if (!gossip_spell_supported ()) {
511                 return;
512         }
513
514         /* NOTE: this is really inefficient, we shouldn't have to
515            reiterate the whole buffer each time and check each work
516            every time. */
517         while (TRUE) {
518                 gboolean correct = FALSE;
519
520                 /* if at start */
521                 if (gtk_text_iter_is_start (&start)) {
522                         end = start;
523
524                         if (!gtk_text_iter_forward_word_end (&end)) {
525                                 /* no whole word yet */
526                                 break;
527                         }
528                 } else {
529                         if (!gtk_text_iter_forward_word_end (&end)) {
530                                 /* must be the end of the buffer */
531                                 break;
532                         }
533
534                         start = end;
535                         gtk_text_iter_backward_word_start (&start);
536                 }
537
538                 str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
539
540                 /* spell check string */
541                 if (!gossip_chat_get_is_command (str)) {
542                         correct = gossip_spell_check (str);
543                 } else {
544                         correct = TRUE;
545                 }
546
547                 if (!correct) {
548                         gtk_text_buffer_apply_tag_by_name (buffer, "misspelled", &start, &end);
549                 } else {
550                         gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
551                 }
552
553                 g_free (str);
554
555                 /* set start iter to the end iters position */
556                 start = end;
557         }
558 }
559
560 typedef struct {
561         GtkWidget *window;
562         gint       width;
563         gint       height;
564 } ChangeSizeData;
565
566 static gboolean
567 chat_change_size_in_idle_cb (ChangeSizeData *data)
568 {
569         gtk_window_resize (GTK_WINDOW (data->window),
570                            data->width, data->height);
571
572         return FALSE;
573 }
574
575 static void
576 chat_text_view_scroll_hide_cb (GtkWidget  *widget,
577                                GossipChat *chat)
578 {
579         GossipChatPriv *priv;
580         GtkWidget      *sw;
581
582         priv = GET_PRIV (chat);
583
584         priv->vscroll_visible = FALSE;
585         g_signal_handlers_disconnect_by_func (widget, chat_text_view_scroll_hide_cb, chat);
586
587         sw = gtk_widget_get_parent (chat->input_text_view);
588         gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
589                                         GTK_POLICY_NEVER,
590                                         GTK_POLICY_NEVER);
591         g_object_set (sw, "height-request", -1, NULL);
592 }
593
594 static void
595 chat_text_view_size_allocate_cb (GtkWidget     *widget,
596                                  GtkAllocation *allocation,
597                                  GossipChat    *chat)
598 {
599         GossipChatPriv *priv;
600         gint            width;
601         GtkWidget      *dialog;
602         ChangeSizeData *data;
603         gint            window_height;
604         gint            new_height;
605         GtkAllocation  *view_allocation;
606         gint            current_height;
607         gint            diff;
608         GtkWidget      *sw;
609
610         priv = GET_PRIV (chat);
611
612         if (priv->default_window_height <= 0) {
613                 return;
614         }
615
616         sw = gtk_widget_get_parent (widget);
617         if (sw->allocation.height >= MAX_INPUT_HEIGHT && !priv->vscroll_visible) {
618                 GtkWidget *vscroll;
619
620                 priv->vscroll_visible = TRUE;
621                 gtk_widget_set_size_request (sw, sw->allocation.width, MAX_INPUT_HEIGHT);
622                 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
623                                                 GTK_POLICY_NEVER,
624                                                 GTK_POLICY_AUTOMATIC);
625                 vscroll = gtk_scrolled_window_get_vscrollbar (GTK_SCROLLED_WINDOW (sw));
626                 g_signal_connect (vscroll, "hide",
627                                   G_CALLBACK (chat_text_view_scroll_hide_cb),
628                                   chat);
629         }
630
631         if (priv->last_input_height <= allocation->height) {
632                 priv->last_input_height = allocation->height;
633                 return;
634         }
635
636         diff = priv->last_input_height - allocation->height;
637         priv->last_input_height = allocation->height;
638
639         view_allocation = &GTK_WIDGET (chat->view)->allocation;
640
641         dialog = gossip_chat_window_get_dialog (priv->window);
642         gtk_window_get_size (GTK_WINDOW (dialog), NULL, &current_height);
643
644         new_height = view_allocation->height + priv->padding_height + allocation->height - diff;
645
646         if (new_height <= priv->default_window_height) {
647                 window_height = priv->default_window_height;
648         } else {
649                 window_height = new_height;
650         }
651
652         if (current_height <= window_height) {
653                 return;
654         }
655
656         /* Restore the window's size */
657         gtk_window_get_size (GTK_WINDOW (dialog), &width, NULL);
658
659         data = g_new0 (ChangeSizeData, 1);
660         data->window = dialog;
661         data->width  = width;
662         data->height = window_height;
663
664         g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
665                          (GSourceFunc) chat_change_size_in_idle_cb,
666                          data, g_free);
667 }
668
669 static void
670 chat_text_view_realize_cb (GtkWidget  *widget,
671                            GossipChat *chat)
672 {
673         gossip_debug (DEBUG_DOMAIN, "Setting focus to the input text view");
674         gtk_widget_grab_focus (widget);
675 }
676
677 static void
678 chat_insert_smiley_activate_cb (GtkWidget  *menuitem,
679                                 GossipChat *chat)
680 {
681         GtkTextBuffer *buffer;
682         GtkTextIter    iter;
683         const gchar   *smiley;
684
685         smiley = g_object_get_data (G_OBJECT (menuitem), "smiley_text");
686
687         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
688
689         gtk_text_buffer_get_end_iter (buffer, &iter);
690         gtk_text_buffer_insert (buffer, &iter, smiley, -1);
691
692         gtk_text_buffer_get_end_iter (buffer, &iter);
693         gtk_text_buffer_insert (buffer, &iter, " ", -1);
694 }
695
696 static void
697 chat_text_populate_popup_cb (GtkTextView *view,
698                              GtkMenu     *menu,
699                              GossipChat  *chat)
700 {
701         GossipChatPriv  *priv;
702         GtkTextBuffer   *buffer;
703         GtkTextTagTable *table;
704         GtkTextTag      *tag;
705         gint             x, y;
706         GtkTextIter      iter, start, end;
707         GtkWidget       *item;
708         gchar           *str = NULL;
709         GossipChatSpell *chat_spell;
710         GtkWidget       *smiley_menu;
711
712         priv = GET_PRIV (chat);
713
714         /* Add the emoticon menu. */
715         item = gtk_separator_menu_item_new ();
716         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
717         gtk_widget_show (item);
718
719         item = gtk_menu_item_new_with_mnemonic (_("Insert Smiley"));
720         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
721         gtk_widget_show (item);
722
723         smiley_menu = gossip_chat_view_get_smiley_menu (
724                 G_CALLBACK (chat_insert_smiley_activate_cb),
725                 chat,
726                 priv->tooltips);
727         gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), smiley_menu);
728
729         /* Add the spell check menu item. */
730         buffer = gtk_text_view_get_buffer (view);
731         table = gtk_text_buffer_get_tag_table (buffer);
732
733         tag = gtk_text_tag_table_lookup (table, "misspelled");
734
735         gtk_widget_get_pointer (GTK_WIDGET (view), &x, &y);
736
737         gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
738                                                GTK_TEXT_WINDOW_WIDGET,
739                                                x, y,
740                                                &x, &y);
741
742         gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y);
743
744         start = end = iter;
745
746         if (gtk_text_iter_backward_to_tag_toggle (&start, tag) &&
747             gtk_text_iter_forward_to_tag_toggle (&end, tag)) {
748
749                 str = gtk_text_buffer_get_text (buffer,
750                                                 &start, &end, FALSE);
751         }
752
753         if (G_STR_EMPTY (str)) {
754                 return;
755         }
756
757         chat_spell = chat_spell_new (chat, str, start, end);
758
759         g_object_set_data_full (G_OBJECT (menu),
760                                 "chat_spell", chat_spell,
761                                 (GDestroyNotify) chat_spell_free);
762
763         item = gtk_separator_menu_item_new ();
764         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
765         gtk_widget_show (item);
766
767         item = gtk_menu_item_new_with_mnemonic (_("_Check Word Spelling..."));
768         g_signal_connect (item,
769                           "activate",
770                           G_CALLBACK (chat_text_check_word_spelling_cb),
771                           chat_spell);
772         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
773         gtk_widget_show (item);
774 }
775
776 static void
777 chat_text_check_word_spelling_cb (GtkMenuItem     *menuitem,
778                                   GossipChatSpell *chat_spell)
779 {
780 /*FIXME:        gossip_spell_dialog_show (chat_spell->chat,
781                                   chat_spell->start,
782                                   chat_spell->end,
783                                   chat_spell->word);*/
784 }
785
786 static GossipChatSpell *
787 chat_spell_new (GossipChat  *chat,
788                 const gchar *word,
789                 GtkTextIter  start,
790                 GtkTextIter  end)
791 {
792         GossipChatSpell *chat_spell;
793
794         chat_spell = g_new0 (GossipChatSpell, 1);
795
796         chat_spell->chat = g_object_ref (chat);
797         chat_spell->word = g_strdup (word);
798         chat_spell->start = start;
799         chat_spell->end = end;
800
801         return chat_spell;
802 }
803
804 static void
805 chat_spell_free (GossipChatSpell *chat_spell)
806 {
807         g_object_unref (chat_spell->chat);
808         g_free (chat_spell->word);
809         g_free (chat_spell);
810 }
811
812 static void
813 chat_composing_start (GossipChat *chat)
814 {
815         GossipChatPriv *priv;
816
817         priv = GET_PRIV (chat);
818
819         if (priv->composing_stop_timeout_id) {
820                 /* Just restart the timeout */
821                 chat_composing_remove_timeout (chat);
822         } else {
823         /* FIXME:
824                 gossip_session_send_composing (gossip_app_get_session (),
825                                                priv->contact, TRUE);
826                                               */
827         }
828
829         priv->composing_stop_timeout_id = g_timeout_add (
830                 1000 * COMPOSING_STOP_TIMEOUT,
831                 (GSourceFunc) chat_composing_stop_timeout_cb,
832                 chat);
833 }
834
835 static void
836 chat_composing_stop (GossipChat *chat)
837 {
838         GossipChatPriv *priv;
839
840         priv = GET_PRIV (chat);
841
842         chat_composing_remove_timeout (chat);
843         /* FIXME:
844         gossip_session_send_composing (gossip_app_get_session (),
845                                        priv->contact, FALSE);*/
846 }
847
848 static void
849 chat_composing_remove_timeout (GossipChat *chat)
850 {
851         GossipChatPriv *priv;
852
853         priv = GET_PRIV (chat);
854
855         if (priv->composing_stop_timeout_id) {
856                 g_source_remove (priv->composing_stop_timeout_id);
857                 priv->composing_stop_timeout_id = 0;
858         }
859 }
860
861 static gboolean
862 chat_composing_stop_timeout_cb (GossipChat *chat)
863 {
864         GossipChatPriv *priv;
865
866         priv = GET_PRIV (chat);
867
868         priv->composing_stop_timeout_id = 0;
869         /* FIXME:
870         gossip_session_send_composing (gossip_app_get_session (),
871                                        priv->contact, FALSE);*/
872
873         return FALSE;
874 }
875
876 gboolean
877 gossip_chat_get_is_command (const gchar *str)
878 {
879         g_return_val_if_fail (str != NULL, FALSE);
880
881         if (str[0] != '/') {
882                 return FALSE;
883         }
884
885         if (g_str_has_prefix (str, "/me")) {
886                 return TRUE;
887         }
888         else if (g_str_has_prefix (str, "/nick")) {
889                 return TRUE;
890         }
891         else if (g_str_has_prefix (str, "/topic")) {
892                 return TRUE;
893         }
894
895         return FALSE;
896 }
897
898 void
899 gossip_chat_correct_word (GossipChat  *chat,
900                           GtkTextIter  start,
901                           GtkTextIter  end,
902                           const gchar *new_word)
903 {
904         GtkTextBuffer *buffer;
905
906         g_return_if_fail (chat != NULL);
907         g_return_if_fail (new_word != NULL);
908
909         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
910
911         gtk_text_buffer_delete (buffer, &start, &end);
912         gtk_text_buffer_insert (buffer, &start,
913                                 new_word,
914                                 -1);
915 }
916
917 const gchar *
918 gossip_chat_get_name (GossipChat *chat)
919 {
920         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL);
921
922         if (GOSSIP_CHAT_GET_CLASS (chat)->get_name) {
923                 return GOSSIP_CHAT_GET_CLASS (chat)->get_name (chat);
924         }
925
926         return NULL;
927 }
928
929 gchar *
930 gossip_chat_get_tooltip (GossipChat *chat)
931 {
932         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL);
933
934         if (GOSSIP_CHAT_GET_CLASS (chat)->get_tooltip) {
935                 return GOSSIP_CHAT_GET_CLASS (chat)->get_tooltip (chat);
936         }
937
938         return NULL;
939 }
940
941 GdkPixbuf *
942 gossip_chat_get_status_pixbuf (GossipChat *chat)
943 {
944         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL);
945
946         if (GOSSIP_CHAT_GET_CLASS (chat)->get_status_pixbuf) {
947                 return GOSSIP_CHAT_GET_CLASS (chat)->get_status_pixbuf (chat);
948         }
949
950         return NULL;
951 }
952
953 GossipContact *
954 gossip_chat_get_contact (GossipChat *chat)
955 {
956         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL);
957
958         if (GOSSIP_CHAT_GET_CLASS (chat)->get_contact) {
959                 return GOSSIP_CHAT_GET_CLASS (chat)->get_contact (chat);
960         }
961
962         return NULL;
963 }
964 GossipContact *
965 gossip_chat_get_own_contact (GossipChat *chat)
966 {
967         EmpathyContactManager *manager;
968
969         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL);
970
971         manager = empathy_session_get_contact_manager ();
972
973         return empathy_contact_manager_get_own (manager, chat->account);
974 }
975
976 GtkWidget *
977 gossip_chat_get_widget (GossipChat *chat)
978 {
979         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), NULL);
980
981         if (GOSSIP_CHAT_GET_CLASS (chat)->get_widget) {
982                 return GOSSIP_CHAT_GET_CLASS (chat)->get_widget (chat);
983         }
984
985         return NULL;
986 }
987
988 gboolean
989 gossip_chat_is_group_chat (GossipChat *chat)
990 {
991         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE);
992
993         if (GOSSIP_CHAT_GET_CLASS (chat)->is_group_chat) {
994                 return GOSSIP_CHAT_GET_CLASS (chat)->is_group_chat (chat);
995         }
996
997         return FALSE;
998 }
999
1000 gboolean 
1001 gossip_chat_is_connected (GossipChat *chat)
1002 {
1003         GossipChatPriv *priv;
1004
1005         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE);
1006
1007         priv = GET_PRIV (chat);
1008
1009         return (priv->tp_chat != NULL);
1010 }
1011
1012 gboolean
1013 gossip_chat_get_show_contacts (GossipChat *chat)
1014 {
1015         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE);
1016
1017         if (GOSSIP_CHAT_GET_CLASS (chat)->get_show_contacts) {
1018                 return GOSSIP_CHAT_GET_CLASS (chat)->get_show_contacts (chat);
1019         }
1020
1021         return FALSE;
1022 }
1023
1024 void
1025 gossip_chat_set_show_contacts (GossipChat *chat,
1026                                gboolean    show)
1027 {
1028         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1029
1030         if (GOSSIP_CHAT_GET_CLASS (chat)->set_show_contacts) {
1031                 GOSSIP_CHAT_GET_CLASS (chat)->set_show_contacts (chat, show);
1032         }
1033 }
1034
1035 void
1036 gossip_chat_save_geometry (GossipChat *chat,
1037                            gint        x,
1038                            gint        y,
1039                            gint        w,
1040                            gint        h)
1041 {
1042         gossip_geometry_save (gossip_chat_get_id (chat), x, y, w, h);
1043 }
1044
1045 void
1046 gossip_chat_load_geometry (GossipChat *chat,
1047                            gint       *x,
1048                            gint       *y,
1049                            gint       *w,
1050                            gint       *h)
1051 {
1052         gossip_geometry_load (gossip_chat_get_id (chat), x, y, w, h);
1053 }
1054
1055 void
1056 gossip_chat_set_tp_chat (GossipChat    *chat,
1057                          EmpathyTpChat *tp_chat)
1058 {
1059         GossipChatPriv *priv;
1060         GtkWidget      *widget;
1061
1062         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1063         g_return_if_fail (EMPATHY_IS_TP_CHAT (tp_chat));
1064
1065         priv = GET_PRIV (chat);
1066
1067         if (tp_chat == priv->tp_chat) {
1068                 return;
1069         }
1070
1071         if (priv->tp_chat) {
1072                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
1073                                                       chat_message_received_cb,
1074                                                       chat);
1075                 g_signal_handlers_disconnect_by_func (priv->tp_chat,
1076                                                       chat_destroy_cb,
1077                                                       chat);
1078                 g_object_unref (priv->tp_chat);
1079         }
1080
1081         g_free (priv->id);
1082         priv->tp_chat = g_object_ref (tp_chat);
1083         priv->id = g_strdup (empathy_tp_chat_get_id (tp_chat));
1084
1085         g_signal_connect (tp_chat, "message-received",
1086                           G_CALLBACK (chat_message_received_cb),
1087                           chat);
1088         g_signal_connect (tp_chat, "destroy",
1089                           G_CALLBACK (chat_destroy_cb),
1090                           chat);
1091
1092         empathy_tp_chat_request_pending (tp_chat);
1093
1094         if (!priv->sensitive) {
1095                 widget = gossip_chat_get_widget (chat);
1096                 gtk_widget_set_sensitive (widget, TRUE);
1097                 gossip_chat_view_append_event (chat->view, _("Connected"));
1098                 priv->sensitive = TRUE;
1099         }
1100 }
1101
1102 const gchar *
1103 gossip_chat_get_id (GossipChat *chat)
1104 {
1105         GossipChatPriv *priv;
1106
1107         priv = GET_PRIV (chat);
1108
1109         return priv->id;
1110 }
1111
1112 void
1113 gossip_chat_clear (GossipChat *chat)
1114 {
1115         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1116
1117         gossip_chat_view_clear (chat->view);
1118 }
1119
1120 void
1121 gossip_chat_set_window (GossipChat       *chat,
1122                         GossipChatWindow *window)
1123 {
1124         GossipChatPriv *priv;
1125
1126         priv = GET_PRIV (chat);
1127         priv->window = window;
1128 }
1129
1130 GossipChatWindow *
1131 gossip_chat_get_window (GossipChat *chat)
1132 {
1133         GossipChatPriv *priv;
1134
1135         priv = GET_PRIV (chat);
1136
1137         return priv->window;
1138 }
1139
1140 void
1141 gossip_chat_scroll_down (GossipChat *chat)
1142 {
1143         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1144
1145         gossip_chat_view_scroll_down (chat->view);
1146 }
1147
1148 void
1149 gossip_chat_cut (GossipChat *chat)
1150 {
1151         GtkTextBuffer *buffer;
1152
1153         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1154
1155         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1156         if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) {
1157                 GtkClipboard *clipboard;
1158
1159                 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1160
1161                 gtk_text_buffer_cut_clipboard (buffer, clipboard, TRUE);
1162         }
1163 }
1164
1165 void
1166 gossip_chat_copy (GossipChat *chat)
1167 {
1168         GtkTextBuffer *buffer;
1169
1170         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1171
1172         if (gossip_chat_view_get_selection_bounds (chat->view, NULL, NULL)) {
1173                 gossip_chat_view_copy_clipboard (chat->view);
1174                 return;
1175         }
1176
1177         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1178         if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) {
1179                 GtkClipboard *clipboard;
1180
1181                 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1182
1183                 gtk_text_buffer_copy_clipboard (buffer, clipboard);
1184         }
1185 }
1186
1187 void
1188 gossip_chat_paste (GossipChat *chat)
1189 {
1190         GtkTextBuffer *buffer;
1191         GtkClipboard  *clipboard;
1192
1193         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1194
1195         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1196         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1197
1198         gtk_text_buffer_paste_clipboard (buffer, clipboard, NULL, TRUE);
1199 }
1200
1201 void
1202 gossip_chat_present (GossipChat *chat)
1203 {
1204         GossipChatPriv *priv;
1205
1206         g_return_if_fail (GOSSIP_IS_CHAT (chat));
1207
1208         priv = GET_PRIV (chat);
1209
1210         if (priv->window == NULL) {
1211                 GossipChatWindow *window;
1212
1213                 window = gossip_chat_window_get_default ();
1214                 if (!window) {
1215                         window = gossip_chat_window_new ();
1216                 }
1217
1218                 gossip_chat_window_add_chat (window, chat);
1219         }
1220
1221         gossip_chat_window_switch_to_chat (priv->window, chat);
1222         gossip_window_present (
1223                 GTK_WINDOW (gossip_chat_window_get_dialog (priv->window)),
1224                 TRUE);
1225
1226         gtk_widget_grab_focus (chat->input_text_view); 
1227 }
1228
1229 gboolean
1230 gossip_chat_should_play_sound (GossipChat *chat)
1231 {
1232         GossipChatWindow *window;
1233         gboolean          play = TRUE;
1234
1235         g_return_val_if_fail (GOSSIP_IS_CHAT (chat), FALSE);
1236
1237         window = gossip_chat_get_window (GOSSIP_CHAT (chat));
1238         if (!window) {
1239                 return TRUE;
1240         }
1241
1242         play = !gossip_chat_window_has_focus (window);
1243
1244         return play;
1245 }
1246
1247 gboolean
1248 gossip_chat_should_highlight_nick (GossipMessage *message)
1249 {
1250         GossipContact *my_contact;
1251         const gchar   *msg, *to;
1252         gchar         *cf_msg, *cf_to;
1253         gchar         *ch;
1254         gboolean       ret_val;
1255
1256         g_return_val_if_fail (GOSSIP_IS_MESSAGE (message), FALSE);
1257
1258         gossip_debug (DEBUG_DOMAIN, "Highlighting nickname");
1259
1260         ret_val = FALSE;
1261
1262         msg = gossip_message_get_body (message);
1263         if (!msg) {
1264                 return FALSE;
1265         }
1266
1267         my_contact = gossip_get_own_contact_from_contact (gossip_message_get_sender (message));
1268         to = gossip_contact_get_name (my_contact);
1269         if (!to) {
1270                 return FALSE;
1271         }
1272
1273         cf_msg = g_utf8_casefold (msg, -1);
1274         cf_to = g_utf8_casefold (to, -1);
1275
1276         ch = strstr (cf_msg, cf_to);
1277         if (ch == NULL) {
1278                 goto finished;
1279         }
1280
1281         if (ch != cf_msg) {
1282                 /* Not first in the message */
1283                 if ((*(ch - 1) != ' ') &&
1284                     (*(ch - 1) != ',') &&
1285                     (*(ch - 1) != '.')) {
1286                         goto finished;
1287                 }
1288         }
1289
1290         ch = ch + strlen (cf_to);
1291         if (ch >= cf_msg + strlen (cf_msg)) {
1292                 ret_val = TRUE;
1293                 goto finished;
1294         }
1295
1296         if ((*ch == ' ') ||
1297             (*ch == ',') ||
1298             (*ch == '.') ||
1299             (*ch == ':')) {
1300                 ret_val = TRUE;
1301                 goto finished;
1302         }
1303
1304 finished:
1305         g_free (cf_msg);
1306         g_free (cf_to);
1307
1308         return ret_val;
1309 }
1310