]> git.0d.be Git - empathy.git/blob - src/empathy-chat-window.c
EmpathyChat: include should_highlight in ::new-message
[empathy.git] / src / empathy-chat-window.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2003-2007 Imendio AB
4  * Copyright (C) 2007-2010 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., 51 Franklin St, Fifth Floor,
19  * Boston, MA  02110-1301  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  *          RĂ´mulo Fernandes Machado <romulo@castorgroup.net>
27  */
28
29 #include <config.h>
30
31 #include <string.h>
32
33 #include <gtk/gtk.h>
34 #include <gdk/gdkkeysyms.h>
35 #include <gdk/gdkx.h>
36 #include <glib/gi18n.h>
37 #include <libnotify/notification.h>
38
39 #include <telepathy-glib/telepathy-glib.h>
40
41 #include <libempathy/empathy-client-factory.h>
42 #include <libempathy/empathy-contact.h>
43 #include <libempathy/empathy-message.h>
44 #include <libempathy/empathy-chatroom-manager.h>
45 #include <libempathy/empathy-gsettings.h>
46 #include <libempathy/empathy-utils.h>
47 #include <libempathy/empathy-tp-contact-factory.h>
48 #include <libempathy/empathy-contact-list.h>
49 #include <libempathy/empathy-request-util.h>
50
51 #include <libempathy-gtk/empathy-images.h>
52 #include <libempathy-gtk/empathy-contact-dialogs.h>
53 #include <libempathy-gtk/empathy-log-window.h>
54 #include <libempathy-gtk/empathy-geometry.h>
55 #include <libempathy-gtk/empathy-smiley-manager.h>
56 #include <libempathy-gtk/empathy-sound-manager.h>
57 #include <libempathy-gtk/empathy-ui-utils.h>
58 #include <libempathy-gtk/empathy-notify-manager.h>
59
60 #include "empathy-chat-manager.h"
61 #include "empathy-chat-window.h"
62 #include "empathy-about-dialog.h"
63 #include "empathy-invite-participant-dialog.h"
64 #include "gedit-close-button.h"
65
66 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
67 #include <libempathy/empathy-debug.h>
68
69 /* Macro to compare guint32 X timestamps, while accounting for wrapping around
70  */
71 #define X_EARLIER_OR_EQL(t1, t2) \
72         ((t1 <= t2 && ((t2 - t1) < G_MAXUINT32/2))  \
73           || (t1 >= t2 && (t1 - t2) > (G_MAXUINT32/2)) \
74         )
75
76 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyChatWindow)
77 typedef struct {
78         EmpathyChat *current_chat;
79         GList       *chats;
80         gboolean     page_added;
81         gboolean     dnd_same_window;
82         EmpathyChatroomManager *chatroom_manager;
83         EmpathyNotifyManager *notify_mgr;
84         GtkWidget   *dialog;
85         GtkWidget   *notebook;
86         NotifyNotification *notification;
87
88         GtkTargetList *contact_targets;
89         GtkTargetList *file_targets;
90
91         EmpathyChatManager *chat_manager;
92         gulong chat_manager_chats_changed_id;
93
94         /* Menu items. */
95         GtkUIManager *ui_manager;
96         GtkAction   *menu_conv_insert_smiley;
97         GtkAction   *menu_conv_favorite;
98         GtkAction   *menu_conv_always_urgent;
99         GtkAction   *menu_conv_toggle_contacts;
100
101         GtkAction   *menu_edit_cut;
102         GtkAction   *menu_edit_copy;
103         GtkAction   *menu_edit_paste;
104         GtkAction   *menu_edit_find;
105
106         GtkAction   *menu_tabs_next;
107         GtkAction   *menu_tabs_prev;
108         GtkAction   *menu_tabs_undo_close_tab;
109         GtkAction   *menu_tabs_left;
110         GtkAction   *menu_tabs_right;
111         GtkAction   *menu_tabs_detach;
112
113         /* Last user action time we acted upon to show a tab */
114         guint32    x_user_action_time;
115
116         GSettings *gsettings_chat;
117         GSettings *gsettings_notif;
118         GSettings *gsettings_ui;
119
120         EmpathySoundManager *sound_mgr;
121 } EmpathyChatWindowPriv;
122
123 static GList *chat_windows = NULL;
124
125 static const guint tab_accel_keys[] = {
126         GDK_KEY_1, GDK_KEY_2, GDK_KEY_3, GDK_KEY_4, GDK_KEY_5,
127         GDK_KEY_6, GDK_KEY_7, GDK_KEY_8, GDK_KEY_9, GDK_KEY_0
128 };
129
130 typedef enum {
131         DND_DRAG_TYPE_CONTACT_ID,
132         DND_DRAG_TYPE_URI_LIST,
133         DND_DRAG_TYPE_TAB
134 } DndDragType;
135
136 static const GtkTargetEntry drag_types_dest[] = {
137         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
138         { "GTK_NOTEBOOK_TAB", GTK_TARGET_SAME_APP, DND_DRAG_TYPE_TAB },
139         { "text/uri-list", 0, DND_DRAG_TYPE_URI_LIST },
140         { "text/path-list", 0, DND_DRAG_TYPE_URI_LIST },
141 };
142
143 static const GtkTargetEntry drag_types_dest_contact[] = {
144         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
145 };
146
147 static const GtkTargetEntry drag_types_dest_file[] = {
148         /* must be first to be prioritized, in order to receive the
149          * note's file path from Tomboy instead of an URI */
150         { "text/path-list", 0, DND_DRAG_TYPE_URI_LIST },
151         { "text/uri-list", 0, DND_DRAG_TYPE_URI_LIST },
152 };
153
154 static void chat_window_update (EmpathyChatWindow *window,
155                 gboolean update_contact_menu);
156
157 static void empathy_chat_window_add_chat (EmpathyChatWindow *window,
158                               EmpathyChat       *chat);
159
160 static void empathy_chat_window_remove_chat (EmpathyChatWindow *window,
161                                  EmpathyChat       *chat);
162
163 static void empathy_chat_window_move_chat (EmpathyChatWindow *old_window,
164                                EmpathyChatWindow *new_window,
165                                EmpathyChat       *chat);
166
167 static void empathy_chat_window_get_nb_chats (EmpathyChatWindow *self,
168                                guint *nb_rooms,
169                                guint *nb_private);
170
171 G_DEFINE_TYPE (EmpathyChatWindow, empathy_chat_window, G_TYPE_OBJECT);
172
173 static void
174 chat_window_accel_cb (GtkAccelGroup    *accelgroup,
175                       GObject          *object,
176                       guint             key,
177                       GdkModifierType   mod,
178                       EmpathyChatWindow *window)
179 {
180         EmpathyChatWindowPriv *priv;
181         gint                  num = -1;
182         guint                 i;
183
184         priv = GET_PRIV (window);
185
186         for (i = 0; i < G_N_ELEMENTS (tab_accel_keys); i++) {
187                 if (tab_accel_keys[i] == key) {
188                         num = i;
189                         break;
190                 }
191         }
192
193         if (num != -1) {
194                 gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), num);
195         }
196 }
197
198 static EmpathyChatWindow *
199 chat_window_find_chat (EmpathyChat *chat)
200 {
201         EmpathyChatWindowPriv *priv;
202         GList                 *l, *ll;
203
204         for (l = chat_windows; l; l = l->next) {
205                 priv = GET_PRIV (l->data);
206                 ll = g_list_find (priv->chats, chat);
207                 if (ll) {
208                         return l->data;
209                 }
210         }
211
212         return NULL;
213 }
214
215 static void
216 chat_window_close_clicked_cb (GtkAction   *action,
217                               EmpathyChat *chat)
218 {
219         EmpathyChatWindow *window;
220
221         window = chat_window_find_chat (chat);
222         empathy_chat_window_remove_chat (window, chat);
223 }
224
225 static void
226 chat_tab_style_updated_cb (GtkWidget *hbox,
227                            gpointer   user_data)
228 {
229         GtkWidget *button;
230         int char_width, h, w;
231         PangoContext *context;
232         const PangoFontDescription *font_desc;
233         PangoFontMetrics *metrics;
234
235         button = g_object_get_data (G_OBJECT (user_data),
236                 "chat-window-tab-close-button");
237         context = gtk_widget_get_pango_context (hbox);
238
239         font_desc = gtk_style_context_get_font (gtk_widget_get_style_context (hbox),
240                                                 GTK_STATE_FLAG_NORMAL);
241
242         metrics = pango_context_get_metrics (context, font_desc,
243                 pango_context_get_language (context));
244         char_width = pango_font_metrics_get_approximate_char_width (metrics);
245         pango_font_metrics_unref (metrics);
246
247         gtk_icon_size_lookup_for_settings (gtk_widget_get_settings (button),
248                                            GTK_ICON_SIZE_MENU, &w, &h);
249
250         /* Request at least about 12 chars width plus at least space for the status
251          * image and the close button */
252         gtk_widget_set_size_request (hbox,
253                 12 * PANGO_PIXELS (char_width) + 2 * w, -1);
254
255         gtk_widget_set_size_request (button, w, h);
256 }
257
258 static GtkWidget *
259 chat_window_create_label (EmpathyChatWindow *window,
260                           EmpathyChat       *chat,
261                           gboolean           is_tab_label)
262 {
263         GtkWidget            *hbox;
264         GtkWidget            *name_label;
265         GtkWidget            *status_image;
266         GtkWidget            *event_box;
267         GtkWidget            *event_box_hbox;
268         PangoAttrList        *attr_list;
269         PangoAttribute       *attr;
270
271         /* The spacing between the button and the label. */
272         hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
273
274         event_box = gtk_event_box_new ();
275         gtk_event_box_set_visible_window (GTK_EVENT_BOX (event_box), FALSE);
276
277         name_label = gtk_label_new (NULL);
278         if (is_tab_label)
279                 gtk_label_set_ellipsize (GTK_LABEL (name_label), PANGO_ELLIPSIZE_END);
280
281         attr_list = pango_attr_list_new ();
282         attr = pango_attr_scale_new (1/1.2);
283         attr->start_index = 0;
284         attr->end_index = -1;
285         pango_attr_list_insert (attr_list, attr);
286         gtk_label_set_attributes (GTK_LABEL (name_label), attr_list);
287         pango_attr_list_unref (attr_list);
288
289         gtk_misc_set_padding (GTK_MISC (name_label), 2, 0);
290         gtk_misc_set_alignment (GTK_MISC (name_label), 0.0, 0.5);
291         g_object_set_data (G_OBJECT (chat),
292                 is_tab_label ? "chat-window-tab-label" : "chat-window-menu-label",
293                 name_label);
294
295         status_image = gtk_image_new ();
296
297         /* Spacing between the icon and label. */
298         event_box_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
299
300         gtk_box_pack_start (GTK_BOX (event_box_hbox), status_image, FALSE, FALSE, 0);
301         gtk_box_pack_start (GTK_BOX (event_box_hbox), name_label, TRUE, TRUE, 0);
302
303         g_object_set_data (G_OBJECT (chat),
304                 is_tab_label ? "chat-window-tab-image" : "chat-window-menu-image",
305                 status_image);
306         g_object_set_data (G_OBJECT (chat),
307                 is_tab_label ? "chat-window-tab-tooltip-widget" : "chat-window-menu-tooltip-widget",
308                 event_box);
309
310         gtk_container_add (GTK_CONTAINER (event_box), event_box_hbox);
311         gtk_box_pack_start (GTK_BOX (hbox), event_box, TRUE, TRUE, 0);
312
313         if (is_tab_label) {
314                 GtkWidget            *close_button;
315                 GtkWidget *sending_spinner;
316
317                 sending_spinner = gtk_spinner_new ();
318
319                 gtk_box_pack_start (GTK_BOX (hbox), sending_spinner,
320                         FALSE, FALSE, 0);
321                 g_object_set_data (G_OBJECT (chat),
322                         "chat-window-tab-sending-spinner",
323                         sending_spinner);
324
325                 close_button = gedit_close_button_new ();
326                 g_object_set_data (G_OBJECT (chat), "chat-window-tab-close-button", close_button);
327
328                 /* We don't want focus/keynav for the button to avoid clutter, and
329                  * Ctrl-W works anyway.
330                  */
331                 gtk_widget_set_can_focus (close_button, FALSE);
332                 gtk_widget_set_can_default (close_button, FALSE);
333
334                 gtk_box_pack_end (GTK_BOX (hbox), close_button, FALSE, FALSE, 0);
335
336                 g_signal_connect (close_button,
337                                   "clicked",
338                                   G_CALLBACK (chat_window_close_clicked_cb),
339                                   chat);
340
341                 /* React to theme changes and also setup the size correctly.  */
342                 g_signal_connect (hbox,
343                                   "style-updated",
344                                   G_CALLBACK (chat_tab_style_updated_cb),
345                                   chat);
346         }
347
348         gtk_widget_show_all (hbox);
349
350         return hbox;
351 }
352
353 static void
354 _submenu_notify_visible_changed_cb (GObject    *object,
355                                     GParamSpec *pspec,
356                                     gpointer    userdata)
357 {
358         g_signal_handlers_disconnect_by_func (object,
359                                               _submenu_notify_visible_changed_cb,
360                                               userdata);
361         chat_window_update (EMPATHY_CHAT_WINDOW (userdata), TRUE);
362 }
363
364 static void
365 chat_window_menu_context_update (EmpathyChatWindowPriv *priv,
366                               gint num_pages)
367 {
368         gboolean first_page;
369         gboolean last_page;
370         gboolean wrap_around;
371         gboolean is_connected;
372         gint     page_num;
373
374         page_num = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
375         first_page = (page_num == 0);
376         last_page = (page_num == (num_pages - 1));
377         g_object_get (gtk_settings_get_default (), "gtk-keynav-wrap-around",
378                       &wrap_around, NULL);
379         is_connected = empathy_chat_get_tp_chat (priv->current_chat) != NULL;
380
381         gtk_action_set_sensitive (priv->menu_tabs_next, (!last_page ||
382                                                          wrap_around));
383         gtk_action_set_sensitive (priv->menu_tabs_prev, (!first_page ||
384                                                          wrap_around));
385         gtk_action_set_sensitive (priv->menu_tabs_detach, num_pages > 1);
386         gtk_action_set_sensitive (priv->menu_tabs_left, !first_page);
387         gtk_action_set_sensitive (priv->menu_tabs_right, !last_page);
388         gtk_action_set_sensitive (priv->menu_conv_insert_smiley, is_connected);
389 }
390
391 static void
392 chat_window_conversation_menu_update (EmpathyChatWindowPriv *priv,
393                                       EmpathyChatWindow     *self)
394 {
395         EmpathyTpChat *tp_chat;
396         TpConnection *connection;
397         GtkAction *action;
398         gboolean sensitive = FALSE;
399
400         g_return_if_fail (priv->current_chat != NULL);
401
402         action = gtk_ui_manager_get_action (priv->ui_manager,
403                 "/chats_menubar/menu_conv/menu_conv_invite_participant");
404         tp_chat = empathy_chat_get_tp_chat (priv->current_chat);
405
406         if (tp_chat != NULL) {
407                 connection = tp_channel_borrow_connection (TP_CHANNEL (tp_chat));
408
409                 sensitive = empathy_tp_chat_can_add_contact (tp_chat) &&
410                         (tp_connection_get_status (connection, NULL) ==
411                          TP_CONNECTION_STATUS_CONNECTED);
412         }
413
414         gtk_action_set_sensitive (action, sensitive);
415 }
416
417 static void
418 chat_window_contact_menu_update (EmpathyChatWindowPriv *priv,
419                                  EmpathyChatWindow     *window)
420 {
421         GtkWidget *menu, *submenu, *orig_submenu;
422
423         menu = gtk_ui_manager_get_widget (priv->ui_manager,
424                 "/chats_menubar/menu_contact");
425         orig_submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (menu));
426
427         if (orig_submenu == NULL || !gtk_widget_get_visible (orig_submenu)) {
428                 submenu = empathy_chat_get_contact_menu (priv->current_chat);
429
430                 if (submenu != NULL) {
431                         /* gtk_menu_attach_to_widget () doesn't behave nicely here */
432                         g_object_set_data (G_OBJECT (submenu), "window", priv->dialog);
433
434                         gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu), submenu);
435                         gtk_widget_show (menu);
436                         gtk_widget_set_sensitive (menu, TRUE);
437                 } else {
438                         gtk_widget_set_sensitive (menu, FALSE);
439                 }
440         } else {
441                 tp_g_signal_connect_object (orig_submenu,
442                                              "notify::visible",
443                                              (GCallback)_submenu_notify_visible_changed_cb,
444                                              window, 0);
445         }
446 }
447
448 static guint
449 get_all_unread_messages (EmpathyChatWindowPriv *priv)
450 {
451         GList *l;
452         guint nb = 0;
453
454         for (l = priv->chats; l != NULL; l = g_list_next (l))
455                 nb += empathy_chat_get_nb_unread_messages (EMPATHY_CHAT (l->data));
456
457         return nb;
458 }
459
460 static gchar *
461 get_window_title_name (EmpathyChatWindowPriv *priv)
462 {
463         gchar *active_name, *ret;
464         guint nb_chats;
465         guint current_unread_msgs;
466
467         nb_chats = g_list_length (priv->chats);
468         g_assert (nb_chats > 0);
469
470         active_name = empathy_chat_dup_name (priv->current_chat);
471
472         current_unread_msgs = empathy_chat_get_nb_unread_messages (
473                         priv->current_chat);
474
475         if (nb_chats == 1) {
476                 /* only one tab */
477                 if (current_unread_msgs == 0)
478                         ret = g_strdup (active_name);
479                 else
480                         ret = g_strdup_printf (ngettext (
481                                 "%s (%d unread)",
482                                 "%s (%d unread)", current_unread_msgs),
483                                 active_name, current_unread_msgs);
484         } else {
485                 guint nb_others = nb_chats - 1;
486                 guint all_unread_msgs;
487
488                 all_unread_msgs = get_all_unread_messages (priv);
489
490                 if (all_unread_msgs == 0) {
491                         /* no unread message */
492                         ret = g_strdup_printf (ngettext (
493                                 "%s (and %u other)",
494                                 "%s (and %u others)", nb_others),
495                                 active_name, nb_others);
496                 }
497
498                 else if (all_unread_msgs == current_unread_msgs) {
499                         /* unread messages are in the current tab */
500                         ret = g_strdup_printf (ngettext (
501                                 "%s (%d unread)",
502                                 "%s (%d unread)", current_unread_msgs),
503                                 active_name, current_unread_msgs);
504                 }
505
506                 else if (current_unread_msgs == 0) {
507                         /* unread messages are in other tabs */
508                         ret = g_strdup_printf (ngettext (
509                                 "%s (%d unread from others)",
510                                 "%s (%d unread from others)",
511                                 all_unread_msgs),
512                                 active_name, all_unread_msgs);
513                 }
514
515                 else {
516                         /* unread messages are in all the tabs */
517                         ret = g_strdup_printf (ngettext (
518                                 "%s (%d unread from all)",
519                                 "%s (%d unread from all)",
520                                 all_unread_msgs),
521                                 active_name, all_unread_msgs);
522                 }
523         }
524
525         g_free (active_name);
526
527         return ret;
528 }
529
530 static void
531 chat_window_title_update (EmpathyChatWindowPriv *priv)
532 {
533         gchar *name;
534
535         name = get_window_title_name (priv);
536         gtk_window_set_title (GTK_WINDOW (priv->dialog), name);
537         g_free (name);
538 }
539
540 static void
541 chat_window_icon_update (EmpathyChatWindowPriv *priv, gboolean new_messages)
542 {
543         GdkPixbuf      *icon;
544         EmpathyContact *remote_contact;
545         gboolean        avatar_in_icon;
546         guint           n_chats;
547
548         n_chats = g_list_length (priv->chats);
549
550         /* Update window icon */
551         if (new_messages) {
552                 gtk_window_set_icon_name (GTK_WINDOW (priv->dialog),
553                                           EMPATHY_IMAGE_MESSAGE);
554         } else {
555                 avatar_in_icon = g_settings_get_boolean (priv->gsettings_chat,
556                                 EMPATHY_PREFS_CHAT_AVATAR_IN_ICON);
557
558                 if (n_chats == 1 && avatar_in_icon) {
559                         remote_contact = empathy_chat_get_remote_contact (priv->current_chat);
560                         icon = empathy_pixbuf_avatar_from_contact_scaled (remote_contact, 0, 0);
561                         gtk_window_set_icon (GTK_WINDOW (priv->dialog), icon);
562
563                         if (icon != NULL) {
564                                 g_object_unref (icon);
565                         }
566                 } else {
567                         gtk_window_set_icon_name (GTK_WINDOW (priv->dialog), NULL);
568                 }
569         }
570 }
571
572 static void
573 chat_window_close_button_update (EmpathyChatWindowPriv *priv,
574                                  gint num_pages)
575 {
576         GtkWidget *chat;
577         GtkWidget *chat_close_button;
578         gint       i;
579
580         if (num_pages == 1) {
581                 chat = gtk_notebook_get_nth_page (GTK_NOTEBOOK (priv->notebook), 0);
582                 chat_close_button = g_object_get_data (G_OBJECT (chat),
583                                 "chat-window-tab-close-button");
584                 gtk_widget_hide (chat_close_button);
585         } else {
586                 for (i=0; i<num_pages; i++) {
587                         chat = gtk_notebook_get_nth_page (GTK_NOTEBOOK (priv->notebook), i);
588                         chat_close_button = g_object_get_data (G_OBJECT (chat),
589                                         "chat-window-tab-close-button");
590                         gtk_widget_show (chat_close_button);
591                 }
592         }
593 }
594
595 static void
596 chat_window_update (EmpathyChatWindow *window,
597                     gboolean update_contact_menu)
598 {
599         EmpathyChatWindowPriv *priv = GET_PRIV (window);
600         gint                   num_pages;
601
602         num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
603
604         /* Update Tab menu */
605         chat_window_menu_context_update (priv,
606                                          num_pages);
607
608         chat_window_conversation_menu_update (priv, window);
609
610         /* If this update is due to a focus-in event, we know the menu will be
611            the same as when we last left it, so no work to do.  Besides, if we
612            swap out the menu on a focus-in, we may confuse any external global
613            menu watching. */
614         if (update_contact_menu) {
615                 chat_window_contact_menu_update (priv,
616                                                  window);
617         }
618
619         chat_window_title_update (priv);
620
621         chat_window_icon_update (priv, get_all_unread_messages (priv) > 0);
622
623         chat_window_close_button_update (priv,
624                                          num_pages);
625 }
626
627 static void
628 append_markup_printf (GString    *string,
629                       const char *format,
630                       ...)
631 {
632         gchar *tmp;
633         va_list args;
634
635         va_start (args, format);
636
637         tmp = g_markup_vprintf_escaped (format, args);
638         g_string_append (string, tmp);
639         g_free (tmp);
640
641         va_end (args);
642 }
643
644 static void
645 chat_window_update_chat_tab_full (EmpathyChat *chat,
646                                   gboolean update_contact_menu)
647 {
648         EmpathyChatWindow     *window;
649         EmpathyChatWindowPriv *priv;
650         EmpathyContact        *remote_contact;
651         gchar                 *name;
652         const gchar           *id;
653         TpAccount             *account;
654         const gchar           *subject;
655         const gchar           *status = NULL;
656         GtkWidget             *widget;
657         GString               *tooltip;
658         gchar                 *markup;
659         const gchar           *icon_name;
660         GtkWidget             *tab_image;
661         GtkWidget             *menu_image;
662         GtkWidget             *sending_spinner;
663         guint                  nb_sending;
664
665         window = chat_window_find_chat (chat);
666         if (!window) {
667                 return;
668         }
669         priv = GET_PRIV (window);
670
671         /* Get information */
672         name = empathy_chat_dup_name (chat);
673         account = empathy_chat_get_account (chat);
674         subject = empathy_chat_get_subject (chat);
675         remote_contact = empathy_chat_get_remote_contact (chat);
676
677         DEBUG ("Updating chat tab, name=%s, account=%s, subject=%s, remote_contact=%p",
678                 name, tp_proxy_get_object_path (account), subject, remote_contact);
679
680         /* Update tab image */
681         if (empathy_chat_get_tp_chat (chat) == NULL) {
682                 /* No TpChat, we are disconnected */
683                 icon_name = NULL;
684         }
685         else if (empathy_chat_get_nb_unread_messages (chat) > 0) {
686                 icon_name = EMPATHY_IMAGE_MESSAGE;
687         }
688         else if (remote_contact && empathy_chat_is_composing (chat)) {
689                 icon_name = EMPATHY_IMAGE_TYPING;
690         }
691         else if (empathy_chat_is_sms_channel (chat)) {
692                 icon_name = EMPATHY_IMAGE_SMS;
693         }
694         else if (remote_contact) {
695                 icon_name = empathy_icon_name_for_contact (remote_contact);
696         } else {
697                 icon_name = EMPATHY_IMAGE_GROUP_MESSAGE;
698         }
699
700         tab_image = g_object_get_data (G_OBJECT (chat), "chat-window-tab-image");
701         menu_image = g_object_get_data (G_OBJECT (chat), "chat-window-menu-image");
702         if (icon_name != NULL) {
703                 gtk_image_set_from_icon_name (GTK_IMAGE (tab_image), icon_name, GTK_ICON_SIZE_MENU);
704                 gtk_widget_show (tab_image);
705                 gtk_image_set_from_icon_name (GTK_IMAGE (menu_image), icon_name, GTK_ICON_SIZE_MENU);
706                 gtk_widget_show (menu_image);
707         } else {
708                 gtk_widget_hide (tab_image);
709                 gtk_widget_hide (menu_image);
710         }
711
712         /* Update the sending spinner */
713         nb_sending = empathy_chat_get_n_messages_sending (chat);
714         sending_spinner = g_object_get_data (G_OBJECT (chat),
715                 "chat-window-tab-sending-spinner");
716
717         g_object_set (sending_spinner,
718                 "active", nb_sending > 0,
719                 "visible", nb_sending > 0,
720                 NULL);
721
722         /* Update tab tooltip */
723         tooltip = g_string_new (NULL);
724
725         if (remote_contact) {
726                 id = empathy_contact_get_id (remote_contact);
727                 status = empathy_contact_get_presence_message (remote_contact);
728         } else {
729                 id = name;
730         }
731
732         if (empathy_chat_is_sms_channel (chat)) {
733                 append_markup_printf (tooltip, "%s ", _("SMS:"));
734         }
735
736         append_markup_printf (tooltip,
737                               "<b>%s</b><small> (%s)</small>",
738                               id,
739                               tp_account_get_display_name (account));
740
741         if (nb_sending > 0) {
742                 char *tmp = g_strdup_printf (
743                         ngettext ("Sending %d message",
744                                   "Sending %d messages",
745                                   nb_sending),
746                         nb_sending);
747
748                 g_string_append (tooltip, "\n");
749                 g_string_append (tooltip, tmp);
750
751                 gtk_widget_set_tooltip_text (sending_spinner, tmp);
752                 g_free (tmp);
753         }
754
755         if (!EMP_STR_EMPTY (status)) {
756                 append_markup_printf (tooltip, "\n<i>%s</i>", status);
757         }
758
759         if (subject) {
760                 append_markup_printf (tooltip, "\n<b>%s</b> %s",
761                                       _("Topic:"), subject);
762         }
763
764         if (remote_contact && empathy_chat_is_composing (chat)) {
765                 append_markup_printf (tooltip, "\n%s", _("Typing a message."));
766         }
767
768         markup = g_string_free (tooltip, FALSE);
769         widget = g_object_get_data (G_OBJECT (chat), "chat-window-tab-tooltip-widget");
770         gtk_widget_set_tooltip_markup (widget, markup);
771         widget = g_object_get_data (G_OBJECT (chat), "chat-window-menu-tooltip-widget");
772         gtk_widget_set_tooltip_markup (widget, markup);
773         g_free (markup);
774
775         /* Update tab and menu label */
776         widget = g_object_get_data (G_OBJECT (chat), "chat-window-tab-label");
777         gtk_label_set_text (GTK_LABEL (widget), name);
778         widget = g_object_get_data (G_OBJECT (chat), "chat-window-menu-label");
779         gtk_label_set_text (GTK_LABEL (widget), name);
780
781         /* Update the window if it's the current chat */
782         if (priv->current_chat == chat) {
783                 chat_window_update (window, update_contact_menu);
784         }
785
786         g_free (name);
787 }
788
789 static void
790 chat_window_update_chat_tab (EmpathyChat *chat)
791 {
792         chat_window_update_chat_tab_full (chat, TRUE);
793 }
794
795 static void
796 chat_window_chat_notify_cb (EmpathyChat *chat)
797 {
798         EmpathyChatWindow *window;
799         EmpathyContact *old_remote_contact;
800         EmpathyContact *remote_contact = NULL;
801
802         old_remote_contact = g_object_get_data (G_OBJECT (chat), "chat-window-remote-contact");
803         remote_contact = empathy_chat_get_remote_contact (chat);
804
805         if (old_remote_contact != remote_contact) {
806                 /* The remote-contact associated with the chat changed, we need
807                  * to keep track of any change of that contact and update the
808                  * window each time. */
809                 if (remote_contact) {
810                         g_signal_connect_swapped (remote_contact, "notify",
811                                                   G_CALLBACK (chat_window_update_chat_tab),
812                                                   chat);
813                 }
814                 if (old_remote_contact) {
815                         g_signal_handlers_disconnect_by_func (old_remote_contact,
816                                                               chat_window_update_chat_tab,
817                                                               chat);
818                 }
819
820                 g_object_set_data_full (G_OBJECT (chat), "chat-window-remote-contact",
821                                    g_object_ref (remote_contact), (GDestroyNotify) g_object_unref);
822         }
823
824         chat_window_update_chat_tab (chat);
825
826         window = chat_window_find_chat (chat);
827         if (window != NULL) {
828                 chat_window_update (window, FALSE);
829         }
830 }
831
832 static void
833 chat_window_insert_smiley_activate_cb (EmpathySmileyManager *manager,
834                                        EmpathySmiley        *smiley,
835                                        gpointer              window)
836 {
837         EmpathyChatWindowPriv *priv = GET_PRIV (window);
838         EmpathyChat           *chat;
839         GtkTextBuffer         *buffer;
840         GtkTextIter            iter;
841
842         chat = priv->current_chat;
843
844         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
845         gtk_text_buffer_get_end_iter (buffer, &iter);
846         gtk_text_buffer_insert (buffer, &iter, smiley->str, -1);
847 }
848
849 static void
850 chat_window_conv_activate_cb (GtkAction         *action,
851                               EmpathyChatWindow *window)
852 {
853         EmpathyChatWindowPriv *priv = GET_PRIV (window);
854         gboolean               is_room;
855         gboolean               active;
856         EmpathyContact        *remote_contact = NULL;
857
858         /* Favorite room menu */
859         is_room = empathy_chat_is_room (priv->current_chat);
860         if (is_room) {
861                 const gchar *room;
862                 TpAccount   *account;
863                 gboolean     found = FALSE;
864                 EmpathyChatroom *chatroom;
865
866                 room = empathy_chat_get_id (priv->current_chat);
867                 account = empathy_chat_get_account (priv->current_chat);
868                 chatroom = empathy_chatroom_manager_find (priv->chatroom_manager,
869                                                        account, room);
870                 if (chatroom != NULL)
871                         found = empathy_chatroom_is_favorite (chatroom);
872
873                 DEBUG ("This room %s favorite", found ? "is" : "is not");
874                 gtk_toggle_action_set_active (
875                         GTK_TOGGLE_ACTION (priv->menu_conv_favorite), found);
876
877                 if (chatroom != NULL)
878                         found = empathy_chatroom_is_always_urgent (chatroom);
879
880                 gtk_toggle_action_set_active (
881                         GTK_TOGGLE_ACTION (priv->menu_conv_always_urgent),
882                         found);
883         }
884         gtk_action_set_visible (priv->menu_conv_favorite, is_room);
885         gtk_action_set_visible (priv->menu_conv_always_urgent, is_room);
886
887         /* Show contacts menu */
888         g_object_get (priv->current_chat,
889                       "remote-contact", &remote_contact,
890                       "show-contacts", &active,
891                       NULL);
892         if (remote_contact == NULL) {
893                 gtk_toggle_action_set_active (
894                         GTK_TOGGLE_ACTION (priv->menu_conv_toggle_contacts),
895                                            active);
896         }
897         gtk_action_set_visible (priv->menu_conv_toggle_contacts,
898                                 (remote_contact == NULL));
899         if (remote_contact != NULL) {
900                 g_object_unref (remote_contact);
901         }
902 }
903
904 static void
905 chat_window_clear_activate_cb (GtkAction         *action,
906                                EmpathyChatWindow *window)
907 {
908         EmpathyChatWindowPriv *priv = GET_PRIV (window);
909
910         empathy_chat_clear (priv->current_chat);
911 }
912
913 static void
914 chat_window_favorite_toggled_cb (GtkToggleAction   *toggle_action,
915                                  EmpathyChatWindow *window)
916 {
917         EmpathyChatWindowPriv *priv = GET_PRIV (window);
918         gboolean               active;
919         TpAccount             *account;
920         gchar                 *name;
921         const gchar           *room;
922         EmpathyChatroom       *chatroom;
923
924         active = gtk_toggle_action_get_active (toggle_action);
925         account = empathy_chat_get_account (priv->current_chat);
926         room = empathy_chat_get_id (priv->current_chat);
927         name = empathy_chat_dup_name (priv->current_chat);
928
929         chatroom = empathy_chatroom_manager_ensure_chatroom (
930                      priv->chatroom_manager,
931                      account,
932                      room,
933                      name);
934
935         empathy_chatroom_set_favorite (chatroom, active);
936         g_object_unref (chatroom);
937         g_free (name);
938 }
939
940 static void
941 chat_window_always_urgent_toggled_cb (GtkToggleAction   *toggle_action,
942                                  EmpathyChatWindow *window)
943 {
944         EmpathyChatWindowPriv *priv = GET_PRIV (window);
945         gboolean               active;
946         TpAccount             *account;
947         gchar                 *name;
948         const gchar           *room;
949         EmpathyChatroom       *chatroom;
950
951         active = gtk_toggle_action_get_active (toggle_action);
952         account = empathy_chat_get_account (priv->current_chat);
953         room = empathy_chat_get_id (priv->current_chat);
954         name = empathy_chat_dup_name (priv->current_chat);
955
956         chatroom = empathy_chatroom_manager_ensure_chatroom (
957                      priv->chatroom_manager,
958                      account,
959                      room,
960                      name);
961
962         empathy_chatroom_set_always_urgent (chatroom, active);
963         g_object_unref (chatroom);
964         g_free (name);
965 }
966
967 static void
968 chat_window_contacts_toggled_cb (GtkToggleAction   *toggle_action,
969                                  EmpathyChatWindow *window)
970 {
971         EmpathyChatWindowPriv *priv = GET_PRIV (window);
972         gboolean               active;
973
974         active = gtk_toggle_action_get_active (toggle_action);
975
976         empathy_chat_set_show_contacts (priv->current_chat, active);
977 }
978
979 static void
980 chat_window_invite_participant_activate_cb (GtkAction         *action,
981                                             EmpathyChatWindow *window)
982 {
983         EmpathyChatWindowPriv *priv;
984         GtkWidget             *dialog;
985         EmpathyTpChat         *tp_chat;
986         int                    response;
987
988         priv = GET_PRIV (window);
989
990         g_return_if_fail (priv->current_chat != NULL);
991
992         tp_chat = empathy_chat_get_tp_chat (priv->current_chat);
993
994         dialog = empathy_invite_participant_dialog_new (
995                         GTK_WINDOW (priv->dialog), tp_chat);
996         gtk_widget_show (dialog);
997
998         response = gtk_dialog_run (GTK_DIALOG (dialog));
999
1000         if (response == GTK_RESPONSE_ACCEPT) {
1001                 TpContact *tp_contact;
1002                 EmpathyContact *contact;
1003
1004                 tp_contact = empathy_invite_participant_dialog_get_selected (
1005                         EMPATHY_INVITE_PARTICIPANT_DIALOG (dialog));
1006                 if (tp_contact == NULL) goto out;
1007
1008                 contact = empathy_contact_dup_from_tp_contact (tp_contact);
1009
1010                 empathy_contact_list_add (EMPATHY_CONTACT_LIST (tp_chat),
1011                                 contact, _("Inviting you to this room"));
1012
1013                 g_object_unref (contact);
1014         }
1015
1016 out:
1017         gtk_widget_destroy (dialog);
1018 }
1019
1020 static void
1021 chat_window_close_activate_cb (GtkAction         *action,
1022                                EmpathyChatWindow *window)
1023 {
1024         EmpathyChatWindowPriv *priv;
1025
1026         priv = GET_PRIV (window);
1027
1028         g_return_if_fail (priv->current_chat != NULL);
1029
1030         empathy_chat_window_remove_chat (window, priv->current_chat);
1031 }
1032
1033 static void
1034 chat_window_edit_activate_cb (GtkAction         *action,
1035                               EmpathyChatWindow *window)
1036 {
1037         EmpathyChatWindowPriv *priv;
1038         GtkClipboard         *clipboard;
1039         GtkTextBuffer        *buffer;
1040         gboolean              text_available;
1041
1042         priv = GET_PRIV (window);
1043
1044         g_return_if_fail (priv->current_chat != NULL);
1045
1046         if (!empathy_chat_get_tp_chat (priv->current_chat)) {
1047                 gtk_action_set_sensitive (priv->menu_edit_copy, FALSE);
1048                 gtk_action_set_sensitive (priv->menu_edit_cut, FALSE);
1049                 gtk_action_set_sensitive (priv->menu_edit_paste, FALSE);
1050                 return;
1051         }
1052
1053         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->current_chat->input_text_view));
1054         if (gtk_text_buffer_get_has_selection (buffer)) {
1055                 gtk_action_set_sensitive (priv->menu_edit_copy, TRUE);
1056                 gtk_action_set_sensitive (priv->menu_edit_cut, TRUE);
1057         } else {
1058                 gboolean selection;
1059
1060                 selection = empathy_chat_view_get_has_selection (priv->current_chat->view);
1061
1062                 gtk_action_set_sensitive (priv->menu_edit_cut, FALSE);
1063                 gtk_action_set_sensitive (priv->menu_edit_copy, selection);
1064         }
1065
1066         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1067         text_available = gtk_clipboard_wait_is_text_available (clipboard);
1068         gtk_action_set_sensitive (priv->menu_edit_paste, text_available);
1069 }
1070
1071 static void
1072 chat_window_cut_activate_cb (GtkAction         *action,
1073                              EmpathyChatWindow *window)
1074 {
1075         EmpathyChatWindowPriv *priv;
1076
1077         g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (window));
1078
1079         priv = GET_PRIV (window);
1080
1081         empathy_chat_cut (priv->current_chat);
1082 }
1083
1084 static void
1085 chat_window_copy_activate_cb (GtkAction         *action,
1086                               EmpathyChatWindow *window)
1087 {
1088         EmpathyChatWindowPriv *priv;
1089
1090         g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (window));
1091
1092         priv = GET_PRIV (window);
1093
1094         empathy_chat_copy (priv->current_chat);
1095 }
1096
1097 static void
1098 chat_window_paste_activate_cb (GtkAction         *action,
1099                                EmpathyChatWindow *window)
1100 {
1101         EmpathyChatWindowPriv *priv;
1102
1103         g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (window));
1104
1105         priv = GET_PRIV (window);
1106
1107         empathy_chat_paste (priv->current_chat);
1108 }
1109
1110 static void
1111 chat_window_find_activate_cb (GtkAction         *action,
1112                               EmpathyChatWindow *window)
1113 {
1114         EmpathyChatWindowPriv *priv;
1115
1116         g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (window));
1117
1118         priv = GET_PRIV (window);
1119
1120         empathy_chat_find (priv->current_chat);
1121 }
1122
1123 static void
1124 chat_window_tabs_next_activate_cb (GtkAction         *action,
1125                                    EmpathyChatWindow *window)
1126 {
1127         EmpathyChatWindowPriv *priv;
1128         gint                  index_, numPages;
1129         gboolean              wrap_around;
1130
1131         priv = GET_PRIV (window);
1132
1133         g_object_get (gtk_settings_get_default (), "gtk-keynav-wrap-around",
1134                       &wrap_around, NULL);
1135
1136         index_ = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
1137         numPages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
1138
1139         if (index_ == (numPages - 1) && wrap_around) {
1140                 gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), 0);
1141                 return;
1142         }
1143
1144         gtk_notebook_next_page (GTK_NOTEBOOK (priv->notebook));
1145 }
1146
1147 static void
1148 chat_window_tabs_previous_activate_cb (GtkAction         *action,
1149                                    EmpathyChatWindow *window)
1150 {
1151         EmpathyChatWindowPriv *priv;
1152         gint                  index_, numPages;
1153         gboolean              wrap_around;
1154
1155         priv = GET_PRIV (window);
1156
1157         g_object_get (gtk_settings_get_default (), "gtk-keynav-wrap-around",
1158                       &wrap_around, NULL);
1159
1160         index_ = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
1161         numPages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
1162
1163         if (index_ <= 0 && wrap_around) {
1164                 gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), numPages - 1);
1165                 return;
1166         }
1167
1168         gtk_notebook_prev_page (GTK_NOTEBOOK (priv->notebook));
1169 }
1170
1171 static void
1172 chat_window_tabs_undo_close_tab_activate_cb (GtkAction         *action,
1173                                              EmpathyChatWindow *window)
1174 {
1175         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1176         empathy_chat_manager_undo_closed_chat (priv->chat_manager,
1177                                                empathy_get_current_action_time ());
1178 }
1179
1180 static void
1181 chat_window_tabs_left_activate_cb (GtkAction         *action,
1182                                    EmpathyChatWindow *window)
1183 {
1184         EmpathyChatWindowPriv *priv;
1185         EmpathyChat           *chat;
1186         gint                  index_, num_pages;
1187
1188         priv = GET_PRIV (window);
1189
1190         chat = priv->current_chat;
1191         index_ = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
1192         if (index_ <= 0) {
1193                 return;
1194         }
1195
1196         gtk_notebook_reorder_child (GTK_NOTEBOOK (priv->notebook),
1197                                     GTK_WIDGET (chat),
1198                                     index_ - 1);
1199
1200         num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
1201         chat_window_menu_context_update (priv, num_pages);
1202 }
1203
1204 static void
1205 chat_window_tabs_right_activate_cb (GtkAction         *action,
1206                                     EmpathyChatWindow *window)
1207 {
1208         EmpathyChatWindowPriv *priv;
1209         EmpathyChat           *chat;
1210         gint                  index_, num_pages;
1211
1212         priv = GET_PRIV (window);
1213
1214         chat = priv->current_chat;
1215         index_ = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
1216
1217         gtk_notebook_reorder_child (GTK_NOTEBOOK (priv->notebook),
1218                                     GTK_WIDGET (chat),
1219                                     index_ + 1);
1220
1221         num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
1222         chat_window_menu_context_update (priv, num_pages);
1223 }
1224
1225 static EmpathyChatWindow *
1226 empathy_chat_window_new (void)
1227 {
1228         return EMPATHY_CHAT_WINDOW (g_object_new (EMPATHY_TYPE_CHAT_WINDOW, NULL));
1229 }
1230
1231 static void
1232 chat_window_detach_activate_cb (GtkAction         *action,
1233                                 EmpathyChatWindow *window)
1234 {
1235         EmpathyChatWindowPriv *priv;
1236         EmpathyChatWindow     *new_window;
1237         EmpathyChat           *chat;
1238
1239         priv = GET_PRIV (window);
1240
1241         chat = priv->current_chat;
1242         new_window = empathy_chat_window_new ();
1243
1244         empathy_chat_window_move_chat (window, new_window, chat);
1245
1246         priv = GET_PRIV (new_window);
1247         gtk_widget_show (priv->dialog);
1248 }
1249
1250 static void
1251 chat_window_help_contents_activate_cb (GtkAction         *action,
1252                                        EmpathyChatWindow *window)
1253 {
1254         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1255
1256         empathy_url_show (priv->dialog, "ghelp:empathy");
1257 }
1258
1259 static void
1260 chat_window_help_about_activate_cb (GtkAction         *action,
1261                                     EmpathyChatWindow *window)
1262 {
1263         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1264
1265         empathy_about_dialog_new (GTK_WINDOW (priv->dialog));
1266 }
1267
1268 static gboolean
1269 chat_window_delete_event_cb (GtkWidget        *dialog,
1270                              GdkEvent         *event,
1271                              EmpathyChatWindow *window)
1272 {
1273         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1274
1275         DEBUG ("Delete event received");
1276
1277         g_object_ref (window);
1278         while (priv->chats) {
1279                 empathy_chat_window_remove_chat (window, priv->chats->data);
1280         }
1281         g_object_unref (window);
1282
1283         return TRUE;
1284 }
1285
1286 static void
1287 chat_window_composing_cb (EmpathyChat       *chat,
1288                           gboolean          is_composing,
1289                           EmpathyChatWindow *window)
1290 {
1291         chat_window_update_chat_tab (chat);
1292 }
1293
1294 static void
1295 chat_window_set_urgency_hint (EmpathyChatWindow *window,
1296                               gboolean          urgent)
1297 {
1298         EmpathyChatWindowPriv *priv;
1299
1300         priv = GET_PRIV (window);
1301
1302         gtk_window_set_urgency_hint (GTK_WINDOW (priv->dialog), urgent);
1303 }
1304
1305 static void
1306 chat_window_notification_closed_cb (NotifyNotification *notify,
1307                                     EmpathyChatWindow *self)
1308 {
1309         EmpathyChatWindowPriv *priv = GET_PRIV (self);
1310
1311         g_object_unref (notify);
1312         if (priv->notification == notify) {
1313                 priv->notification = NULL;
1314         }
1315 }
1316
1317 static void
1318 chat_window_show_or_update_notification (EmpathyChatWindow *window,
1319                                          EmpathyMessage *message,
1320                                          EmpathyChat *chat)
1321 {
1322         EmpathyContact *sender;
1323         const gchar *header;
1324         char *escaped;
1325         const char *body;
1326         GdkPixbuf *pixbuf;
1327         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1328         gboolean res, has_x_canonical_append;
1329         NotifyNotification *notification = priv->notification;
1330
1331         if (!empathy_notify_manager_notification_is_enabled (priv->notify_mgr)) {
1332                 return;
1333         } else {
1334                 res = g_settings_get_boolean (priv->gsettings_notif,
1335                                 EMPATHY_PREFS_NOTIFICATIONS_FOCUS);
1336
1337                 if (!res) {
1338                         return;
1339                 }
1340         }
1341
1342         sender = empathy_message_get_sender (message);
1343         header = empathy_contact_get_alias (sender);
1344         body = empathy_message_get_body (message);
1345         escaped = g_markup_escape_text (body, -1);
1346         has_x_canonical_append = empathy_notify_manager_has_capability (
1347                 priv->notify_mgr, EMPATHY_NOTIFY_MANAGER_CAP_X_CANONICAL_APPEND);
1348
1349         if (notification != NULL && !has_x_canonical_append) {
1350                 /* if the notification server supports x-canonical-append, it is
1351                    better to not use notify_notification_update to avoid
1352                    overwriting the current notification message */
1353                 notify_notification_update (notification,
1354                                             header, escaped, NULL);
1355         } else {
1356                 /* if the notification server supports x-canonical-append,
1357                    the hint will be added, so that the message from the
1358                    just created notification will be automatically appended
1359                    to an existing notification with the same title.
1360                    In this way the previous message will not be lost: the new
1361                    message will appear below it, in the same notification */
1362                 notification = notify_notification_new (header, escaped, NULL);
1363
1364                 if (priv->notification == NULL) {
1365                         priv->notification = notification;
1366                 }
1367
1368                 notify_notification_set_timeout (notification, NOTIFY_EXPIRES_DEFAULT);
1369
1370                 tp_g_signal_connect_object (notification, "closed",
1371                                   G_CALLBACK (chat_window_notification_closed_cb), window, 0);
1372
1373                 if (has_x_canonical_append) {
1374                         /* We have to set a not empty string to keep libnotify happy */
1375                         notify_notification_set_hint_string (notification,
1376                                 EMPATHY_NOTIFY_MANAGER_CAP_X_CANONICAL_APPEND, "1");
1377                 }
1378
1379                 notify_notification_set_hint (notification,
1380                         EMPATHY_NOTIFY_MANAGER_CAP_CATEGORY,
1381                         g_variant_new_string ("im.received"));
1382         }
1383
1384         pixbuf = empathy_notify_manager_get_pixbuf_for_notification (priv->notify_mgr,
1385                 sender, EMPATHY_IMAGE_NEW_MESSAGE);
1386
1387         if (pixbuf != NULL) {
1388                 notify_notification_set_icon_from_pixbuf (notification, pixbuf);
1389                 g_object_unref (pixbuf);
1390         }
1391
1392         notify_notification_show (notification, NULL);
1393
1394         g_free (escaped);
1395 }
1396
1397 static void
1398 chat_window_set_highlight_room_labels (EmpathyChat *chat)
1399 {
1400         gchar *markup, *name;
1401         GtkWidget *widget;
1402
1403         if (!empathy_chat_is_room (chat))
1404                 return;
1405
1406         name = empathy_chat_dup_name (chat);
1407         markup = g_markup_printf_escaped (
1408                 "<span color=\"red\" weight=\"bold\">%s</span>",
1409                 name);
1410
1411         widget = g_object_get_data (G_OBJECT (chat), "chat-window-tab-label");
1412         gtk_label_set_markup (GTK_LABEL (widget), markup);
1413
1414         widget = g_object_get_data (G_OBJECT (chat), "chat-window-menu-label");
1415         gtk_label_set_markup (GTK_LABEL (widget), markup);
1416
1417         g_free (name);
1418         g_free (markup);
1419 }
1420
1421 static gboolean
1422 empathy_chat_window_has_focus (EmpathyChatWindow *window)
1423 {
1424         EmpathyChatWindowPriv *priv;
1425         gboolean              has_focus;
1426
1427         g_return_val_if_fail (EMPATHY_IS_CHAT_WINDOW (window), FALSE);
1428
1429         priv = GET_PRIV (window);
1430
1431         g_object_get (priv->dialog, "has-toplevel-focus", &has_focus, NULL);
1432
1433         return has_focus;
1434 }
1435
1436 static void
1437 chat_window_new_message_cb (EmpathyChat       *chat,
1438                             EmpathyMessage    *message,
1439                             gboolean pending,
1440                             gboolean should_highlight,
1441                             EmpathyChatWindow *window)
1442 {
1443         EmpathyChatWindowPriv *priv;
1444         gboolean              has_focus;
1445         gboolean              needs_urgency;
1446         EmpathyContact        *sender;
1447
1448         priv = GET_PRIV (window);
1449
1450         has_focus = empathy_chat_window_has_focus (window);
1451
1452         /* - if we're the sender, we play the sound if it's specified in the
1453          *   preferences and we're not away.
1454          * - if we receive a message, we play the sound if it's specified in the
1455          *   preferences and the window does not have focus on the chat receiving
1456          *   the message.
1457          */
1458
1459         sender = empathy_message_get_sender (message);
1460
1461         if (empathy_contact_is_user (sender)) {
1462                 empathy_sound_manager_play (priv->sound_mgr, GTK_WIDGET (priv->dialog),
1463                                     EMPATHY_SOUND_MESSAGE_OUTGOING);
1464         }
1465
1466         if (has_focus && priv->current_chat == chat) {
1467                 /* window and tab are focused so consider the message to be read */
1468
1469                 /* FIXME: see Bug#610994 and coments about it in EmpathyChatPriv */
1470                 empathy_chat_messages_read (chat);
1471                 return;
1472         }
1473
1474         /* Update the chat tab if this is the first unread message */
1475         if (empathy_chat_get_nb_unread_messages (chat) == 1) {
1476                 chat_window_update_chat_tab (chat);
1477         }
1478
1479         /* If empathy_chat_is_room () returns TRUE, that means it's a named MUC.
1480          * If empathy_chat_get_remote_contact () returns NULL, that means it's
1481          * an unamed MUC (msn-like).
1482          * In case of a MUC, we set urgency if either:
1483          *   a) the chatroom's always_urgent property is TRUE
1484          *   b) the message contains our alias
1485          */
1486         if (empathy_chat_is_room (chat)) {
1487                 TpAccount             *account;
1488                 const gchar           *room;
1489                 EmpathyChatroom       *chatroom;
1490
1491                 account = empathy_chat_get_account (chat);
1492                 room = empathy_chat_get_id (chat);
1493
1494                 chatroom = empathy_chatroom_manager_find (priv->chatroom_manager,
1495                                                           account, room);
1496
1497                 if (chatroom != NULL && empathy_chatroom_is_always_urgent (chatroom)) {
1498                         needs_urgency = TRUE;
1499                 } else {
1500                         needs_urgency = should_highlight;
1501                 }
1502         } else {
1503                 needs_urgency = TRUE;
1504         }
1505
1506         if (needs_urgency) {
1507                 chat_window_set_highlight_room_labels (chat);
1508
1509                 if (!has_focus) {
1510                         chat_window_set_urgency_hint (window, TRUE);
1511                 }
1512
1513                 /* Pending messages have already been displayed and notified in the
1514                 * approver, so we don't display a notification and play a sound for those */
1515                 if (!pending) {
1516                         empathy_sound_manager_play (priv->sound_mgr, GTK_WIDGET (priv->dialog),
1517                                     EMPATHY_SOUND_MESSAGE_INCOMING);
1518
1519                         chat_window_show_or_update_notification (window, message, chat);
1520                 }
1521         }
1522
1523         /* update the number of unread messages and the window icon */
1524         chat_window_title_update (priv);
1525         chat_window_icon_update (priv, TRUE);
1526 }
1527
1528 static void
1529 chat_window_command_part (EmpathyChat *chat,
1530                            GStrv        strv)
1531 {
1532         EmpathyChat *chat_to_be_parted;
1533         EmpathyTpChat *tp_chat = NULL;
1534
1535         if (strv[1] == NULL) {
1536                 /* No chatroom ID specified  */
1537                 tp_chat = empathy_chat_get_tp_chat (chat);
1538                 if (tp_chat)
1539                         empathy_tp_chat_leave (tp_chat, "");
1540                 return;
1541         }
1542         chat_to_be_parted = empathy_chat_window_find_chat (
1543                 empathy_chat_get_account (chat), strv[1], FALSE);
1544
1545         if (chat_to_be_parted != NULL) {
1546                 /* Found a chatroom matching the specified ID */
1547                 tp_chat = empathy_chat_get_tp_chat (chat_to_be_parted);
1548                 if (tp_chat)
1549                         empathy_tp_chat_leave (tp_chat, strv[2]);
1550         } else {
1551                 gchar *message;
1552
1553                 /* Going by the syntax of PART command:
1554                  *
1555                  * /PART [<chatroom-ID>] [<reason>]
1556                  *
1557                  * Chatroom-ID is not a must to specify a reason.
1558                  * If strv[1] (chatroom-ID) is not a valid identifier for a connected
1559                  * MUC then the current chatroom should be parted and srtv[1] should
1560                  * be treated as part of the optional part-message. */
1561                 message = g_strconcat (strv[1], " ", strv[2], NULL);
1562                 tp_chat = empathy_chat_get_tp_chat (chat);
1563                 if (tp_chat)
1564                         empathy_tp_chat_leave (tp_chat, message);
1565
1566                 g_free (message);
1567         }
1568 }
1569
1570 static GtkNotebook *
1571 notebook_create_window_cb (GtkNotebook *source,
1572                          GtkWidget   *page,
1573                          gint         x,
1574                          gint         y,
1575                          gpointer     user_data)
1576 {
1577         EmpathyChatWindowPriv *priv;
1578         EmpathyChatWindow     *window, *new_window;
1579         EmpathyChat           *chat;
1580
1581         chat = EMPATHY_CHAT (page);
1582         window = chat_window_find_chat (chat);
1583
1584         new_window = empathy_chat_window_new ();
1585         priv = GET_PRIV (new_window);
1586
1587         DEBUG ("Detach hook called");
1588
1589         empathy_chat_window_move_chat (window, new_window, chat);
1590
1591         gtk_widget_show (priv->dialog);
1592         gtk_window_move (GTK_WINDOW (priv->dialog), x, y);
1593
1594         return NULL;
1595 }
1596
1597 static void
1598 chat_window_page_switched_cb (GtkNotebook      *notebook,
1599                               GtkWidget         *child,
1600                               gint              page_num,
1601                               EmpathyChatWindow *window)
1602 {
1603         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1604         EmpathyChat           *chat = EMPATHY_CHAT (child);
1605
1606         DEBUG ("Page switched");
1607
1608         if (priv->page_added) {
1609                 priv->page_added = FALSE;
1610                 empathy_chat_scroll_down (chat);
1611         }
1612         else if (priv->current_chat == chat) {
1613                 return;
1614         }
1615
1616         priv->current_chat = chat;
1617         empathy_chat_messages_read (chat);
1618
1619         chat_window_update_chat_tab (chat);
1620 }
1621
1622 static void
1623 chat_window_page_added_cb (GtkNotebook      *notebook,
1624                            GtkWidget        *child,
1625                            guint             page_num,
1626                            EmpathyChatWindow *window)
1627 {
1628         EmpathyChatWindowPriv *priv;
1629         EmpathyChat           *chat;
1630
1631         priv = GET_PRIV (window);
1632
1633         /* If we just received DND to the same window, we don't want
1634          * to do anything here like removing the tab and then readding
1635          * it, so we return here and in "page-added".
1636          */
1637         if (priv->dnd_same_window) {
1638                 DEBUG ("Page added (back to the same window)");
1639                 priv->dnd_same_window = FALSE;
1640                 return;
1641         }
1642
1643         DEBUG ("Page added");
1644
1645         /* Get chat object */
1646         chat = EMPATHY_CHAT (child);
1647
1648         /* Connect chat signals for this window */
1649         g_signal_connect (chat, "composing",
1650                           G_CALLBACK (chat_window_composing_cb),
1651                           window);
1652         g_signal_connect (chat, "new-message",
1653                           G_CALLBACK (chat_window_new_message_cb),
1654                           window);
1655         g_signal_connect (chat, "part-command-entered",
1656                           G_CALLBACK (chat_window_command_part),
1657                           NULL);
1658         g_signal_connect (chat, "notify::tp-chat",
1659                           G_CALLBACK (chat_window_update_chat_tab),
1660                           window);
1661
1662         /* Set flag so we know to perform some special operations on
1663          * switch page due to the new page being added.
1664          */
1665         priv->page_added = TRUE;
1666
1667         /* Get list of chats up to date */
1668         priv->chats = g_list_append (priv->chats, chat);
1669
1670         chat_window_update_chat_tab (chat);
1671 }
1672
1673 static void
1674 chat_window_page_removed_cb (GtkNotebook      *notebook,
1675                              GtkWidget        *child,
1676                              guint             page_num,
1677                              EmpathyChatWindow *window)
1678 {
1679         EmpathyChatWindowPriv *priv;
1680         EmpathyChat           *chat;
1681
1682         priv = GET_PRIV (window);
1683
1684         /* If we just received DND to the same window, we don't want
1685          * to do anything here like removing the tab and then readding
1686          * it, so we return here and in "page-added".
1687          */
1688         if (priv->dnd_same_window) {
1689                 DEBUG ("Page removed (and will be readded to same window)");
1690                 return;
1691         }
1692
1693         DEBUG ("Page removed");
1694
1695         /* Get chat object */
1696         chat = EMPATHY_CHAT (child);
1697
1698         /* Disconnect all signal handlers for this chat and this window */
1699         g_signal_handlers_disconnect_by_func (chat,
1700                                               G_CALLBACK (chat_window_composing_cb),
1701                                               window);
1702         g_signal_handlers_disconnect_by_func (chat,
1703                                               G_CALLBACK (chat_window_new_message_cb),
1704                                               window);
1705         g_signal_handlers_disconnect_by_func (chat,
1706                                               G_CALLBACK (chat_window_update_chat_tab),
1707                                               window);
1708
1709         /* Keep list of chats up to date */
1710         priv->chats = g_list_remove (priv->chats, chat);
1711         empathy_chat_messages_read (chat);
1712
1713         if (priv->chats == NULL) {
1714                 g_object_unref (window);
1715         } else {
1716                 chat_window_update (window, TRUE);
1717         }
1718 }
1719
1720 static gboolean
1721 chat_window_focus_in_event_cb (GtkWidget        *widget,
1722                                GdkEvent         *event,
1723                                EmpathyChatWindow *window)
1724 {
1725         EmpathyChatWindowPriv *priv;
1726
1727         priv = GET_PRIV (window);
1728
1729         empathy_chat_messages_read (priv->current_chat);
1730
1731         chat_window_set_urgency_hint (window, FALSE);
1732
1733         /* Update the title, since we now mark all unread messages as read. */
1734         chat_window_update_chat_tab_full (priv->current_chat, FALSE);
1735
1736         return FALSE;
1737 }
1738
1739 static gboolean
1740 chat_window_drag_drop (GtkWidget        *widget,
1741                          GdkDragContext   *context,
1742                          int               x,
1743                          int               y,
1744                          guint             time_,
1745                          EmpathyChatWindow *window)
1746 {
1747         GdkAtom target;
1748         EmpathyChatWindowPriv *priv;
1749
1750         priv = GET_PRIV (window);
1751
1752         target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
1753         if (target == GDK_NONE)
1754                 target = gtk_drag_dest_find_target (widget, context, priv->contact_targets);
1755
1756         if (target != GDK_NONE) {
1757                 gtk_drag_get_data (widget, context, target, time_);
1758                 return TRUE;
1759         }
1760
1761         return FALSE;
1762 }
1763
1764 static gboolean
1765 chat_window_drag_motion (GtkWidget        *widget,
1766                          GdkDragContext   *context,
1767                          int               x,
1768                          int               y,
1769                          guint             time_,
1770                          EmpathyChatWindow *window)
1771 {
1772         GdkAtom target;
1773         EmpathyChatWindowPriv *priv;
1774
1775         priv = GET_PRIV (window);
1776
1777         target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
1778         if (target != GDK_NONE) {
1779                 /* This is a file drag.  Ensure the contact is online and set the
1780                    drag type to COPY.  Note that it's possible that the tab will
1781                    be switched by GTK+ after a timeout from drag_motion without
1782                    getting another drag_motion to disable the drop.  You have
1783                    to hold your mouse really still.
1784                  */
1785                 EmpathyContact *contact;
1786
1787                 priv = GET_PRIV (window);
1788                 contact = empathy_chat_get_remote_contact (priv->current_chat);
1789                 /* contact is NULL for multi-user chats.  We don't do
1790                  * file transfers to MUCs.  We also don't send files
1791                  * to offline contacts or contacts that don't support
1792                  * file transfer.
1793                  */
1794                 if ((contact == NULL) || !empathy_contact_is_online (contact)) {
1795                         gdk_drag_status (context, 0, time_);
1796                         return FALSE;
1797                 }
1798                 if (!(empathy_contact_get_capabilities (contact)
1799                            & EMPATHY_CAPABILITIES_FT)) {
1800                         gdk_drag_status (context, 0, time_);
1801                         return FALSE;
1802                 }
1803                 gdk_drag_status (context, GDK_ACTION_COPY, time_);
1804                 return TRUE;
1805         }
1806
1807         target = gtk_drag_dest_find_target (widget, context, priv->contact_targets);
1808         if (target != GDK_NONE) {
1809                 /* This is a drag of a contact from a contact list.  Set to COPY.
1810                    FIXME: If this drag is to a MUC window, it invites the user.
1811                    Otherwise, it opens a chat.  Should we use a different drag
1812                    type for invites?  Should we allow ASK?
1813                  */
1814                 gdk_drag_status (context, GDK_ACTION_COPY, time_);
1815                 return TRUE;
1816         }
1817
1818         return FALSE;
1819 }
1820
1821 static void
1822 chat_window_drag_data_received (GtkWidget        *widget,
1823                                 GdkDragContext   *context,
1824                                 int               x,
1825                                 int               y,
1826                                 GtkSelectionData *selection,
1827                                 guint             info,
1828                                 guint             time_,
1829                                 EmpathyChatWindow *window)
1830 {
1831         if (info == DND_DRAG_TYPE_CONTACT_ID) {
1832                 EmpathyChat           *chat = NULL;
1833                 EmpathyChatWindow     *old_window;
1834                 TpAccount             *account = NULL;
1835                 EmpathyClientFactory  *factory;
1836                 const gchar           *id;
1837                 gchar                **strv;
1838                 const gchar           *account_id;
1839                 const gchar           *contact_id;
1840
1841                 id = (const gchar*) gtk_selection_data_get_data (selection);
1842
1843                 factory = empathy_client_factory_dup ();
1844
1845                 DEBUG ("DND contact from roster with id:'%s'", id);
1846
1847                 strv = g_strsplit (id, ":", 2);
1848                 if (g_strv_length (strv) == 2) {
1849                         account_id = strv[0];
1850                         contact_id = strv[1];
1851                         account =
1852                                 tp_simple_client_factory_ensure_account (
1853                                 TP_SIMPLE_CLIENT_FACTORY (factory), account_id,
1854                                  NULL, NULL);
1855
1856                         g_object_unref (factory);
1857                         if (account != NULL)
1858                                 chat = empathy_chat_window_find_chat (account, contact_id, FALSE);
1859                 }
1860
1861                 if (account == NULL) {
1862                         g_strfreev (strv);
1863                         gtk_drag_finish (context, FALSE, FALSE, time_);
1864                         return;
1865                 }
1866
1867                 if (!chat) {
1868                         empathy_chat_with_contact_id (
1869                                 account, contact_id,
1870                                 empathy_get_current_action_time (),
1871                                 NULL, NULL);
1872
1873                         g_strfreev (strv);
1874                         return;
1875                 }
1876                 g_strfreev (strv);
1877
1878                 old_window = chat_window_find_chat (chat);
1879                 if (old_window) {
1880                         if (old_window == window) {
1881                                 gtk_drag_finish (context, TRUE, FALSE, time_);
1882                                 return;
1883                         }
1884
1885                         empathy_chat_window_move_chat (old_window, window, chat);
1886                 } else {
1887                         empathy_chat_window_add_chat (window, chat);
1888                 }
1889
1890                 /* Added to take care of any outstanding chat events */
1891                 empathy_chat_window_present_chat (chat,
1892                         TP_USER_ACTION_TIME_NOT_USER_ACTION);
1893
1894                 /* We should return TRUE to remove the data when doing
1895                  * GDK_ACTION_MOVE, but we don't here otherwise it has
1896                  * weird consequences, and we handle that internally
1897                  * anyway with add_chat () and remove_chat ().
1898                  */
1899                 gtk_drag_finish (context, TRUE, FALSE, time_);
1900         }
1901         else if (info == DND_DRAG_TYPE_URI_LIST) {
1902                 EmpathyChatWindowPriv *priv;
1903                 EmpathyContact *contact;
1904                 const gchar *data;
1905
1906                 priv = GET_PRIV (window);
1907                 contact = empathy_chat_get_remote_contact (priv->current_chat);
1908
1909                 /* contact is NULL when current_chat is a multi-user chat.
1910                  * We don't do file transfers to MUCs, so just cancel the drag.
1911                  */
1912                 if (contact == NULL) {
1913                         gtk_drag_finish (context, TRUE, FALSE, time_);
1914                         return;
1915                 }
1916
1917                 data = (const gchar *) gtk_selection_data_get_data (selection);
1918                 empathy_send_file_from_uri_list (contact, data);
1919
1920                 gtk_drag_finish (context, TRUE, FALSE, time_);
1921         }
1922         else if (info == DND_DRAG_TYPE_TAB) {
1923                 EmpathyChat        **chat;
1924                 EmpathyChatWindow   *old_window = NULL;
1925
1926                 DEBUG ("DND tab");
1927
1928                 chat = (void *) gtk_selection_data_get_data (selection);
1929                 old_window = chat_window_find_chat (*chat);
1930
1931                 if (old_window) {
1932                         EmpathyChatWindowPriv *priv;
1933
1934                         priv = GET_PRIV (window);
1935                         priv->dnd_same_window = (old_window == window);
1936                         DEBUG ("DND tab (within same window: %s)",
1937                                 priv->dnd_same_window ? "Yes" : "No");
1938                 }
1939         } else {
1940                 DEBUG ("DND from unknown source");
1941                 gtk_drag_finish (context, FALSE, FALSE, time_);
1942         }
1943 }
1944
1945 static void
1946 chat_window_chat_manager_chats_changed_cb (EmpathyChatManager *chat_manager,
1947                                            guint num_chats_in_manager,
1948                                            EmpathyChatWindow *window)
1949 {
1950         EmpathyChatWindowPriv *priv = GET_PRIV (window);
1951
1952         gtk_action_set_sensitive (priv->menu_tabs_undo_close_tab,
1953                                   num_chats_in_manager > 0);
1954 }
1955
1956 static void
1957 chat_window_finalize (GObject *object)
1958 {
1959         EmpathyChatWindow     *window;
1960         EmpathyChatWindowPriv *priv;
1961
1962         window = EMPATHY_CHAT_WINDOW (object);
1963         priv = GET_PRIV (window);
1964
1965         DEBUG ("Finalized: %p", object);
1966
1967         g_object_unref (priv->ui_manager);
1968         g_object_unref (priv->chatroom_manager);
1969         g_object_unref (priv->notify_mgr);
1970         g_object_unref (priv->gsettings_chat);
1971         g_object_unref (priv->gsettings_notif);
1972         g_object_unref (priv->gsettings_ui);
1973         g_object_unref (priv->sound_mgr);
1974
1975         if (priv->notification != NULL) {
1976                 notify_notification_close (priv->notification, NULL);
1977                 priv->notification = NULL;
1978         }
1979
1980         if (priv->contact_targets) {
1981                 gtk_target_list_unref (priv->contact_targets);
1982         }
1983         if (priv->file_targets) {
1984                 gtk_target_list_unref (priv->file_targets);
1985         }
1986
1987         if (priv->chat_manager) {
1988                 g_signal_handler_disconnect (priv->chat_manager,
1989                                              priv->chat_manager_chats_changed_id);
1990                 g_object_unref (priv->chat_manager);
1991                 priv->chat_manager = NULL;
1992         }
1993
1994         chat_windows = g_list_remove (chat_windows, window);
1995         gtk_widget_destroy (priv->dialog);
1996
1997         G_OBJECT_CLASS (empathy_chat_window_parent_class)->finalize (object);
1998 }
1999
2000 static void
2001 empathy_chat_window_class_init (EmpathyChatWindowClass *klass)
2002 {
2003         GObjectClass *object_class = G_OBJECT_CLASS (klass);
2004
2005         object_class->finalize = chat_window_finalize;
2006
2007         g_type_class_add_private (object_class, sizeof (EmpathyChatWindowPriv));
2008 }
2009
2010 static void
2011 empathy_chat_window_init (EmpathyChatWindow *window)
2012 {
2013         GtkBuilder            *gui;
2014         GtkAccelGroup         *accel_group;
2015         GClosure              *closure;
2016         GtkWidget             *menu;
2017         GtkWidget             *submenu;
2018         guint                  i;
2019         GtkWidget             *chat_vbox;
2020         gchar                 *filename;
2021         EmpathySmileyManager  *smiley_manager;
2022         EmpathyChatWindowPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (window,
2023                 EMPATHY_TYPE_CHAT_WINDOW, EmpathyChatWindowPriv);
2024
2025         window->priv = priv;
2026         filename = empathy_file_lookup ("empathy-chat-window.ui", "src");
2027         gui = empathy_builder_get_file (filename,
2028                                        "chat_window", &priv->dialog,
2029                                        "chat_vbox", &chat_vbox,
2030                                        "ui_manager", &priv->ui_manager,
2031                                        "menu_conv_insert_smiley", &priv->menu_conv_insert_smiley,
2032                                        "menu_conv_favorite", &priv->menu_conv_favorite,
2033                                        "menu_conv_always_urgent", &priv->menu_conv_always_urgent,
2034                                        "menu_conv_toggle_contacts", &priv->menu_conv_toggle_contacts,
2035                                        "menu_edit_cut", &priv->menu_edit_cut,
2036                                        "menu_edit_copy", &priv->menu_edit_copy,
2037                                        "menu_edit_paste", &priv->menu_edit_paste,
2038                                        "menu_edit_find", &priv->menu_edit_find,
2039                                        "menu_tabs_next", &priv->menu_tabs_next,
2040                                        "menu_tabs_prev", &priv->menu_tabs_prev,
2041                                        "menu_tabs_undo_close_tab", &priv->menu_tabs_undo_close_tab,
2042                                        "menu_tabs_left", &priv->menu_tabs_left,
2043                                        "menu_tabs_right", &priv->menu_tabs_right,
2044                                        "menu_tabs_detach", &priv->menu_tabs_detach,
2045                                        NULL);
2046         g_free (filename);
2047
2048         empathy_builder_connect (gui, window,
2049                               "menu_conv", "activate", chat_window_conv_activate_cb,
2050                               "menu_conv_clear", "activate", chat_window_clear_activate_cb,
2051                               "menu_conv_favorite", "toggled", chat_window_favorite_toggled_cb,
2052                               "menu_conv_always_urgent", "toggled", chat_window_always_urgent_toggled_cb,
2053                               "menu_conv_toggle_contacts", "toggled", chat_window_contacts_toggled_cb,
2054                               "menu_conv_invite_participant", "activate", chat_window_invite_participant_activate_cb,
2055                               "menu_conv_close", "activate", chat_window_close_activate_cb,
2056                               "menu_edit", "activate", chat_window_edit_activate_cb,
2057                               "menu_edit_cut", "activate", chat_window_cut_activate_cb,
2058                               "menu_edit_copy", "activate", chat_window_copy_activate_cb,
2059                               "menu_edit_paste", "activate", chat_window_paste_activate_cb,
2060                               "menu_edit_find", "activate", chat_window_find_activate_cb,
2061                               "menu_tabs_next", "activate", chat_window_tabs_next_activate_cb,
2062                               "menu_tabs_prev", "activate", chat_window_tabs_previous_activate_cb,
2063                               "menu_tabs_undo_close_tab", "activate", chat_window_tabs_undo_close_tab_activate_cb,
2064                               "menu_tabs_left", "activate", chat_window_tabs_left_activate_cb,
2065                               "menu_tabs_right", "activate", chat_window_tabs_right_activate_cb,
2066                               "menu_tabs_detach", "activate", chat_window_detach_activate_cb,
2067                               "menu_help_contents", "activate", chat_window_help_contents_activate_cb,
2068                               "menu_help_about", "activate", chat_window_help_about_activate_cb,
2069                               NULL);
2070
2071         g_object_ref (priv->ui_manager);
2072         g_object_unref (gui);
2073
2074         priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
2075         priv->gsettings_notif = g_settings_new (EMPATHY_PREFS_NOTIFICATIONS_SCHEMA);
2076         priv->gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
2077         priv->chatroom_manager = empathy_chatroom_manager_dup_singleton (NULL);
2078
2079         priv->sound_mgr = empathy_sound_manager_dup_singleton ();
2080
2081         priv->notebook = gtk_notebook_new ();
2082
2083         g_signal_connect (priv->notebook, "create-window",
2084                 G_CALLBACK (notebook_create_window_cb), window);
2085
2086         gtk_notebook_set_group_name (GTK_NOTEBOOK (priv->notebook),
2087                 "EmpathyChatWindow");
2088         gtk_notebook_set_scrollable (GTK_NOTEBOOK (priv->notebook), TRUE);
2089         gtk_notebook_popup_enable (GTK_NOTEBOOK (priv->notebook));
2090         gtk_box_pack_start (GTK_BOX (chat_vbox), priv->notebook, TRUE, TRUE, 0);
2091         gtk_widget_show (priv->notebook);
2092
2093         /* Set up accels */
2094         accel_group = gtk_accel_group_new ();
2095         gtk_window_add_accel_group (GTK_WINDOW (priv->dialog), accel_group);
2096
2097         for (i = 0; i < G_N_ELEMENTS (tab_accel_keys); i++) {
2098                 closure =  g_cclosure_new (G_CALLBACK (chat_window_accel_cb),
2099                                            window,
2100                                            NULL);
2101                 gtk_accel_group_connect (accel_group,
2102                                          tab_accel_keys[i],
2103                                          GDK_MOD1_MASK,
2104                                          0,
2105                                          closure);
2106         }
2107
2108         g_object_unref (accel_group);
2109
2110         /* Set up drag target lists */
2111         priv->contact_targets = gtk_target_list_new (drag_types_dest_contact,
2112                                                      G_N_ELEMENTS (drag_types_dest_contact));
2113         priv->file_targets = gtk_target_list_new (drag_types_dest_file,
2114                                                   G_N_ELEMENTS (drag_types_dest_file));
2115
2116         /* Set up smiley menu */
2117         smiley_manager = empathy_smiley_manager_dup_singleton ();
2118         submenu = empathy_smiley_menu_new (smiley_manager,
2119                                            chat_window_insert_smiley_activate_cb,
2120                                            window);
2121         menu = gtk_ui_manager_get_widget (priv->ui_manager,
2122                 "/chats_menubar/menu_conv/menu_conv_insert_smiley");
2123         gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu), submenu);
2124         g_object_unref (smiley_manager);
2125
2126         /* Set up signals we can't do with ui file since we may need to
2127          * block/unblock them at some later stage.
2128          */
2129
2130         g_signal_connect (priv->dialog,
2131                           "delete_event",
2132                           G_CALLBACK (chat_window_delete_event_cb),
2133                           window);
2134         g_signal_connect (priv->dialog,
2135                           "focus_in_event",
2136                           G_CALLBACK (chat_window_focus_in_event_cb),
2137                           window);
2138         g_signal_connect_after (priv->notebook,
2139                                 "switch_page",
2140                                 G_CALLBACK (chat_window_page_switched_cb),
2141                                 window);
2142         g_signal_connect (priv->notebook,
2143                           "page_added",
2144                           G_CALLBACK (chat_window_page_added_cb),
2145                           window);
2146         g_signal_connect (priv->notebook,
2147                           "page_removed",
2148                           G_CALLBACK (chat_window_page_removed_cb),
2149                           window);
2150
2151         /* Set up drag and drop */
2152         gtk_drag_dest_set (GTK_WIDGET (priv->notebook),
2153                            GTK_DEST_DEFAULT_HIGHLIGHT,
2154                            drag_types_dest,
2155                            G_N_ELEMENTS (drag_types_dest),
2156                            GDK_ACTION_MOVE | GDK_ACTION_COPY);
2157
2158         /* connect_after to allow GtkNotebook's built-in tab switching */
2159         g_signal_connect_after (priv->notebook,
2160                                 "drag-motion",
2161                                 G_CALLBACK (chat_window_drag_motion),
2162                                 window);
2163         g_signal_connect (priv->notebook,
2164                           "drag-data-received",
2165                           G_CALLBACK (chat_window_drag_data_received),
2166                           window);
2167         g_signal_connect (priv->notebook,
2168                           "drag-drop",
2169                           G_CALLBACK (chat_window_drag_drop),
2170                           window);
2171
2172         chat_windows = g_list_prepend (chat_windows, window);
2173
2174         /* Set up private details */
2175         priv->chats = NULL;
2176         priv->current_chat = NULL;
2177         priv->notification = NULL;
2178
2179         priv->notify_mgr = empathy_notify_manager_dup_singleton ();
2180
2181         priv->chat_manager = empathy_chat_manager_dup_singleton ();
2182         priv->chat_manager_chats_changed_id =
2183                 g_signal_connect (priv->chat_manager, "closed-chats-changed",
2184                                   G_CALLBACK (chat_window_chat_manager_chats_changed_cb),
2185                                   window);
2186
2187         chat_window_chat_manager_chats_changed_cb (priv->chat_manager,
2188                                                    empathy_chat_manager_get_num_closed_chats (priv->chat_manager),
2189                                                    window);
2190 }
2191
2192 /* Returns the window to open a new tab in if there is a suitable window,
2193  * otherwise, returns NULL indicating that a new window should be added.
2194  */
2195 static EmpathyChatWindow *
2196 empathy_chat_window_get_default (gboolean room)
2197 {
2198         GSettings *gsettings = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
2199         GList    *l;
2200         gboolean  separate_windows = TRUE;
2201
2202         separate_windows = g_settings_get_boolean (gsettings,
2203                         EMPATHY_PREFS_UI_SEPARATE_CHAT_WINDOWS);
2204
2205         g_object_unref (gsettings);
2206
2207         if (separate_windows) {
2208                 /* Always create a new window */
2209                 return NULL;
2210         }
2211
2212         for (l = chat_windows; l; l = l->next) {
2213                 EmpathyChatWindow *chat_window;
2214                 guint nb_rooms, nb_private;
2215
2216                 chat_window = l->data;
2217
2218                 empathy_chat_window_get_nb_chats (chat_window, &nb_rooms, &nb_private);
2219
2220                 /* Skip the window if there aren't any rooms in it */
2221                 if (room && nb_rooms == 0)
2222                         continue;
2223
2224                 /* Skip the window if there aren't any 1-1 chats in it */
2225                 if (!room && nb_private == 0)
2226                         continue;
2227
2228                 return chat_window;
2229         }
2230
2231         return NULL;
2232 }
2233
2234 static void
2235 empathy_chat_window_add_chat (EmpathyChatWindow *window,
2236                               EmpathyChat       *chat)
2237 {
2238         EmpathyChatWindowPriv *priv;
2239         GtkWidget             *label;
2240         GtkWidget             *popup_label;
2241         GtkWidget             *child;
2242         GValue                value = { 0, };
2243
2244         g_return_if_fail (window != NULL);
2245         g_return_if_fail (EMPATHY_IS_CHAT (chat));
2246
2247         priv = GET_PRIV (window);
2248
2249         /* Reference the chat object */
2250         g_object_ref (chat);
2251
2252         /* If this window has just been created, position it */
2253         if (priv->chats == NULL) {
2254                 const gchar *name = "chat-window";
2255                 gboolean     separate_windows;
2256
2257                 separate_windows = g_settings_get_boolean (priv->gsettings_ui,
2258                                 EMPATHY_PREFS_UI_SEPARATE_CHAT_WINDOWS);
2259
2260                 if (empathy_chat_is_room (chat))
2261                         name = "room-window";
2262
2263                 if (separate_windows) {
2264                         gint x, y;
2265
2266                         /* Save current position of the window */
2267                         gtk_window_get_position (GTK_WINDOW (priv->dialog), &x, &y);
2268
2269                         /* First bind to the 'generic' name. So new window for which we didn't
2270                         * save a geometry yet will have the geometry of the last saved
2271                         * window (bgo #601191). */
2272                         empathy_geometry_bind (GTK_WINDOW (priv->dialog), name);
2273
2274                         /* Restore previous position of the window so the newly created window
2275                         * won't be in the same position as the latest saved window and so
2276                         * completely hide it. */
2277                         gtk_window_move (GTK_WINDOW (priv->dialog), x, y);
2278
2279                         /* Then bind it to the name of the contact/room so we'll save the
2280                         * geometry specific to this window */
2281                         name = empathy_chat_get_id (chat);
2282                 }
2283
2284                 empathy_geometry_bind (GTK_WINDOW (priv->dialog), name);
2285         }
2286
2287         child = GTK_WIDGET (chat);
2288         label = chat_window_create_label (window, chat, TRUE);
2289         popup_label = chat_window_create_label (window, chat, FALSE);
2290         gtk_widget_show (child);
2291
2292         g_signal_connect (chat, "notify::name",
2293                           G_CALLBACK (chat_window_chat_notify_cb),
2294                           NULL);
2295         g_signal_connect (chat, "notify::subject",
2296                           G_CALLBACK (chat_window_chat_notify_cb),
2297                           NULL);
2298         g_signal_connect (chat, "notify::remote-contact",
2299                           G_CALLBACK (chat_window_chat_notify_cb),
2300                           NULL);
2301         g_signal_connect (chat, "notify::sms-channel",
2302                           G_CALLBACK (chat_window_chat_notify_cb),
2303                           NULL);
2304         g_signal_connect (chat, "notify::n-messages-sending",
2305                           G_CALLBACK (chat_window_chat_notify_cb),
2306                           NULL);
2307         g_signal_connect (chat, "notify::nb-unread-messages",
2308                           G_CALLBACK (chat_window_chat_notify_cb),
2309                           NULL);
2310         chat_window_chat_notify_cb (chat);
2311
2312         gtk_notebook_append_page_menu (GTK_NOTEBOOK (priv->notebook), child, label, popup_label);
2313         gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (priv->notebook), child, TRUE);
2314         gtk_notebook_set_tab_detachable (GTK_NOTEBOOK (priv->notebook), child, TRUE);
2315         g_value_init (&value, G_TYPE_BOOLEAN);
2316         g_value_set_boolean (&value, TRUE);
2317         gtk_container_child_set_property (GTK_CONTAINER (priv->notebook),
2318                                           child, "tab-expand" , &value);
2319         gtk_container_child_set_property (GTK_CONTAINER (priv->notebook),
2320                                           child,  "tab-fill" , &value);
2321         g_value_unset (&value);
2322
2323         DEBUG ("Chat added (%d references)", G_OBJECT (chat)->ref_count);
2324 }
2325
2326 static void
2327 empathy_chat_window_remove_chat (EmpathyChatWindow *window,
2328                                  EmpathyChat       *chat)
2329 {
2330         EmpathyChatWindowPriv *priv;
2331         gint                   position;
2332         EmpathyContact        *remote_contact;
2333         EmpathyChatManager    *chat_manager;
2334
2335         g_return_if_fail (window != NULL);
2336         g_return_if_fail (EMPATHY_IS_CHAT (chat));
2337
2338         priv = GET_PRIV (window);
2339
2340         g_signal_handlers_disconnect_by_func (chat,
2341                                               chat_window_chat_notify_cb,
2342                                               NULL);
2343         remote_contact = g_object_get_data (G_OBJECT (chat),
2344                                             "chat-window-remote-contact");
2345         if (remote_contact) {
2346                 g_signal_handlers_disconnect_by_func (remote_contact,
2347                                                       chat_window_update_chat_tab,
2348                                                       chat);
2349         }
2350
2351         chat_manager = empathy_chat_manager_dup_singleton ();
2352         empathy_chat_manager_closed_chat (chat_manager, chat);
2353         g_object_unref (chat_manager);
2354
2355         position = gtk_notebook_page_num (GTK_NOTEBOOK (priv->notebook),
2356                                           GTK_WIDGET (chat));
2357         gtk_notebook_remove_page (GTK_NOTEBOOK (priv->notebook), position);
2358
2359         DEBUG ("Chat removed (%d references)", G_OBJECT (chat)->ref_count - 1);
2360
2361         g_object_unref (chat);
2362 }
2363
2364 static void
2365 empathy_chat_window_move_chat (EmpathyChatWindow *old_window,
2366                                EmpathyChatWindow *new_window,
2367                                EmpathyChat       *chat)
2368 {
2369         GtkWidget *widget;
2370
2371         g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (old_window));
2372         g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (new_window));
2373         g_return_if_fail (EMPATHY_IS_CHAT (chat));
2374
2375         widget = GTK_WIDGET (chat);
2376
2377         DEBUG ("Chat moving with widget:%p (%d references)", widget,
2378                 G_OBJECT (widget)->ref_count);
2379
2380         /* We reference here to make sure we don't loose the widget
2381          * and the EmpathyChat object during the move.
2382          */
2383         g_object_ref (chat);
2384         g_object_ref (widget);
2385
2386         empathy_chat_window_remove_chat (old_window, chat);
2387         empathy_chat_window_add_chat (new_window, chat);
2388
2389         g_object_unref (widget);
2390         g_object_unref (chat);
2391 }
2392
2393 static void
2394 empathy_chat_window_switch_to_chat (EmpathyChatWindow *window,
2395                                     EmpathyChat       *chat)
2396 {
2397         EmpathyChatWindowPriv *priv;
2398         gint                  page_num;
2399
2400         g_return_if_fail (window != NULL);
2401         g_return_if_fail (EMPATHY_IS_CHAT (chat));
2402
2403         priv = GET_PRIV (window);
2404
2405         page_num = gtk_notebook_page_num (GTK_NOTEBOOK (priv->notebook),
2406                                           GTK_WIDGET (chat));
2407         gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook),
2408                                        page_num);
2409 }
2410
2411 EmpathyChat *
2412 empathy_chat_window_find_chat (TpAccount   *account,
2413                                const gchar *id,
2414                                gboolean     sms_channel)
2415 {
2416         GList *l;
2417
2418         g_return_val_if_fail (!EMP_STR_EMPTY (id), NULL);
2419
2420         for (l = chat_windows; l; l = l->next) {
2421                 EmpathyChatWindowPriv *priv;
2422                 EmpathyChatWindow     *window;
2423                 GList                *ll;
2424
2425                 window = l->data;
2426                 priv = GET_PRIV (window);
2427
2428                 for (ll = priv->chats; ll; ll = ll->next) {
2429                         EmpathyChat *chat;
2430
2431                         chat = ll->data;
2432
2433                         if (account == empathy_chat_get_account (chat) &&
2434                             !tp_strdiff (id, empathy_chat_get_id (chat)) &&
2435                             sms_channel == empathy_chat_is_sms_channel (chat)) {
2436                                 return chat;
2437                         }
2438                 }
2439         }
2440
2441         return NULL;
2442 }
2443
2444 void
2445 empathy_chat_window_present_chat (EmpathyChat *chat,
2446                                   gint64 timestamp)
2447 {
2448         EmpathyChatWindow     *window;
2449         EmpathyChatWindowPriv *priv;
2450         guint32 x_timestamp;
2451
2452         g_return_if_fail (EMPATHY_IS_CHAT (chat));
2453
2454         window = chat_window_find_chat (chat);
2455
2456         /* If the chat has no window, create one */
2457         if (window == NULL) {
2458                 window = empathy_chat_window_get_default (empathy_chat_is_room (chat));
2459                 if (!window) {
2460                         window = empathy_chat_window_new ();
2461
2462                         /* we want to display the newly created window even if we don't present
2463                         * it */
2464                         priv = GET_PRIV (window);
2465                         gtk_widget_show (priv->dialog);
2466                 }
2467
2468                 empathy_chat_window_add_chat (window, chat);
2469         }
2470
2471         /* Don't force the window to show itself when it wasn't
2472          * an action by the user
2473          */
2474         if (!tp_user_action_time_should_present (timestamp, &x_timestamp))
2475                 return;
2476
2477         priv = GET_PRIV (window);
2478
2479         if (x_timestamp != GDK_CURRENT_TIME) {
2480                 /* Don't present or switch tab if the action was earlier than the
2481                  * last actions X time, accounting for overflow and the first ever
2482                 * presentation */
2483
2484                 if (priv->x_user_action_time != 0
2485                         && X_EARLIER_OR_EQL (x_timestamp, priv->x_user_action_time))
2486                         return;
2487
2488                 priv->x_user_action_time = x_timestamp;
2489         }
2490
2491         empathy_chat_window_switch_to_chat (window, chat);
2492
2493         /* Don't use empathy_window_present_with_time () which would move the window
2494          * to our current desktop but move to the window's desktop instead. This is
2495          * more coherent with Shell's 'app is ready' notication which moves the view
2496          * to the app desktop rather than moving the app itself. */
2497         empathy_move_to_window_desktop (GTK_WINDOW (priv->dialog), x_timestamp);
2498
2499         gtk_widget_grab_focus (chat->input_text_view);
2500 }
2501
2502 static void
2503 empathy_chat_window_get_nb_chats (EmpathyChatWindow *self,
2504                                guint *nb_rooms,
2505                                guint *nb_private)
2506 {
2507         EmpathyChatWindowPriv *priv = GET_PRIV (self);
2508         GList *l;
2509         guint _nb_rooms = 0, _nb_private = 0;
2510
2511         for (l = priv->chats; l != NULL; l = g_list_next (l)) {
2512                 if (empathy_chat_is_room (EMPATHY_CHAT (l->data)))
2513                         _nb_rooms++;
2514                 else
2515                         _nb_private++;
2516         }
2517
2518         if (nb_rooms != NULL)
2519                 *nb_rooms = _nb_rooms;
2520         if (nb_private != NULL)
2521                 *nb_private = _nb_private;
2522 }