]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-contact-list-view.c
Merge branch 'butterfly-conference-misc'
[empathy.git] / libempathy-gtk / empathy-contact-list-view.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2005-2007 Imendio AB
4  * Copyright (C) 2007-2008 Collabora Ltd.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public
17  * License along with this program; if not, write to the
18  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19  * Boston, MA  02110-1301  USA
20  *
21  * Authors: Mikael Hallendal <micke@imendio.com>
22  *          Martyn Russell <martyn@imendio.com>
23  *          Xavier Claessens <xclaesse@gmail.com>
24  */
25
26 #include "config.h"
27
28 #include <string.h>
29
30 #include <glib/gi18n-lib.h>
31 #include <gdk/gdkkeysyms.h>
32 #include <gtk/gtk.h>
33
34 #include <telepathy-glib/account-manager.h>
35 #include <telepathy-glib/util.h>
36
37 #include <libempathy/empathy-call-factory.h>
38 #include <libempathy/empathy-tp-contact-factory.h>
39 #include <libempathy/empathy-contact-list.h>
40 #include <libempathy/empathy-contact-groups.h>
41 #include <libempathy/empathy-dispatcher.h>
42 #include <libempathy/empathy-utils.h>
43
44 #include "empathy-contact-list-view.h"
45 #include "empathy-contact-list-store.h"
46 #include "empathy-images.h"
47 #include "empathy-cell-renderer-expander.h"
48 #include "empathy-cell-renderer-text.h"
49 #include "empathy-cell-renderer-activatable.h"
50 #include "empathy-ui-utils.h"
51 #include "empathy-gtk-enum-types.h"
52 #include "empathy-gtk-marshal.h"
53
54 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
55 #include <libempathy/empathy-debug.h>
56
57 /* Active users are those which have recently changed state
58  * (e.g. online, offline or from normal to a busy state).
59  */
60
61 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyContactListView)
62 typedef struct {
63         EmpathyContactListStore        *store;
64         GtkTreeRowReference            *drag_row;
65         EmpathyContactListFeatureFlags  list_features;
66         EmpathyContactFeatureFlags      contact_features;
67         GtkWidget                      *tooltip_widget;
68         GtkTargetList                  *file_targets;
69 } EmpathyContactListViewPriv;
70
71 typedef struct {
72         EmpathyContactListView *view;
73         GtkTreePath           *path;
74         guint                  timeout_id;
75 } DragMotionData;
76
77 typedef struct {
78         EmpathyContactListView *view;
79         EmpathyContact         *contact;
80         gboolean               remove;
81 } ShowActiveData;
82
83 enum {
84         PROP_0,
85         PROP_STORE,
86         PROP_LIST_FEATURES,
87         PROP_CONTACT_FEATURES,
88 };
89
90 enum DndDragType {
91         DND_DRAG_TYPE_CONTACT_ID,
92         DND_DRAG_TYPE_URI_LIST,
93         DND_DRAG_TYPE_STRING,
94 };
95
96 static const GtkTargetEntry drag_types_dest[] = {
97         { "text/uri-list",   0, DND_DRAG_TYPE_URI_LIST },
98         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
99         { "text/plain",      0, DND_DRAG_TYPE_STRING },
100         { "STRING",          0, DND_DRAG_TYPE_STRING },
101 };
102
103 static const GtkTargetEntry drag_types_dest_file[] = {
104         { "text/uri-list",   0, DND_DRAG_TYPE_URI_LIST },
105 };
106
107 static const GtkTargetEntry drag_types_source[] = {
108         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
109 };
110
111 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
112 static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
113
114 enum {
115         DRAG_CONTACT_RECEIVED,
116         LAST_SIGNAL
117 };
118
119 static guint signals[LAST_SIGNAL];
120
121 G_DEFINE_TYPE (EmpathyContactListView, empathy_contact_list_view, GTK_TYPE_TREE_VIEW);
122
123 static void
124 contact_list_view_tooltip_destroy_cb (GtkWidget              *widget,
125                                       EmpathyContactListView *view)
126 {
127         EmpathyContactListViewPriv *priv = GET_PRIV (view);
128
129         if (priv->tooltip_widget) {
130                 DEBUG ("Tooltip destroyed");
131                 g_object_unref (priv->tooltip_widget);
132                 priv->tooltip_widget = NULL;
133         }
134 }
135
136 static gboolean
137 contact_list_view_query_tooltip_cb (EmpathyContactListView *view,
138                                     gint                    x,
139                                     gint                    y,
140                                     gboolean                keyboard_mode,
141                                     GtkTooltip             *tooltip,
142                                     gpointer                user_data)
143 {
144         EmpathyContactListViewPriv *priv = GET_PRIV (view);
145         EmpathyContact             *contact;
146         GtkTreeModel               *model;
147         GtkTreeIter                 iter;
148         GtkTreePath                *path;
149         static gint                 running = 0;
150         gboolean                    ret = FALSE;
151
152         /* Avoid an infinite loop. See GNOME bug #574377 */
153         if (running > 0) {
154                 return FALSE;
155         }
156         running++;
157
158         /* Don't show the tooltip if there's already a popup menu */
159         if (gtk_menu_get_for_attach_widget (GTK_WIDGET (view)) != NULL) {
160                 goto OUT;
161         }
162
163         if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (view), &x, &y,
164                                                 keyboard_mode,
165                                                 &model, &path, &iter)) {
166                 goto OUT;
167         }
168
169         gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (view), tooltip, path);
170         gtk_tree_path_free (path);
171
172         gtk_tree_model_get (model, &iter,
173                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
174                             -1);
175         if (!contact) {
176                 goto OUT;
177         }
178
179         if (!priv->tooltip_widget) {
180                 priv->tooltip_widget = empathy_contact_widget_new (contact,
181                         EMPATHY_CONTACT_WIDGET_FOR_TOOLTIP |
182                         EMPATHY_CONTACT_WIDGET_SHOW_LOCATION);
183                 gtk_container_set_border_width (
184                         GTK_CONTAINER (priv->tooltip_widget), 8);
185                 g_object_ref (priv->tooltip_widget);
186                 g_signal_connect (priv->tooltip_widget, "destroy",
187                                   G_CALLBACK (contact_list_view_tooltip_destroy_cb),
188                                   view);
189                 gtk_widget_show (priv->tooltip_widget);
190         } else {
191                 empathy_contact_widget_set_contact (priv->tooltip_widget,
192                                                     contact);
193         }
194
195         gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
196         ret = TRUE;
197
198         g_object_unref (contact);
199 OUT:
200         running--;
201
202         return ret;
203 }
204
205 typedef struct {
206         gchar *new_group;
207         gchar *old_group;
208         GdkDragAction action;
209 } DndGetContactData;
210
211 static void
212 contact_list_view_dnd_get_contact_free (DndGetContactData *data)
213 {
214         g_free (data->new_group);
215         g_free (data->old_group);
216         g_slice_free (DndGetContactData, data);
217 }
218
219 static void
220 contact_list_view_drag_got_contact (EmpathyTpContactFactory *factory,
221                                     EmpathyContact          *contact,
222                                     const GError            *error,
223                                     gpointer                 user_data,
224                                     GObject                 *view)
225 {
226         EmpathyContactListViewPriv *priv = GET_PRIV (view);
227         DndGetContactData          *data = user_data;
228         EmpathyContactList         *list;
229
230         if (error != NULL) {
231                 DEBUG ("Error: %s", error->message);
232                 return;
233         }
234
235         DEBUG ("contact %s (%d) dragged from '%s' to '%s'",
236                 empathy_contact_get_id (contact),
237                 empathy_contact_get_handle (contact),
238                 data->old_group, data->new_group);
239
240         list = empathy_contact_list_store_get_list_iface (priv->store);
241         if (data->new_group) {
242                 empathy_contact_list_add_to_group (list, contact, data->new_group);
243         }
244         if (data->old_group && data->action == GDK_ACTION_MOVE) {
245                 empathy_contact_list_remove_from_group (list, contact, data->old_group);
246         }
247 }
248
249 static gboolean
250 contact_list_view_contact_drag_received (GtkWidget         *view,
251                                          GdkDragContext    *context,
252                                          GtkTreeModel      *model,
253                                          GtkTreePath       *path,
254                                          GtkSelectionData  *selection)
255 {
256         EmpathyContactListViewPriv *priv;
257         TpAccountManager           *account_manager;
258         EmpathyTpContactFactory    *factory = NULL;
259         TpAccount                  *account;
260         DndGetContactData          *data;
261         GtkTreePath                *source_path;
262         const gchar   *sel_data;
263         gchar        **strv = NULL;
264         const gchar   *account_id = NULL;
265         const gchar   *contact_id = NULL;
266         gchar         *new_group = NULL;
267         gchar         *old_group = NULL;
268         gboolean       success = TRUE;
269
270         priv = GET_PRIV (view);
271
272         sel_data = (const gchar *) gtk_selection_data_get_data (selection);
273         new_group = empathy_contact_list_store_get_parent_group (model,
274                                                                  path, NULL);
275
276         /* Get source group information. */
277         if (priv->drag_row) {
278                 source_path = gtk_tree_row_reference_get_path (priv->drag_row);
279                 if (source_path) {
280                         old_group = empathy_contact_list_store_get_parent_group (
281                                                                                  model, source_path, NULL);
282                         gtk_tree_path_free (source_path);
283                 }
284         }
285
286         if (!tp_strdiff (old_group, new_group)) {
287                 g_free (new_group);
288                 g_free (old_group);
289                 return FALSE;
290         }
291
292         account_manager = tp_account_manager_dup ();
293         strv = g_strsplit (sel_data, ":", 2);
294         if (g_strv_length (strv) == 2) {
295                 account_id = strv[0];
296                 contact_id = strv[1];
297                 account = tp_account_manager_ensure_account (account_manager, account_id);
298         }
299         if (account) {
300                 TpConnection *connection;
301
302                 connection = tp_account_get_connection (account);
303                 if (connection) {
304                         factory = empathy_tp_contact_factory_dup_singleton (connection);
305                 }
306         }
307         g_object_unref (account_manager);
308
309         if (!factory) {
310                 DEBUG ("Failed to get factory for account '%s'", account_id);
311                 success = FALSE;
312                 g_free (new_group);
313                 g_free (old_group);
314                 return FALSE;
315         }
316
317         data = g_slice_new0 (DndGetContactData);
318         data->new_group = new_group;
319         data->old_group = old_group;
320         data->action = context->action;
321
322         /* FIXME: We should probably wait for the cb before calling
323          * gtk_drag_finish */
324         empathy_tp_contact_factory_get_from_id (factory, contact_id,
325                                                 contact_list_view_drag_got_contact,
326                                                 data, (GDestroyNotify) contact_list_view_dnd_get_contact_free,
327                                                 G_OBJECT (view));
328         g_strfreev (strv);
329         g_object_unref (factory);
330
331         return TRUE;
332 }
333
334 static gboolean
335 contact_list_view_file_drag_received (GtkWidget         *view,
336                                       GdkDragContext    *context,
337                                       GtkTreeModel      *model,
338                                       GtkTreePath       *path,
339                                       GtkSelectionData  *selection)
340 {
341         GtkTreeIter     iter;
342         const gchar    *sel_data;
343         EmpathyContact *contact;
344
345         sel_data = (const gchar *) gtk_selection_data_get_data (selection);
346
347         gtk_tree_model_get_iter (model, &iter, path);
348         gtk_tree_model_get (model, &iter,
349                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
350                             -1);
351         if (!contact) {
352                 return FALSE;
353         }
354
355         empathy_send_file_from_uri_list (contact, sel_data);
356
357         g_object_unref (contact);
358
359         return TRUE;
360 }
361
362 static void
363 contact_list_view_drag_data_received (GtkWidget         *view,
364                                       GdkDragContext    *context,
365                                       gint               x,
366                                       gint               y,
367                                       GtkSelectionData  *selection,
368                                       guint              info,
369                                       guint              time_)
370 {
371         GtkTreeModel               *model;
372         gboolean                    is_row;
373         GtkTreeViewDropPosition     position;
374         GtkTreePath                *path;
375         gboolean                    success = TRUE;
376
377         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
378
379         /* Get destination group information. */
380         is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (view),
381                                                     x,
382                                                     y,
383                                                     &path,
384                                                     &position);
385         if (!is_row) {
386                 success = FALSE;
387         }
388         else if (info == DND_DRAG_TYPE_CONTACT_ID || info == DND_DRAG_TYPE_STRING) {
389                 success = contact_list_view_contact_drag_received (view,
390                                                                    context,
391                                                                    model,
392                                                                    path,
393                                                                    selection);
394         }
395         else if (info == DND_DRAG_TYPE_URI_LIST) {
396                 success = contact_list_view_file_drag_received (view,
397                                                                 context,
398                                                                 model,
399                                                                 path,
400                                                                 selection);
401         }
402
403         gtk_tree_path_free (path);
404         gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
405 }
406
407 static gboolean
408 contact_list_view_drag_motion_cb (DragMotionData *data)
409 {
410         gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view),
411                                   data->path,
412                                   FALSE);
413
414         data->timeout_id = 0;
415
416         return FALSE;
417 }
418
419 static gboolean
420 contact_list_view_drag_motion (GtkWidget      *widget,
421                                GdkDragContext *context,
422                                gint            x,
423                                gint            y,
424                                guint           time_)
425 {
426         EmpathyContactListViewPriv *priv;
427         GtkTreeModel               *model;
428         GdkAtom                target;
429         GtkTreeIter            iter;
430         static DragMotionData *dm = NULL;
431         GtkTreePath           *path;
432         gboolean               is_row;
433         gboolean               is_different = FALSE;
434         gboolean               cleanup = TRUE;
435         gboolean               retval = TRUE;
436
437         priv = GET_PRIV (EMPATHY_CONTACT_LIST_VIEW (widget));
438         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
439
440         is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
441                                                 x,
442                                                 y,
443                                                 &path,
444                                                 NULL,
445                                                 NULL,
446                                                 NULL);
447
448         cleanup &= (!dm);
449
450         if (is_row) {
451                 cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
452                 is_different = (!dm || (dm && gtk_tree_path_compare (dm->path, path) != 0));
453         } else {
454                 cleanup &= FALSE;
455         }
456
457         if (path == NULL) {
458                 /* Coordinates don't point to an actual row, so make sure the pointer
459                    and highlighting don't indicate that a drag is possible.
460                  */
461                 gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
462                 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
463                 return FALSE;
464         }
465         target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
466         gtk_tree_model_get_iter (model, &iter, path);
467
468         if (target == GDK_NONE) {
469                 /* If target == GDK_NONE, then we don't have a target that can be
470                    dropped on a contact.  This means a contact drag.  If we're
471                    pointing to a group, highlight it.  Otherwise, if the contact
472                    we're pointing to is in a group, highlight that.  Otherwise,
473                    set the drag position to before the first row for a drag into
474                    the "non-group" at the top.
475                  */
476                 GtkTreeIter  group_iter;
477                 gboolean     is_group;
478                 GtkTreePath *group_path;
479                 gtk_tree_model_get (model, &iter,
480                                     EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
481                                     -1);
482                 if (is_group) {
483                         group_iter = iter;
484                 }
485                 else {
486                         if (gtk_tree_model_iter_parent (model, &group_iter, &iter))
487                                 gtk_tree_model_get (model, &group_iter,
488                                                     EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
489                                                     -1);
490                 }
491                 if (is_group) {
492                         gdk_drag_status (context, GDK_ACTION_MOVE, time_);
493                         group_path = gtk_tree_model_get_path (model, &group_iter);
494                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
495                                                          group_path,
496                                                          GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
497                         gtk_tree_path_free (group_path);
498                 }
499                 else {
500                         group_path = gtk_tree_path_new_first ();
501                         gdk_drag_status (context, GDK_ACTION_MOVE, time_);
502                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
503                                                          group_path,
504                                                          GTK_TREE_VIEW_DROP_BEFORE);
505                 }
506         }
507         else {
508                 /* This is a file drag, and it can only be dropped on contacts,
509                    not groups.
510                  */
511                 EmpathyContact *contact;
512                 gtk_tree_model_get (model, &iter,
513                                     EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
514                                     -1);
515                 if (contact != NULL &&
516                     empathy_contact_is_online (contact) &&
517                     (empathy_contact_get_capabilities (contact) & EMPATHY_CAPABILITIES_FT)) {
518                         gdk_drag_status (context, GDK_ACTION_COPY, time_);
519                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
520                                                          path,
521                                                          GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
522                         g_object_unref (contact);
523                 }
524                 else {
525                         gdk_drag_status (context, 0, time_);
526                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
527                         retval = FALSE;
528                 }
529         }
530
531         if (!is_different && !cleanup) {
532                 return retval;
533         }
534
535         if (dm) {
536                 gtk_tree_path_free (dm->path);
537                 if (dm->timeout_id) {
538                         g_source_remove (dm->timeout_id);
539                 }
540
541                 g_free (dm);
542
543                 dm = NULL;
544         }
545
546         if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) {
547                 dm = g_new0 (DragMotionData, 1);
548
549                 dm->view = EMPATHY_CONTACT_LIST_VIEW (widget);
550                 dm->path = gtk_tree_path_copy (path);
551
552                 dm->timeout_id = g_timeout_add_seconds (1,
553                         (GSourceFunc) contact_list_view_drag_motion_cb,
554                         dm);
555         }
556
557         return retval;
558 }
559
560 static void
561 contact_list_view_drag_begin (GtkWidget      *widget,
562                               GdkDragContext *context)
563 {
564         EmpathyContactListViewPriv *priv;
565         GtkTreeSelection          *selection;
566         GtkTreeModel              *model;
567         GtkTreePath               *path;
568         GtkTreeIter                iter;
569
570         priv = GET_PRIV (widget);
571
572         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_begin (widget,
573                                                                               context);
574
575         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
576         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
577                 return;
578         }
579
580         path = gtk_tree_model_get_path (model, &iter);
581         priv->drag_row = gtk_tree_row_reference_new (model, path);
582         gtk_tree_path_free (path);
583 }
584
585 static void
586 contact_list_view_drag_data_get (GtkWidget        *widget,
587                                  GdkDragContext   *context,
588                                  GtkSelectionData *selection,
589                                  guint             info,
590                                  guint             time_)
591 {
592         EmpathyContactListViewPriv *priv;
593         GtkTreePath                *src_path;
594         GtkTreeIter                 iter;
595         GtkTreeModel               *model;
596         EmpathyContact             *contact;
597         TpAccount                  *account;
598         const gchar                *contact_id;
599         const gchar                *account_id;
600         gchar                      *str;
601
602         priv = GET_PRIV (widget);
603
604         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
605         if (!priv->drag_row) {
606                 return;
607         }
608
609         src_path = gtk_tree_row_reference_get_path (priv->drag_row);
610         if (!src_path) {
611                 return;
612         }
613
614         if (!gtk_tree_model_get_iter (model, &iter, src_path)) {
615                 gtk_tree_path_free (src_path);
616                 return;
617         }
618
619         gtk_tree_path_free (src_path);
620
621         contact = empathy_contact_list_view_dup_selected (EMPATHY_CONTACT_LIST_VIEW (widget));
622         if (!contact) {
623                 return;
624         }
625
626         account = empathy_contact_get_account (contact);
627         account_id = tp_proxy_get_object_path (account);
628         contact_id = empathy_contact_get_id (contact);
629         g_object_unref (contact);
630         str = g_strconcat (account_id, ":", contact_id, NULL);
631
632         switch (info) {
633         case DND_DRAG_TYPE_CONTACT_ID:
634                 gtk_selection_data_set (selection, drag_atoms_source[info], 8,
635                                         (guchar *) str, strlen (str) + 1);
636                 break;
637         }
638
639         g_free (str);
640 }
641
642 static void
643 contact_list_view_drag_end (GtkWidget      *widget,
644                             GdkDragContext *context)
645 {
646         EmpathyContactListViewPriv *priv;
647
648         priv = GET_PRIV (widget);
649
650         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_end (widget,
651                                                                             context);
652
653         if (priv->drag_row) {
654                 gtk_tree_row_reference_free (priv->drag_row);
655                 priv->drag_row = NULL;
656         }
657 }
658
659 static gboolean
660 contact_list_view_drag_drop (GtkWidget      *widget,
661                              GdkDragContext *drag_context,
662                              gint            x,
663                              gint            y,
664                              guint           time_)
665 {
666         return FALSE;
667 }
668
669 typedef struct {
670         EmpathyContactListView *view;
671         guint                   button;
672         guint32                 time;
673 } MenuPopupData;
674
675 static gboolean
676 contact_list_view_popup_menu_idle_cb (gpointer user_data)
677 {
678         MenuPopupData *data = user_data;
679         GtkWidget     *menu;
680
681         menu = empathy_contact_list_view_get_contact_menu (data->view);
682         if (!menu) {
683                 menu = empathy_contact_list_view_get_group_menu (data->view);
684         }
685
686         if (menu) {
687                 g_signal_connect (menu, "deactivate",
688                                   G_CALLBACK (gtk_menu_detach), NULL);
689                 gtk_menu_attach_to_widget (GTK_MENU (menu),
690                                            GTK_WIDGET (data->view), NULL);
691                 gtk_widget_show (menu);
692                 gtk_menu_popup (GTK_MENU (menu),
693                                 NULL, NULL, NULL, NULL,
694                                 data->button, data->time);
695                 g_object_ref_sink (menu);
696                 g_object_unref (menu);
697         }
698
699         g_slice_free (MenuPopupData, data);
700
701         return FALSE;
702 }
703
704 static gboolean
705 contact_list_view_button_press_event_cb (EmpathyContactListView *view,
706                                          GdkEventButton         *event,
707                                          gpointer                user_data)
708 {
709         if (event->button == 3) {
710                 MenuPopupData *data;
711
712                 data = g_slice_new (MenuPopupData);
713                 data->view = view;
714                 data->button = event->button;
715                 data->time = event->time;
716                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
717         }
718
719         return FALSE;
720 }
721
722 static gboolean
723 contact_list_view_key_press_event_cb (EmpathyContactListView *view,
724                                       GdkEventKey            *event,
725                                       gpointer                user_data)
726 {
727         if (event->keyval == GDK_Menu) {
728                 MenuPopupData *data;
729
730                 data = g_slice_new (MenuPopupData);
731                 data->view = view;
732                 data->button = 0;
733                 data->time = event->time;
734                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
735         }
736
737         return FALSE;
738 }
739
740 static void
741 contact_list_view_row_activated (GtkTreeView       *view,
742                                  GtkTreePath       *path,
743                                  GtkTreeViewColumn *column)
744 {
745         EmpathyContactListViewPriv *priv = GET_PRIV (view);
746         EmpathyContact             *contact;
747         GtkTreeModel               *model;
748         GtkTreeIter                 iter;
749
750         if (!(priv->contact_features & EMPATHY_CONTACT_FEATURE_CHAT)) {
751                 return;
752         }
753
754         model = GTK_TREE_MODEL (priv->store);
755         gtk_tree_model_get_iter (model, &iter, path);
756         gtk_tree_model_get (model, &iter,
757                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
758                             -1);
759
760         if (contact) {
761                 DEBUG ("Starting a chat");
762                 empathy_dispatcher_chat_with_contact (contact, NULL, NULL);
763                 g_object_unref (contact);
764         }
765 }
766
767 static void
768 contact_list_view_call_activated_cb (
769     EmpathyCellRendererActivatable *cell,
770     const gchar                    *path_string,
771     EmpathyContactListView         *view)
772 {
773         GtkWidget *menu;
774         GtkTreeModel *model;
775         GtkTreeIter iter;
776         EmpathyContact *contact;
777         GdkEventButton *event;
778         GtkMenuShell *shell;
779         GtkWidget *item;
780
781         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
782         if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
783                 return;
784
785         gtk_tree_model_get (model, &iter,
786                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
787                             -1);
788         if (contact == NULL)
789                 return;
790
791         event = (GdkEventButton *) gtk_get_current_event ();
792
793         menu = gtk_menu_new ();
794         shell = GTK_MENU_SHELL (menu);
795
796         /* audio */
797         item = empathy_contact_audio_call_menu_item_new (contact);
798         gtk_menu_shell_append (shell, item);
799         gtk_widget_show (item);
800
801         /* video */
802         item = empathy_contact_video_call_menu_item_new (contact);
803         gtk_menu_shell_append (shell, item);
804         gtk_widget_show (item);
805
806         g_signal_connect (menu, "deactivate",
807                           G_CALLBACK (gtk_menu_detach), NULL);
808         gtk_menu_attach_to_widget (GTK_MENU (menu),
809                                    GTK_WIDGET (view), NULL);
810         gtk_widget_show (menu);
811         gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
812                         event->button, event->time);
813         g_object_ref_sink (menu);
814         g_object_unref (menu);
815
816         g_object_unref (contact);
817 }
818
819 static void
820 contact_list_view_cell_set_background (EmpathyContactListView *view,
821                                        GtkCellRenderer       *cell,
822                                        gboolean               is_group,
823                                        gboolean               is_active)
824 {
825         GdkColor  color;
826         GtkStyle *style;
827
828         style = gtk_widget_get_style (GTK_WIDGET (view));
829
830         if (!is_group && is_active) {
831                 color = style->bg[GTK_STATE_SELECTED];
832
833                 /* Here we take the current theme colour and add it to
834                  * the colour for white and average the two. This
835                  * gives a colour which is inline with the theme but
836                  * slightly whiter.
837                  */
838                 color.red = (color.red + (style->white).red) / 2;
839                 color.green = (color.green + (style->white).green) / 2;
840                 color.blue = (color.blue + (style->white).blue) / 2;
841
842                 g_object_set (cell,
843                               "cell-background-gdk", &color,
844                               NULL);
845         } else {
846                 g_object_set (cell,
847                               "cell-background-gdk", NULL,
848                               NULL);
849         }
850 }
851
852 static void
853 contact_list_view_pixbuf_cell_data_func (GtkTreeViewColumn     *tree_column,
854                                          GtkCellRenderer       *cell,
855                                          GtkTreeModel          *model,
856                                          GtkTreeIter           *iter,
857                                          EmpathyContactListView *view)
858 {
859         GdkPixbuf *pixbuf;
860         gboolean   is_group;
861         gboolean   is_active;
862
863         gtk_tree_model_get (model, iter,
864                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
865                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
866                             EMPATHY_CONTACT_LIST_STORE_COL_ICON_STATUS, &pixbuf,
867                             -1);
868
869         g_object_set (cell,
870                       "visible", !is_group,
871                       "pixbuf", pixbuf,
872                       NULL);
873
874         if (pixbuf != NULL) {
875                 g_object_unref (pixbuf);
876         }
877
878         contact_list_view_cell_set_background (view, cell, is_group, is_active);
879 }
880
881 static void
882 contact_list_view_audio_call_cell_data_func (
883                                        GtkTreeViewColumn      *tree_column,
884                                        GtkCellRenderer        *cell,
885                                        GtkTreeModel           *model,
886                                        GtkTreeIter            *iter,
887                                        EmpathyContactListView *view)
888 {
889         gboolean is_group;
890         gboolean is_active;
891         gboolean can_audio, can_video;
892
893         gtk_tree_model_get (model, iter,
894                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
895                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
896                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_AUDIO_CALL, &can_audio,
897                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_VIDEO_CALL, &can_video,
898                             -1);
899
900         g_object_set (cell,
901                       "visible", !is_group && (can_audio || can_video),
902                       "icon-name", can_video? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
903                       NULL);
904
905         contact_list_view_cell_set_background (view, cell, is_group, is_active);
906 }
907
908 static void
909 contact_list_view_avatar_cell_data_func (GtkTreeViewColumn     *tree_column,
910                                          GtkCellRenderer       *cell,
911                                          GtkTreeModel          *model,
912                                          GtkTreeIter           *iter,
913                                          EmpathyContactListView *view)
914 {
915         GdkPixbuf *pixbuf;
916         gboolean   show_avatar;
917         gboolean   is_group;
918         gboolean   is_active;
919
920         gtk_tree_model_get (model, iter,
921                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR, &pixbuf,
922                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
923                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
924                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
925                             -1);
926
927         g_object_set (cell,
928                       "visible", !is_group && show_avatar,
929                       "pixbuf", pixbuf,
930                       NULL);
931
932         if (pixbuf) {
933                 g_object_unref (pixbuf);
934         }
935
936         contact_list_view_cell_set_background (view, cell, is_group, is_active);
937 }
938
939 static void
940 contact_list_view_text_cell_data_func (GtkTreeViewColumn     *tree_column,
941                                        GtkCellRenderer       *cell,
942                                        GtkTreeModel          *model,
943                                        GtkTreeIter           *iter,
944                                        EmpathyContactListView *view)
945 {
946         gboolean is_group;
947         gboolean is_active;
948         gboolean show_status;
949         gchar *name;
950
951         gtk_tree_model_get (model, iter,
952                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
953                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
954                             EMPATHY_CONTACT_LIST_STORE_COL_STATUS_VISIBLE, &show_status,
955                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
956                             -1);
957
958         g_object_set (cell,
959                       "show-status", show_status,
960                       "text", name,
961                       NULL);
962         g_free (name);
963
964         contact_list_view_cell_set_background (view, cell, is_group, is_active);
965 }
966
967 static void
968 contact_list_view_expander_cell_data_func (GtkTreeViewColumn     *column,
969                                            GtkCellRenderer       *cell,
970                                            GtkTreeModel          *model,
971                                            GtkTreeIter           *iter,
972                                            EmpathyContactListView *view)
973 {
974         gboolean is_group;
975         gboolean is_active;
976
977         gtk_tree_model_get (model, iter,
978                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
979                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
980                             -1);
981
982         if (gtk_tree_model_iter_has_child (model, iter)) {
983                 GtkTreePath *path;
984                 gboolean     row_expanded;
985
986                 path = gtk_tree_model_get_path (model, iter);
987                 row_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (gtk_tree_view_column_get_tree_view (column)), path);
988                 gtk_tree_path_free (path);
989
990                 g_object_set (cell,
991                               "visible", TRUE,
992                               "expander-style", row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
993                               NULL);
994         } else {
995                 g_object_set (cell, "visible", FALSE, NULL);
996         }
997
998         contact_list_view_cell_set_background (view, cell, is_group, is_active);
999 }
1000
1001 static void
1002 contact_list_view_row_expand_or_collapse_cb (EmpathyContactListView *view,
1003                                              GtkTreeIter           *iter,
1004                                              GtkTreePath           *path,
1005                                              gpointer               user_data)
1006 {
1007         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1008         GtkTreeModel               *model;
1009         gchar                      *name;
1010         gboolean                    expanded;
1011
1012         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE)) {
1013                 return;
1014         }
1015
1016         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1017
1018         gtk_tree_model_get (model, iter,
1019                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1020                             -1);
1021
1022         expanded = GPOINTER_TO_INT (user_data);
1023         empathy_contact_group_set_expanded (name, expanded);
1024
1025         g_free (name);
1026 }
1027
1028 static void
1029 contact_list_view_row_has_child_toggled_cb (GtkTreeModel          *model,
1030                                             GtkTreePath           *path,
1031                                             GtkTreeIter           *iter,
1032                                             EmpathyContactListView *view)
1033 {
1034         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1035         gboolean  is_group = FALSE;
1036         gchar    *name = NULL;
1037
1038         gtk_tree_model_get (model, iter,
1039                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1040                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1041                             -1);
1042
1043         if (!is_group || EMP_STR_EMPTY (name)) {
1044                 g_free (name);
1045                 return;
1046         }
1047
1048         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) ||
1049             empathy_contact_group_get_expanded (name)) {
1050                 g_signal_handlers_block_by_func (view,
1051                                                  contact_list_view_row_expand_or_collapse_cb,
1052                                                  GINT_TO_POINTER (TRUE));
1053                 gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
1054                 g_signal_handlers_unblock_by_func (view,
1055                                                    contact_list_view_row_expand_or_collapse_cb,
1056                                                    GINT_TO_POINTER (TRUE));
1057         } else {
1058                 g_signal_handlers_block_by_func (view,
1059                                                  contact_list_view_row_expand_or_collapse_cb,
1060                                                  GINT_TO_POINTER (FALSE));
1061                 gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1062                 g_signal_handlers_unblock_by_func (view,
1063                                                    contact_list_view_row_expand_or_collapse_cb,
1064                                                    GINT_TO_POINTER (FALSE));
1065         }
1066
1067         g_free (name);
1068 }
1069
1070 static void
1071 contact_list_view_setup (EmpathyContactListView *view)
1072 {
1073         EmpathyContactListViewPriv *priv;
1074         GtkCellRenderer           *cell;
1075         GtkTreeViewColumn         *col;
1076         guint                      i;
1077
1078         priv = GET_PRIV (view);
1079
1080         gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (view),
1081                                              empathy_contact_list_store_search_equal_func,
1082                                              NULL, NULL);
1083
1084         g_signal_connect (priv->store, "row-has-child-toggled",
1085                           G_CALLBACK (contact_list_view_row_has_child_toggled_cb),
1086                           view);
1087         gtk_tree_view_set_model (GTK_TREE_VIEW (view),
1088                                  GTK_TREE_MODEL (priv->store));
1089
1090         /* Setup view */
1091         /* Setting reorderable is a hack that gets us row previews as drag icons
1092            for free.  We override all the drag handlers.  It's tricky to get the
1093            position of the drag icon right in drag_begin.  GtkTreeView has special
1094            voodoo for it, so we let it do the voodoo that he do.
1095          */
1096         g_object_set (view,
1097                       "headers-visible", FALSE,
1098                       "reorderable", TRUE,
1099                       "show-expanders", FALSE,
1100                       NULL);
1101
1102         col = gtk_tree_view_column_new ();
1103
1104         /* State */
1105         cell = gtk_cell_renderer_pixbuf_new ();
1106         gtk_tree_view_column_pack_start (col, cell, FALSE);
1107         gtk_tree_view_column_set_cell_data_func (
1108                 col, cell,
1109                 (GtkTreeCellDataFunc) contact_list_view_pixbuf_cell_data_func,
1110                 view, NULL);
1111
1112         g_object_set (cell,
1113                       "xpad", 5,
1114                       "ypad", 1,
1115                       "visible", FALSE,
1116                       NULL);
1117
1118         /* Name */
1119         cell = empathy_cell_renderer_text_new ();
1120         gtk_tree_view_column_pack_start (col, cell, TRUE);
1121         gtk_tree_view_column_set_cell_data_func (
1122                 col, cell,
1123                 (GtkTreeCellDataFunc) contact_list_view_text_cell_data_func,
1124                 view, NULL);
1125
1126         gtk_tree_view_column_add_attribute (col, cell,
1127                                             "name", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
1128         gtk_tree_view_column_add_attribute (col, cell,
1129                                             "status", EMPATHY_CONTACT_LIST_STORE_COL_STATUS);
1130         gtk_tree_view_column_add_attribute (col, cell,
1131                                             "is_group", EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP);
1132
1133         /* Audio Call Icon */
1134         cell = empathy_cell_renderer_activatable_new ();
1135         gtk_tree_view_column_pack_start (col, cell, FALSE);
1136         gtk_tree_view_column_set_cell_data_func (
1137                 col, cell,
1138                 (GtkTreeCellDataFunc) contact_list_view_audio_call_cell_data_func,
1139                 view, NULL);
1140
1141         g_object_set (cell,
1142                       "visible", FALSE,
1143                       NULL);
1144
1145         g_signal_connect (cell, "path-activated",
1146                           G_CALLBACK (contact_list_view_call_activated_cb),
1147                           view);
1148
1149         /* Avatar */
1150         cell = gtk_cell_renderer_pixbuf_new ();
1151         gtk_tree_view_column_pack_start (col, cell, FALSE);
1152         gtk_tree_view_column_set_cell_data_func (
1153                 col, cell,
1154                 (GtkTreeCellDataFunc) contact_list_view_avatar_cell_data_func,
1155                 view, NULL);
1156
1157         g_object_set (cell,
1158                       "xpad", 0,
1159                       "ypad", 0,
1160                       "visible", FALSE,
1161                       "width", 32,
1162                       "height", 32,
1163                       NULL);
1164
1165         /* Expander */
1166         cell = empathy_cell_renderer_expander_new ();
1167         gtk_tree_view_column_pack_end (col, cell, FALSE);
1168         gtk_tree_view_column_set_cell_data_func (
1169                 col, cell,
1170                 (GtkTreeCellDataFunc) contact_list_view_expander_cell_data_func,
1171                 view, NULL);
1172
1173         /* Actually add the column now we have added all cell renderers */
1174         gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1175
1176         /* Drag & Drop. */
1177         for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i) {
1178                 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target,
1179                                                       FALSE);
1180         }
1181
1182         for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i) {
1183                 drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1184                                                         FALSE);
1185         }
1186 }
1187
1188 static void
1189 contact_list_view_set_list_features (EmpathyContactListView         *view,
1190                                      EmpathyContactListFeatureFlags  features)
1191 {
1192         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1193         gboolean                    has_tooltip;
1194
1195         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view));
1196
1197         priv->list_features = features;
1198
1199         /* Update DnD source/dest */
1200         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DRAG) {
1201                 gtk_drag_source_set (GTK_WIDGET (view),
1202                                      GDK_BUTTON1_MASK,
1203                                      drag_types_source,
1204                                      G_N_ELEMENTS (drag_types_source),
1205                                      GDK_ACTION_MOVE | GDK_ACTION_COPY);
1206         } else {
1207                 gtk_drag_source_unset (GTK_WIDGET (view));
1208
1209         }
1210
1211         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DROP) {
1212                 gtk_drag_dest_set (GTK_WIDGET (view),
1213                                    GTK_DEST_DEFAULT_ALL,
1214                                    drag_types_dest,
1215                                    G_N_ELEMENTS (drag_types_dest),
1216                                    GDK_ACTION_MOVE | GDK_ACTION_COPY);
1217         } else {
1218                 /* FIXME: URI could still be droped depending on FT feature */
1219                 gtk_drag_dest_unset (GTK_WIDGET (view));
1220         }
1221
1222         /* Update has-tooltip */
1223         has_tooltip = (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_TOOLTIP) != 0;
1224         gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1225 }
1226
1227 static void
1228 contact_list_view_finalize (GObject *object)
1229 {
1230         EmpathyContactListViewPriv *priv;
1231
1232         priv = GET_PRIV (object);
1233
1234         if (priv->store) {
1235                 g_object_unref (priv->store);
1236         }
1237         if (priv->tooltip_widget) {
1238                 gtk_widget_destroy (priv->tooltip_widget);
1239         }
1240         if (priv->file_targets) {
1241                 gtk_target_list_unref (priv->file_targets);
1242         }
1243
1244         G_OBJECT_CLASS (empathy_contact_list_view_parent_class)->finalize (object);
1245 }
1246
1247 static void
1248 contact_list_view_get_property (GObject    *object,
1249                                 guint       param_id,
1250                                 GValue     *value,
1251                                 GParamSpec *pspec)
1252 {
1253         EmpathyContactListViewPriv *priv;
1254
1255         priv = GET_PRIV (object);
1256
1257         switch (param_id) {
1258         case PROP_STORE:
1259                 g_value_set_object (value, priv->store);
1260                 break;
1261         case PROP_LIST_FEATURES:
1262                 g_value_set_flags (value, priv->list_features);
1263                 break;
1264         case PROP_CONTACT_FEATURES:
1265                 g_value_set_flags (value, priv->contact_features);
1266                 break;
1267         default:
1268                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1269                 break;
1270         };
1271 }
1272
1273 static void
1274 contact_list_view_set_property (GObject      *object,
1275                                 guint         param_id,
1276                                 const GValue *value,
1277                                 GParamSpec   *pspec)
1278 {
1279         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
1280         EmpathyContactListViewPriv *priv = GET_PRIV (object);
1281
1282         switch (param_id) {
1283         case PROP_STORE:
1284                 priv->store = g_value_dup_object (value);
1285                 contact_list_view_setup (view);
1286                 break;
1287         case PROP_LIST_FEATURES:
1288                 contact_list_view_set_list_features (view, g_value_get_flags (value));
1289                 break;
1290         case PROP_CONTACT_FEATURES:
1291                 priv->contact_features = g_value_get_flags (value);
1292                 break;
1293         default:
1294                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1295                 break;
1296         };
1297 }
1298
1299 static void
1300 empathy_contact_list_view_class_init (EmpathyContactListViewClass *klass)
1301 {
1302         GObjectClass     *object_class = G_OBJECT_CLASS (klass);
1303         GtkWidgetClass   *widget_class = GTK_WIDGET_CLASS (klass);
1304         GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
1305
1306         object_class->finalize = contact_list_view_finalize;
1307         object_class->get_property = contact_list_view_get_property;
1308         object_class->set_property = contact_list_view_set_property;
1309
1310         widget_class->drag_data_received = contact_list_view_drag_data_received;
1311         widget_class->drag_drop          = contact_list_view_drag_drop;
1312         widget_class->drag_begin         = contact_list_view_drag_begin;
1313         widget_class->drag_data_get      = contact_list_view_drag_data_get;
1314         widget_class->drag_end           = contact_list_view_drag_end;
1315         widget_class->drag_motion        = contact_list_view_drag_motion;
1316
1317         /* We use the class method to let user of this widget to connect to
1318          * the signal and stop emission of the signal so the default handler
1319          * won't be called. */
1320         tree_view_class->row_activated = contact_list_view_row_activated;
1321
1322         signals[DRAG_CONTACT_RECEIVED] =
1323                 g_signal_new ("drag-contact-received",
1324                               G_OBJECT_CLASS_TYPE (klass),
1325                               G_SIGNAL_RUN_LAST,
1326                               0,
1327                               NULL, NULL,
1328                               _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
1329                               G_TYPE_NONE,
1330                               3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
1331
1332         g_object_class_install_property (object_class,
1333                                          PROP_STORE,
1334                                          g_param_spec_object ("store",
1335                                                              "The store of the view",
1336                                                              "The store of the view",
1337                                                               EMPATHY_TYPE_CONTACT_LIST_STORE,
1338                                                               G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
1339         g_object_class_install_property (object_class,
1340                                          PROP_LIST_FEATURES,
1341                                          g_param_spec_flags ("list-features",
1342                                                              "Features of the view",
1343                                                              "Falgs for all enabled features",
1344                                                               EMPATHY_TYPE_CONTACT_LIST_FEATURE_FLAGS,
1345                                                               EMPATHY_CONTACT_LIST_FEATURE_NONE,
1346                                                               G_PARAM_READWRITE));
1347         g_object_class_install_property (object_class,
1348                                          PROP_CONTACT_FEATURES,
1349                                          g_param_spec_flags ("contact-features",
1350                                                              "Features of the contact menu",
1351                                                              "Falgs for all enabled features for the menu",
1352                                                               EMPATHY_TYPE_CONTACT_FEATURE_FLAGS,
1353                                                               EMPATHY_CONTACT_FEATURE_NONE,
1354                                                               G_PARAM_READWRITE));
1355
1356         g_type_class_add_private (object_class, sizeof (EmpathyContactListViewPriv));
1357 }
1358
1359 static void
1360 empathy_contact_list_view_init (EmpathyContactListView *view)
1361 {
1362         EmpathyContactListViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
1363                 EMPATHY_TYPE_CONTACT_LIST_VIEW, EmpathyContactListViewPriv);
1364
1365         view->priv = priv;
1366         /* Get saved group states. */
1367         empathy_contact_groups_get_all ();
1368
1369         gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
1370                                               empathy_contact_list_store_row_separator_func,
1371                                               NULL, NULL);
1372
1373         /* Set up drag target lists. */
1374         priv->file_targets = gtk_target_list_new (drag_types_dest_file,
1375                                                   G_N_ELEMENTS (drag_types_dest_file));
1376
1377         /* Connect to tree view signals rather than override. */
1378         g_signal_connect (view, "button-press-event",
1379                           G_CALLBACK (contact_list_view_button_press_event_cb),
1380                           NULL);
1381         g_signal_connect (view, "key-press-event",
1382                           G_CALLBACK (contact_list_view_key_press_event_cb),
1383                           NULL);
1384         g_signal_connect (view, "row-expanded",
1385                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1386                           GINT_TO_POINTER (TRUE));
1387         g_signal_connect (view, "row-collapsed",
1388                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1389                           GINT_TO_POINTER (FALSE));
1390         g_signal_connect (view, "query-tooltip",
1391                           G_CALLBACK (contact_list_view_query_tooltip_cb),
1392                           NULL);
1393 }
1394
1395 EmpathyContactListView *
1396 empathy_contact_list_view_new (EmpathyContactListStore        *store,
1397                                EmpathyContactListFeatureFlags  list_features,
1398                                EmpathyContactFeatureFlags      contact_features)
1399 {
1400         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), NULL);
1401
1402         return g_object_new (EMPATHY_TYPE_CONTACT_LIST_VIEW,
1403                              "store", store,
1404                              "contact-features", contact_features,
1405                              "list-features", list_features,
1406                              NULL);
1407 }
1408
1409 EmpathyContact *
1410 empathy_contact_list_view_dup_selected (EmpathyContactListView *view)
1411 {
1412         EmpathyContactListViewPriv *priv;
1413         GtkTreeSelection          *selection;
1414         GtkTreeIter                iter;
1415         GtkTreeModel              *model;
1416         EmpathyContact             *contact;
1417
1418         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1419
1420         priv = GET_PRIV (view);
1421
1422         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1423         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1424                 return NULL;
1425         }
1426
1427         gtk_tree_model_get (model, &iter,
1428                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
1429                             -1);
1430
1431         return contact;
1432 }
1433
1434 EmpathyContactListFlags
1435 empathy_contact_list_view_get_flags (EmpathyContactListView *view)
1436 {
1437         EmpathyContactListViewPriv *priv;
1438         GtkTreeSelection          *selection;
1439         GtkTreeIter                iter;
1440         GtkTreeModel              *model;
1441         EmpathyContactListFlags    flags;
1442
1443         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), 0);
1444
1445         priv = GET_PRIV (view);
1446
1447         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1448         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1449                 return 0;
1450         }
1451
1452         gtk_tree_model_get (model, &iter,
1453                             EMPATHY_CONTACT_LIST_STORE_COL_FLAGS, &flags,
1454                             -1);
1455
1456         return flags;
1457 }
1458
1459 gchar *
1460 empathy_contact_list_view_get_selected_group (EmpathyContactListView *view)
1461 {
1462         EmpathyContactListViewPriv *priv;
1463         GtkTreeSelection          *selection;
1464         GtkTreeIter                iter;
1465         GtkTreeModel              *model;
1466         gboolean                   is_group;
1467         gchar                     *name;
1468
1469         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1470
1471         priv = GET_PRIV (view);
1472
1473         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1474         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1475                 return NULL;
1476         }
1477
1478         gtk_tree_model_get (model, &iter,
1479                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1480                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1481                             -1);
1482
1483         if (!is_group) {
1484                 g_free (name);
1485                 return NULL;
1486         }
1487
1488         return name;
1489 }
1490
1491 static gboolean
1492 contact_list_view_remove_dialog_show (GtkWindow   *parent,
1493                                       const gchar *message,
1494                                       const gchar *secondary_text)
1495 {
1496         GtkWidget *dialog;
1497         gboolean res;
1498
1499         dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
1500                                          GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
1501                                          "%s", message);
1502         gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1503                                 GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1504                                 GTK_STOCK_DELETE, GTK_RESPONSE_YES,
1505                                 NULL);
1506         gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
1507                                                   "%s", secondary_text);
1508
1509         gtk_widget_show (dialog);
1510
1511         res = gtk_dialog_run (GTK_DIALOG (dialog));
1512         gtk_widget_destroy (dialog);
1513
1514         return (res == GTK_RESPONSE_YES);
1515 }
1516
1517 static void
1518 contact_list_view_group_remove_activate_cb (GtkMenuItem            *menuitem,
1519                                             EmpathyContactListView *view)
1520 {
1521         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1522         gchar                      *group;
1523
1524         group = empathy_contact_list_view_get_selected_group (view);
1525         if (group) {
1526                 gchar     *text;
1527                 GtkWindow *parent;
1528
1529                 text = g_strdup_printf (_("Do you really want to remove the group '%s'?"), group);
1530                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
1531                 if (contact_list_view_remove_dialog_show (parent, _("Removing group"), text)) {
1532                         EmpathyContactList *list;
1533
1534                         list = empathy_contact_list_store_get_list_iface (priv->store);
1535                         empathy_contact_list_remove_group (list, group);
1536                 }
1537
1538                 g_free (text);
1539         }
1540
1541         g_free (group);
1542 }
1543
1544 GtkWidget *
1545 empathy_contact_list_view_get_group_menu (EmpathyContactListView *view)
1546 {
1547         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1548         gchar                      *group;
1549         GtkWidget                  *menu;
1550         GtkWidget                  *item;
1551         GtkWidget                  *image;
1552
1553         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1554
1555         if (!(priv->list_features & (EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME |
1556                                      EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE))) {
1557                 return NULL;
1558         }
1559
1560         group = empathy_contact_list_view_get_selected_group (view);
1561         if (!group) {
1562                 return NULL;
1563         }
1564
1565         menu = gtk_menu_new ();
1566
1567         /* FIXME: Not implemented yet
1568         if (priv->features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME) {
1569                 item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
1570                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1571                 gtk_widget_show (item);
1572                 g_signal_connect (item, "activate",
1573                                   G_CALLBACK (contact_list_view_group_rename_activate_cb),
1574                                   view);
1575         }*/
1576
1577         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE) {
1578                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
1579                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
1580                                                       GTK_ICON_SIZE_MENU);
1581                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1582                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1583                 gtk_widget_show (item);
1584                 g_signal_connect (item, "activate",
1585                                   G_CALLBACK (contact_list_view_group_remove_activate_cb),
1586                                   view);
1587         }
1588
1589         g_free (group);
1590
1591         return menu;
1592 }
1593
1594 static void
1595 contact_list_view_remove_activate_cb (GtkMenuItem            *menuitem,
1596                                       EmpathyContactListView *view)
1597 {
1598         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1599         EmpathyContact             *contact;
1600
1601         contact = empathy_contact_list_view_dup_selected (view);
1602
1603         if (contact) {
1604                 gchar     *text;
1605                 GtkWindow *parent;
1606
1607                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
1608                 text = g_strdup_printf (_("Do you really want to remove the contact '%s'?"),
1609                                         empathy_contact_get_name (contact));
1610                 if (contact_list_view_remove_dialog_show (parent, _("Removing contact"), text)) {
1611                         EmpathyContactList *list;
1612
1613                         list = empathy_contact_list_store_get_list_iface (priv->store);
1614                         empathy_contact_list_remove (list, contact, "");
1615                 }
1616
1617                 g_free (text);
1618                 g_object_unref (contact);
1619         }
1620 }
1621
1622 GtkWidget *
1623 empathy_contact_list_view_get_contact_menu (EmpathyContactListView *view)
1624 {
1625         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1626         EmpathyContact             *contact;
1627         GtkWidget                  *menu;
1628         GtkWidget                  *item;
1629         GtkWidget                  *image;
1630         EmpathyContactListFlags     flags;
1631
1632         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1633
1634         contact = empathy_contact_list_view_dup_selected (view);
1635         if (!contact) {
1636                 return NULL;
1637         }
1638         flags = empathy_contact_list_view_get_flags (view);
1639
1640         menu = empathy_contact_menu_new (contact, priv->contact_features);
1641
1642         /* Remove contact */
1643         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_REMOVE &&
1644             flags & EMPATHY_CONTACT_LIST_CAN_REMOVE) {
1645                 /* create the menu if required, or just add a separator */
1646                 if (!menu) {
1647                         menu = gtk_menu_new ();
1648                 } else {
1649                         item = gtk_separator_menu_item_new ();
1650                         gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1651                         gtk_widget_show (item);
1652                 }
1653
1654                 /* Remove */
1655                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
1656                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
1657                                                       GTK_ICON_SIZE_MENU);
1658                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1659                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1660                 gtk_widget_show (item);
1661                 g_signal_connect (item, "activate",
1662                                   G_CALLBACK (contact_list_view_remove_activate_cb),
1663                                   view);
1664         }
1665
1666         g_object_unref (contact);
1667
1668         return menu;
1669 }
1670