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