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