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