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