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