]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-contact-list-view.c
8d234e375da2b7baf9e3ae175be292a8a2feeca3
[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-tp-contact-factory.h>
38 #include <libempathy/empathy-contact-list.h>
39 #include <libempathy/empathy-contact-groups.h>
40 #include <libempathy/empathy-dispatcher.h>
41 #include <libempathy/empathy-utils.h>
42
43 #include "empathy-contact-list-view.h"
44 #include "empathy-contact-list-store.h"
45 #include "empathy-images.h"
46 #include "empathy-cell-renderer-expander.h"
47 #include "empathy-cell-renderer-text.h"
48 #include "empathy-cell-renderer-activatable.h"
49 #include "empathy-ui-utils.h"
50 #include "empathy-gtk-enum-types.h"
51 #include "empathy-gtk-marshal.h"
52
53 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
54 #include <libempathy/empathy-debug.h>
55
56 /* Active users are those which have recently changed state
57  * (e.g. online, offline or from normal to a busy state).
58  */
59
60 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyContactListView)
61 typedef struct {
62         EmpathyContactListStore        *store;
63         GtkTreeRowReference            *drag_row;
64         EmpathyContactListFeatureFlags  list_features;
65         EmpathyContactFeatureFlags      contact_features;
66         GtkWidget                      *tooltip_widget;
67         GtkTargetList                  *file_targets;
68
69         GtkTreeModelFilter             *filter;
70         GtkWidget                      *search_widget;
71 } EmpathyContactListViewPriv;
72
73 typedef struct {
74         EmpathyContactListView *view;
75         GtkTreePath           *path;
76         guint                  timeout_id;
77 } DragMotionData;
78
79 typedef struct {
80         EmpathyContactListView *view;
81         EmpathyContact         *contact;
82         gboolean               remove;
83 } ShowActiveData;
84
85 enum {
86         PROP_0,
87         PROP_STORE,
88         PROP_LIST_FEATURES,
89         PROP_CONTACT_FEATURES,
90 };
91
92 enum DndDragType {
93         DND_DRAG_TYPE_CONTACT_ID,
94         DND_DRAG_TYPE_URI_LIST,
95         DND_DRAG_TYPE_STRING,
96 };
97
98 static const GtkTargetEntry drag_types_dest[] = {
99         { "text/path-list",  0, DND_DRAG_TYPE_URI_LIST },
100         { "text/uri-list",   0, DND_DRAG_TYPE_URI_LIST },
101         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
102         { "text/plain",      0, DND_DRAG_TYPE_STRING },
103         { "STRING",          0, DND_DRAG_TYPE_STRING },
104 };
105
106 static const GtkTargetEntry drag_types_dest_file[] = {
107         { "text/path-list",  0, DND_DRAG_TYPE_URI_LIST },
108         { "text/uri-list",   0, DND_DRAG_TYPE_URI_LIST },
109 };
110
111 static const GtkTargetEntry drag_types_source[] = {
112         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
113 };
114
115 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
116 static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
117
118 enum {
119         DRAG_CONTACT_RECEIVED,
120         LAST_SIGNAL
121 };
122
123 static guint signals[LAST_SIGNAL];
124
125 G_DEFINE_TYPE (EmpathyContactListView, empathy_contact_list_view, GTK_TYPE_TREE_VIEW);
126
127 static void
128 contact_list_view_tooltip_destroy_cb (GtkWidget              *widget,
129                                       EmpathyContactListView *view)
130 {
131         EmpathyContactListViewPriv *priv = GET_PRIV (view);
132
133         if (priv->tooltip_widget) {
134                 DEBUG ("Tooltip destroyed");
135                 g_object_unref (priv->tooltip_widget);
136                 priv->tooltip_widget = NULL;
137         }
138 }
139
140 static gboolean
141 contact_list_view_is_visible_contact (EmpathyContactListView *self,
142                                       EmpathyContact *contact)
143 {
144         EmpathyContactListViewPriv *priv = GET_PRIV (self);
145         EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
146         const gchar *str;
147         const gchar *p;
148         gchar *dup_str = NULL;
149         gboolean visible;
150
151         g_assert (live != NULL);
152
153         /* check alias name */
154         str = empathy_contact_get_alias (contact);
155         if (empathy_live_search_match (live, str))
156                 return TRUE;
157
158         /* check contact id, remove the @server.com part */
159         str = empathy_contact_get_id (contact);
160         p = strstr (str, "@");
161         if (p != NULL)
162                 str = dup_str = g_strndup (str, p - str);
163
164         visible = empathy_live_search_match (live, str);
165         g_free (dup_str);
166         if (visible)
167                 return TRUE;
168
169         /* FIXME: Add more rules here, we could check phone numbers in
170          * contact's vCard for example. */
171
172         return FALSE;
173 }
174
175 static gboolean
176 contact_list_view_filter_visible_func (GtkTreeModel *model,
177                                        GtkTreeIter  *iter,
178                                        gpointer      user_data)
179 {
180         EmpathyContactListView     *self = EMPATHY_CONTACT_LIST_VIEW (user_data);
181         EmpathyContactListViewPriv *priv = GET_PRIV (self);
182         EmpathyContact             *contact = NULL;
183         gboolean                    is_group, is_separator, valid;
184         GtkTreeIter                 child_iter;
185         gboolean                    visible;
186
187         if (priv->search_widget == NULL ||
188             !gtk_widget_get_visible (priv->search_widget))
189                 return TRUE;
190
191         gtk_tree_model_get (model, iter,
192                 EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
193                 EMPATHY_CONTACT_LIST_STORE_COL_IS_SEPARATOR, &is_separator,
194                 EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
195                 -1);
196
197         if (contact != NULL) {
198                 visible = contact_list_view_is_visible_contact (self, contact);
199                 g_object_unref (contact);
200                 return visible;
201         }
202
203         if (is_separator) {
204                 return TRUE;
205         }
206
207         /* Not a contact, not a separator, must be a group */
208         g_return_val_if_fail (is_group, FALSE);
209
210         /* only show groups which are not empty */
211         for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
212              valid; valid = gtk_tree_model_iter_next (model, &child_iter)) {
213                 gtk_tree_model_get (model, &child_iter,
214                         EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
215                         -1);
216
217                 if (contact == NULL)
218                         continue;
219
220                 visible = contact_list_view_is_visible_contact (self, contact);
221                 g_object_unref (contact);
222
223                 /* show group if it has at least one visible contact in it */
224                 if (visible)
225                         return TRUE;
226         }
227
228         return FALSE;
229 }
230
231 static gboolean
232 contact_list_view_query_tooltip_cb (EmpathyContactListView *view,
233                                     gint                    x,
234                                     gint                    y,
235                                     gboolean                keyboard_mode,
236                                     GtkTooltip             *tooltip,
237                                     gpointer                user_data)
238 {
239         EmpathyContactListViewPriv *priv = GET_PRIV (view);
240         EmpathyContact             *contact;
241         GtkTreeModel               *model;
242         GtkTreeIter                 iter;
243         GtkTreePath                *path;
244         static gint                 running = 0;
245         gboolean                    ret = FALSE;
246
247         /* Avoid an infinite loop. See GNOME bug #574377 */
248         if (running > 0) {
249                 return FALSE;
250         }
251         running++;
252
253         /* Don't show the tooltip if there's already a popup menu */
254         if (gtk_menu_get_for_attach_widget (GTK_WIDGET (view)) != NULL) {
255                 goto OUT;
256         }
257
258         if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (view), &x, &y,
259                                                 keyboard_mode,
260                                                 &model, &path, &iter)) {
261                 goto OUT;
262         }
263
264         gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (view), tooltip, path);
265         gtk_tree_path_free (path);
266
267         gtk_tree_model_get (model, &iter,
268                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
269                             -1);
270         if (!contact) {
271                 goto OUT;
272         }
273
274         if (!priv->tooltip_widget) {
275                 priv->tooltip_widget = empathy_contact_widget_new (contact,
276                         EMPATHY_CONTACT_WIDGET_FOR_TOOLTIP |
277                         EMPATHY_CONTACT_WIDGET_SHOW_LOCATION);
278                 gtk_container_set_border_width (
279                         GTK_CONTAINER (priv->tooltip_widget), 8);
280                 g_object_ref (priv->tooltip_widget);
281                 g_signal_connect (priv->tooltip_widget, "destroy",
282                                   G_CALLBACK (contact_list_view_tooltip_destroy_cb),
283                                   view);
284                 gtk_widget_show (priv->tooltip_widget);
285         } else {
286                 empathy_contact_widget_set_contact (priv->tooltip_widget,
287                                                     contact);
288         }
289
290         gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
291         ret = TRUE;
292
293         g_object_unref (contact);
294 OUT:
295         running--;
296
297         return ret;
298 }
299
300 typedef struct {
301         gchar *new_group;
302         gchar *old_group;
303         GdkDragAction action;
304 } DndGetContactData;
305
306 static void
307 contact_list_view_dnd_get_contact_free (DndGetContactData *data)
308 {
309         g_free (data->new_group);
310         g_free (data->old_group);
311         g_slice_free (DndGetContactData, data);
312 }
313
314 static void
315 contact_list_view_drag_got_contact (TpConnection            *connection,
316                                     EmpathyContact          *contact,
317                                     const GError            *error,
318                                     gpointer                 user_data,
319                                     GObject                 *view)
320 {
321         EmpathyContactListViewPriv *priv = GET_PRIV (view);
322         DndGetContactData          *data = user_data;
323         EmpathyContactList         *list;
324
325         if (error != NULL) {
326                 DEBUG ("Error: %s", error->message);
327                 return;
328         }
329
330         DEBUG ("contact %s (%d) dragged from '%s' to '%s'",
331                 empathy_contact_get_id (contact),
332                 empathy_contact_get_handle (contact),
333                 data->old_group, data->new_group);
334
335         list = empathy_contact_list_store_get_list_iface (priv->store);
336
337         if (!tp_strdiff (data->new_group, EMPATHY_CONTACT_LIST_STORE_FAVORITE)) {
338                 /* Mark contact as favourite */
339                 empathy_contact_list_add_to_favourites (list, contact);
340                 return;
341         }
342
343         if (!tp_strdiff (data->old_group, EMPATHY_CONTACT_LIST_STORE_FAVORITE)) {
344                 /* Remove contact as favourite */
345                 empathy_contact_list_remove_from_favourites (list, contact);
346                 /* Don't try to remove it */
347                 g_free (data->old_group);
348                 data->old_group = NULL;
349         }
350
351         if (data->new_group) {
352                 empathy_contact_list_add_to_group (list, contact, data->new_group);
353         }
354         if (data->old_group && data->action == GDK_ACTION_MOVE) {
355                 empathy_contact_list_remove_from_group (list, contact, data->old_group);
356         }
357 }
358
359 static gboolean
360 group_can_be_modified (const gchar *name,
361                        gboolean     is_fake_group,
362                        gboolean     adding)
363 {
364         /* Real groups can always be modified */
365         if (!is_fake_group)
366                 return TRUE;
367
368         /* The favorite fake group can be modified so users can
369          * add/remove favorites using DnD */
370         if (!tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_FAVORITE))
371                 return TRUE;
372
373         /* We can remove contacts from the 'ungrouped' fake group */
374         if (!adding && !tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_UNGROUPED))
375                 return TRUE;
376
377         return FALSE;
378 }
379
380 static gboolean
381 contact_list_view_contact_drag_received (GtkWidget         *view,
382                                          GdkDragContext    *context,
383                                          GtkTreeModel      *model,
384                                          GtkTreePath       *path,
385                                          GtkSelectionData  *selection)
386 {
387         EmpathyContactListViewPriv *priv;
388         TpAccountManager           *account_manager;
389         TpConnection               *connection = NULL;
390         TpAccount                  *account = NULL;
391         DndGetContactData          *data;
392         GtkTreePath                *source_path;
393         const gchar   *sel_data;
394         gchar        **strv = NULL;
395         const gchar   *account_id = NULL;
396         const gchar   *contact_id = NULL;
397         gchar         *new_group = NULL;
398         gchar         *old_group = NULL;
399         gboolean       success = TRUE;
400         gboolean       new_group_is_fake, old_group_is_fake = TRUE;
401
402         priv = GET_PRIV (view);
403
404         sel_data = (const gchar *) gtk_selection_data_get_data (selection);
405         new_group = empathy_contact_list_store_get_parent_group (model,
406                                                                  path, NULL, &new_group_is_fake);
407
408         if (!group_can_be_modified (new_group, new_group_is_fake, TRUE))
409                 return FALSE;
410
411         /* Get source group information. */
412         if (priv->drag_row) {
413                 source_path = gtk_tree_row_reference_get_path (priv->drag_row);
414                 if (source_path) {
415                         old_group = empathy_contact_list_store_get_parent_group (
416                                                                                  model, source_path, NULL, &old_group_is_fake);
417                         gtk_tree_path_free (source_path);
418                 }
419         }
420
421         if (!group_can_be_modified (old_group, old_group_is_fake, FALSE))
422                 return FALSE;
423
424         if (!tp_strdiff (old_group, new_group)) {
425                 g_free (new_group);
426                 g_free (old_group);
427                 return FALSE;
428         }
429
430         account_manager = tp_account_manager_dup ();
431         strv = g_strsplit (sel_data, ":", 2);
432         if (g_strv_length (strv) == 2) {
433                 account_id = strv[0];
434                 contact_id = strv[1];
435                 account = tp_account_manager_ensure_account (account_manager, account_id);
436         }
437         if (account) {
438                 connection = tp_account_get_connection (account);
439         }
440
441         if (!connection) {
442                 DEBUG ("Failed to get connection for account '%s'", account_id);
443                 success = FALSE;
444                 g_free (new_group);
445                 g_free (old_group);
446                 g_object_unref (account_manager);
447                 return FALSE;
448         }
449
450         data = g_slice_new0 (DndGetContactData);
451         data->new_group = new_group;
452         data->old_group = old_group;
453         data->action = gdk_drag_context_get_selected_action (context);
454
455         /* FIXME: We should probably wait for the cb before calling
456          * gtk_drag_finish */
457         empathy_tp_contact_factory_get_from_id (connection, contact_id,
458                                                 contact_list_view_drag_got_contact,
459                                                 data, (GDestroyNotify) contact_list_view_dnd_get_contact_free,
460                                                 G_OBJECT (view));
461         g_strfreev (strv);
462         g_object_unref (account_manager);
463
464         return TRUE;
465 }
466
467 static gboolean
468 contact_list_view_file_drag_received (GtkWidget         *view,
469                                       GdkDragContext    *context,
470                                       GtkTreeModel      *model,
471                                       GtkTreePath       *path,
472                                       GtkSelectionData  *selection)
473 {
474         GtkTreeIter     iter;
475         const gchar    *sel_data;
476         EmpathyContact *contact;
477
478         sel_data = (const gchar *) gtk_selection_data_get_data (selection);
479
480         gtk_tree_model_get_iter (model, &iter, path);
481         gtk_tree_model_get (model, &iter,
482                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
483                             -1);
484         if (!contact) {
485                 return FALSE;
486         }
487
488         empathy_send_file_from_uri_list (contact, sel_data);
489
490         g_object_unref (contact);
491
492         return TRUE;
493 }
494
495 static void
496 contact_list_view_drag_data_received (GtkWidget         *view,
497                                       GdkDragContext    *context,
498                                       gint               x,
499                                       gint               y,
500                                       GtkSelectionData  *selection,
501                                       guint              info,
502                                       guint              time_)
503 {
504         GtkTreeModel               *model;
505         gboolean                    is_row;
506         GtkTreeViewDropPosition     position;
507         GtkTreePath                *path;
508         gboolean                    success = TRUE;
509
510         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
511
512         /* Get destination group information. */
513         is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (view),
514                                                     x,
515                                                     y,
516                                                     &path,
517                                                     &position);
518         if (!is_row) {
519                 success = FALSE;
520         }
521         else if (info == DND_DRAG_TYPE_CONTACT_ID || info == DND_DRAG_TYPE_STRING) {
522                 success = contact_list_view_contact_drag_received (view,
523                                                                    context,
524                                                                    model,
525                                                                    path,
526                                                                    selection);
527         }
528         else if (info == DND_DRAG_TYPE_URI_LIST) {
529                 success = contact_list_view_file_drag_received (view,
530                                                                 context,
531                                                                 model,
532                                                                 path,
533                                                                 selection);
534         }
535
536         gtk_tree_path_free (path);
537         gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
538 }
539
540 static gboolean
541 contact_list_view_drag_motion_cb (DragMotionData *data)
542 {
543         gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view),
544                                   data->path,
545                                   FALSE);
546
547         data->timeout_id = 0;
548
549         return FALSE;
550 }
551
552 static gboolean
553 contact_list_view_drag_motion (GtkWidget      *widget,
554                                GdkDragContext *context,
555                                gint            x,
556                                gint            y,
557                                guint           time_)
558 {
559         EmpathyContactListViewPriv *priv;
560         GtkTreeModel               *model;
561         GdkAtom                target;
562         GtkTreeIter            iter;
563         static DragMotionData *dm = NULL;
564         GtkTreePath           *path;
565         gboolean               is_row;
566         gboolean               is_different = FALSE;
567         gboolean               cleanup = TRUE;
568         gboolean               retval = TRUE;
569
570         priv = GET_PRIV (EMPATHY_CONTACT_LIST_VIEW (widget));
571         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
572
573         is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
574                                                 x,
575                                                 y,
576                                                 &path,
577                                                 NULL,
578                                                 NULL,
579                                                 NULL);
580
581         cleanup &= (!dm);
582
583         if (is_row) {
584                 cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
585                 is_different = (!dm || (dm && gtk_tree_path_compare (dm->path, path) != 0));
586         } else {
587                 cleanup &= FALSE;
588         }
589
590         if (path == NULL) {
591                 /* Coordinates don't point to an actual row, so make sure the pointer
592                    and highlighting don't indicate that a drag is possible.
593                  */
594                 gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
595                 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
596                 return FALSE;
597         }
598         target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
599         gtk_tree_model_get_iter (model, &iter, path);
600
601         if (target == GDK_NONE) {
602                 /* If target == GDK_NONE, then we don't have a target that can be
603                    dropped on a contact.  This means a contact drag.  If we're
604                    pointing to a group, highlight it.  Otherwise, if the contact
605                    we're pointing to is in a group, highlight that.  Otherwise,
606                    set the drag position to before the first row for a drag into
607                    the "non-group" at the top.
608                  */
609                 GtkTreeIter  group_iter;
610                 gboolean     is_group;
611                 GtkTreePath *group_path;
612                 gtk_tree_model_get (model, &iter,
613                                     EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
614                                     -1);
615                 if (is_group) {
616                         group_iter = iter;
617                 }
618                 else {
619                         if (gtk_tree_model_iter_parent (model, &group_iter, &iter))
620                                 gtk_tree_model_get (model, &group_iter,
621                                                     EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
622                                                     -1);
623                 }
624                 if (is_group) {
625                         gdk_drag_status (context, GDK_ACTION_MOVE, time_);
626                         group_path = gtk_tree_model_get_path (model, &group_iter);
627                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
628                                                          group_path,
629                                                          GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
630                         gtk_tree_path_free (group_path);
631                 }
632                 else {
633                         group_path = gtk_tree_path_new_first ();
634                         gdk_drag_status (context, GDK_ACTION_MOVE, time_);
635                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
636                                                          group_path,
637                                                          GTK_TREE_VIEW_DROP_BEFORE);
638                 }
639         }
640         else {
641                 /* This is a file drag, and it can only be dropped on contacts,
642                    not groups.
643                  */
644                 EmpathyContact *contact;
645                 gtk_tree_model_get (model, &iter,
646                                     EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
647                                     -1);
648                 if (contact != NULL &&
649                     empathy_contact_is_online (contact) &&
650                     (empathy_contact_get_capabilities (contact) & EMPATHY_CAPABILITIES_FT)) {
651                         gdk_drag_status (context, GDK_ACTION_COPY, time_);
652                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
653                                                          path,
654                                                          GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
655                         g_object_unref (contact);
656                 }
657                 else {
658                         gdk_drag_status (context, 0, time_);
659                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
660                         retval = FALSE;
661                 }
662         }
663
664         if (!is_different && !cleanup) {
665                 return retval;
666         }
667
668         if (dm) {
669                 gtk_tree_path_free (dm->path);
670                 if (dm->timeout_id) {
671                         g_source_remove (dm->timeout_id);
672                 }
673
674                 g_free (dm);
675
676                 dm = NULL;
677         }
678
679         if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) {
680                 dm = g_new0 (DragMotionData, 1);
681
682                 dm->view = EMPATHY_CONTACT_LIST_VIEW (widget);
683                 dm->path = gtk_tree_path_copy (path);
684
685                 dm->timeout_id = g_timeout_add_seconds (1,
686                         (GSourceFunc) contact_list_view_drag_motion_cb,
687                         dm);
688         }
689
690         return retval;
691 }
692
693 static void
694 contact_list_view_drag_begin (GtkWidget      *widget,
695                               GdkDragContext *context)
696 {
697         EmpathyContactListViewPriv *priv;
698         GtkTreeSelection          *selection;
699         GtkTreeModel              *model;
700         GtkTreePath               *path;
701         GtkTreeIter                iter;
702
703         priv = GET_PRIV (widget);
704
705         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_begin (widget,
706                                                                               context);
707
708         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
709         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
710                 return;
711         }
712
713         path = gtk_tree_model_get_path (model, &iter);
714         priv->drag_row = gtk_tree_row_reference_new (model, path);
715         gtk_tree_path_free (path);
716 }
717
718 static void
719 contact_list_view_drag_data_get (GtkWidget        *widget,
720                                  GdkDragContext   *context,
721                                  GtkSelectionData *selection,
722                                  guint             info,
723                                  guint             time_)
724 {
725         EmpathyContactListViewPriv *priv;
726         GtkTreePath                *src_path;
727         GtkTreeIter                 iter;
728         GtkTreeModel               *model;
729         EmpathyContact             *contact;
730         TpAccount                  *account;
731         const gchar                *contact_id;
732         const gchar                *account_id;
733         gchar                      *str;
734
735         priv = GET_PRIV (widget);
736
737         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
738         if (!priv->drag_row) {
739                 return;
740         }
741
742         src_path = gtk_tree_row_reference_get_path (priv->drag_row);
743         if (!src_path) {
744                 return;
745         }
746
747         if (!gtk_tree_model_get_iter (model, &iter, src_path)) {
748                 gtk_tree_path_free (src_path);
749                 return;
750         }
751
752         gtk_tree_path_free (src_path);
753
754         contact = empathy_contact_list_view_dup_selected (EMPATHY_CONTACT_LIST_VIEW (widget));
755         if (!contact) {
756                 return;
757         }
758
759         account = empathy_contact_get_account (contact);
760         account_id = tp_proxy_get_object_path (account);
761         contact_id = empathy_contact_get_id (contact);
762         g_object_unref (contact);
763         str = g_strconcat (account_id, ":", contact_id, NULL);
764
765         if (info == DND_DRAG_TYPE_CONTACT_ID) {
766                 gtk_selection_data_set (selection, drag_atoms_source[info], 8,
767                                         (guchar *) str, strlen (str) + 1);
768         }
769
770         g_free (str);
771 }
772
773 static void
774 contact_list_view_drag_end (GtkWidget      *widget,
775                             GdkDragContext *context)
776 {
777         EmpathyContactListViewPriv *priv;
778
779         priv = GET_PRIV (widget);
780
781         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_end (widget,
782                                                                             context);
783
784         if (priv->drag_row) {
785                 gtk_tree_row_reference_free (priv->drag_row);
786                 priv->drag_row = NULL;
787         }
788 }
789
790 static gboolean
791 contact_list_view_drag_drop (GtkWidget      *widget,
792                              GdkDragContext *drag_context,
793                              gint            x,
794                              gint            y,
795                              guint           time_)
796 {
797         return FALSE;
798 }
799
800 typedef struct {
801         EmpathyContactListView *view;
802         guint                   button;
803         guint32                 time;
804 } MenuPopupData;
805
806 static void
807 menu_deactivate_cb (GtkMenuShell *menushell,
808                     gpointer user_data)
809 {
810         gtk_menu_detach (GTK_MENU (menushell));
811
812         /* FIXME: we shouldn't have to disconnec the signal (bgo #641327) */
813         g_signal_handlers_disconnect_by_func (menushell,
814                 menu_deactivate_cb, user_data);
815 }
816
817 static gboolean
818 contact_list_view_popup_menu_idle_cb (gpointer user_data)
819 {
820         MenuPopupData *data = user_data;
821         GtkWidget     *menu;
822
823         menu = empathy_contact_list_view_get_contact_menu (data->view);
824         if (!menu) {
825                 menu = empathy_contact_list_view_get_group_menu (data->view);
826         }
827
828         if (menu) {
829                 gtk_menu_attach_to_widget (GTK_MENU (menu),
830                                            GTK_WIDGET (data->view), NULL);
831                 gtk_widget_show (menu);
832                 gtk_menu_popup (GTK_MENU (menu),
833                                 NULL, NULL, NULL, NULL,
834                                 data->button, data->time);
835
836                 /* menu is initially unowned but gtk_menu_attach_to_widget() taked its
837                  * floating ref. We can either wait that the treeview releases its ref
838                  * when it will be destroyed (when leaving Empathy) or explicitely
839                  * detach the menu when it's not displayed any more.
840                  * We go for the latter as we don't want to keep useless menus in memory
841                  * during the whole lifetime of Empathy. */
842                 g_signal_connect (menu, "deactivate", G_CALLBACK (menu_deactivate_cb),
843                         NULL);
844         }
845
846         g_slice_free (MenuPopupData, data);
847
848         return FALSE;
849 }
850
851 static gboolean
852 contact_list_view_button_press_event_cb (EmpathyContactListView *view,
853                                          GdkEventButton         *event,
854                                          gpointer                user_data)
855 {
856         if (event->button == 3) {
857                 MenuPopupData *data;
858
859                 data = g_slice_new (MenuPopupData);
860                 data->view = view;
861                 data->button = event->button;
862                 data->time = event->time;
863                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
864         }
865
866         return FALSE;
867 }
868
869 static gboolean
870 contact_list_view_key_press_event_cb (EmpathyContactListView *view,
871                                       GdkEventKey            *event,
872                                       gpointer                user_data)
873 {
874         if (event->keyval == GDK_KEY_Menu) {
875                 MenuPopupData *data;
876
877                 data = g_slice_new (MenuPopupData);
878                 data->view = view;
879                 data->button = 0;
880                 data->time = event->time;
881                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
882         }
883
884         return FALSE;
885 }
886
887 static void
888 contact_list_view_row_activated (GtkTreeView       *view,
889                                  GtkTreePath       *path,
890                                  GtkTreeViewColumn *column)
891 {
892         EmpathyContactListViewPriv *priv = GET_PRIV (view);
893         EmpathyContact             *contact;
894         GtkTreeModel               *model;
895         GtkTreeIter                 iter;
896
897         if (!(priv->contact_features & EMPATHY_CONTACT_FEATURE_CHAT)) {
898                 return;
899         }
900
901         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
902         gtk_tree_model_get_iter (model, &iter, path);
903         gtk_tree_model_get (model, &iter,
904                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
905                             -1);
906
907         if (contact) {
908                 DEBUG ("Starting a chat");
909                 empathy_dispatcher_chat_with_contact (contact,
910                         gtk_get_current_event_time ());
911                 g_object_unref (contact);
912         }
913 }
914
915 static void
916 contact_list_view_call_activated_cb (
917     EmpathyCellRendererActivatable *cell,
918     const gchar                    *path_string,
919     EmpathyContactListView         *view)
920 {
921         GtkWidget *menu;
922         GtkTreeModel *model;
923         GtkTreeIter iter;
924         EmpathyContact *contact;
925         GdkEventButton *event;
926         GtkMenuShell *shell;
927         GtkWidget *item;
928
929         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
930         if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
931                 return;
932
933         gtk_tree_model_get (model, &iter,
934                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
935                             -1);
936         if (contact == NULL)
937                 return;
938
939         event = (GdkEventButton *) gtk_get_current_event ();
940
941         menu = empathy_context_menu_new (GTK_WIDGET (view));
942         shell = GTK_MENU_SHELL (menu);
943
944         /* audio */
945         item = empathy_contact_audio_call_menu_item_new (contact);
946         gtk_menu_shell_append (shell, item);
947         gtk_widget_show (item);
948
949         /* video */
950         item = empathy_contact_video_call_menu_item_new (contact);
951         gtk_menu_shell_append (shell, item);
952         gtk_widget_show (item);
953
954         gtk_widget_show (menu);
955         gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
956                         event->button, event->time);
957
958         g_object_unref (contact);
959 }
960
961 static void
962 contact_list_view_cell_set_background (EmpathyContactListView *view,
963                                        GtkCellRenderer        *cell,
964                                        gboolean                is_group,
965                                        gboolean                is_active)
966 {
967         if (!is_group && is_active) {
968                 GdkRGBA color;
969                 GtkStyleContext *style;
970
971                 style = gtk_widget_get_style_context (GTK_WIDGET (view));
972
973                 gtk_style_context_get_background_color (style, GTK_STATE_FLAG_SELECTED,
974         &color);
975
976                 /* Here we take the current theme colour and add it to
977                  * the colour for white and average the two. This
978                  * gives a colour which is inline with the theme but
979                  * slightly whiter.
980                  */
981                 empathy_make_color_whiter (&color);
982
983                 g_object_set (cell,
984                               "cell-background-rgba", &color,
985                               NULL);
986         } else {
987                 g_object_set (cell,
988                               "cell-background-rgba", NULL,
989                               NULL);
990         }
991 }
992
993 static void
994 contact_list_view_pixbuf_cell_data_func (GtkTreeViewColumn      *tree_column,
995                                          GtkCellRenderer        *cell,
996                                          GtkTreeModel           *model,
997                                          GtkTreeIter            *iter,
998                                          EmpathyContactListView *view)
999 {
1000         GdkPixbuf *pixbuf;
1001         gboolean   is_group;
1002         gboolean   is_active;
1003
1004         gtk_tree_model_get (model, iter,
1005                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1006                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1007                             EMPATHY_CONTACT_LIST_STORE_COL_ICON_STATUS, &pixbuf,
1008                             -1);
1009
1010         g_object_set (cell,
1011                       "visible", !is_group,
1012                       "pixbuf", pixbuf,
1013                       NULL);
1014
1015         if (pixbuf != NULL) {
1016                 g_object_unref (pixbuf);
1017         }
1018
1019         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1020 }
1021
1022 static void
1023 contact_list_view_group_icon_cell_data_func (GtkTreeViewColumn     *tree_column,
1024                                              GtkCellRenderer       *cell,
1025                                              GtkTreeModel          *model,
1026                                              GtkTreeIter           *iter,
1027                                              EmpathyContactListView *view)
1028 {
1029         GdkPixbuf *pixbuf = NULL;
1030         gboolean is_group;
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_NAME, &name,
1036                             -1);
1037
1038         if (!is_group)
1039                 goto out;
1040
1041         if (!tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_FAVORITE)) {
1042                 pixbuf = empathy_pixbuf_from_icon_name ("emblem-favorite",
1043                         GTK_ICON_SIZE_MENU);
1044         }
1045         else if (!tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_PEOPLE_NEARBY)) {
1046                 pixbuf = empathy_pixbuf_from_icon_name ("im-local-xmpp",
1047                         GTK_ICON_SIZE_MENU);
1048         }
1049
1050 out:
1051         g_object_set (cell,
1052                       "visible", pixbuf != NULL,
1053                       "pixbuf", pixbuf,
1054                       NULL);
1055
1056         if (pixbuf != NULL)
1057                 g_object_unref (pixbuf);
1058
1059         g_free (name);
1060 }
1061
1062 static void
1063 contact_list_view_audio_call_cell_data_func (
1064                                        GtkTreeViewColumn      *tree_column,
1065                                        GtkCellRenderer        *cell,
1066                                        GtkTreeModel           *model,
1067                                        GtkTreeIter            *iter,
1068                                        EmpathyContactListView *view)
1069 {
1070         gboolean is_group;
1071         gboolean is_active;
1072         gboolean can_audio, can_video;
1073
1074         gtk_tree_model_get (model, iter,
1075                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1076                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1077                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_AUDIO_CALL, &can_audio,
1078                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_VIDEO_CALL, &can_video,
1079                             -1);
1080
1081         g_object_set (cell,
1082                       "visible", !is_group && (can_audio || can_video),
1083                       "icon-name", can_video? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
1084                       NULL);
1085
1086         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1087 }
1088
1089 static void
1090 contact_list_view_avatar_cell_data_func (GtkTreeViewColumn      *tree_column,
1091                                          GtkCellRenderer        *cell,
1092                                          GtkTreeModel           *model,
1093                                          GtkTreeIter            *iter,
1094                                          EmpathyContactListView *view)
1095 {
1096         GdkPixbuf *pixbuf;
1097         gboolean   show_avatar;
1098         gboolean   is_group;
1099         gboolean   is_active;
1100
1101         gtk_tree_model_get (model, iter,
1102                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR, &pixbuf,
1103                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
1104                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1105                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1106                             -1);
1107
1108         g_object_set (cell,
1109                       "visible", !is_group && show_avatar,
1110                       "pixbuf", pixbuf,
1111                       NULL);
1112
1113         if (pixbuf) {
1114                 g_object_unref (pixbuf);
1115         }
1116
1117         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1118 }
1119
1120 static void
1121 contact_list_view_text_cell_data_func (GtkTreeViewColumn      *tree_column,
1122                                        GtkCellRenderer        *cell,
1123                                        GtkTreeModel           *model,
1124                                        GtkTreeIter            *iter,
1125                                        EmpathyContactListView *view)
1126 {
1127         gboolean is_group;
1128         gboolean is_active;
1129
1130         gtk_tree_model_get (model, iter,
1131                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1132                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1133                             -1);
1134
1135         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1136 }
1137
1138 static void
1139 contact_list_view_expander_cell_data_func (GtkTreeViewColumn      *column,
1140                                            GtkCellRenderer        *cell,
1141                                            GtkTreeModel           *model,
1142                                            GtkTreeIter            *iter,
1143                                            EmpathyContactListView *view)
1144 {
1145         gboolean is_group;
1146         gboolean is_active;
1147
1148         gtk_tree_model_get (model, iter,
1149                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1150                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1151                             -1);
1152
1153         if (gtk_tree_model_iter_has_child (model, iter)) {
1154                 GtkTreePath *path;
1155                 gboolean     row_expanded;
1156
1157                 path = gtk_tree_model_get_path (model, iter);
1158                 row_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (gtk_tree_view_column_get_tree_view (column)), path);
1159                 gtk_tree_path_free (path);
1160
1161                 g_object_set (cell,
1162                               "visible", TRUE,
1163                               "expander-style", row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
1164                               NULL);
1165         } else {
1166                 g_object_set (cell, "visible", FALSE, NULL);
1167         }
1168
1169         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1170 }
1171
1172 static void
1173 contact_list_view_row_expand_or_collapse_cb (EmpathyContactListView *view,
1174                                              GtkTreeIter            *iter,
1175                                              GtkTreePath            *path,
1176                                              gpointer                user_data)
1177 {
1178         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1179         GtkTreeModel               *model;
1180         gchar                      *name;
1181         gboolean                    expanded;
1182
1183         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE)) {
1184                 return;
1185         }
1186
1187         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1188
1189         gtk_tree_model_get (model, iter,
1190                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1191                             -1);
1192
1193         expanded = GPOINTER_TO_INT (user_data);
1194         empathy_contact_group_set_expanded (name, expanded);
1195
1196         g_free (name);
1197 }
1198
1199 static gboolean
1200 contact_list_view_start_search_cb (EmpathyContactListView *view,
1201                                    gpointer                data)
1202 {
1203         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1204
1205         if (priv->search_widget == NULL)
1206                 return FALSE;
1207
1208         if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
1209                 gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
1210         else
1211                 gtk_widget_show (GTK_WIDGET (priv->search_widget));
1212
1213         return TRUE;
1214 }
1215
1216 static void
1217 contact_list_view_search_text_notify_cb (EmpathyLiveSearch      *search,
1218                                          GParamSpec             *pspec,
1219                                          EmpathyContactListView *view)
1220 {
1221         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1222         GtkTreePath *path;
1223         GtkTreeViewColumn *focus_column;
1224         GtkTreeModel *model;
1225         GtkTreeIter iter;
1226         gboolean set_cursor = FALSE;
1227
1228         gtk_tree_model_filter_refilter (priv->filter);
1229
1230         /* Set cursor on the first contact. If it is already set on a group,
1231          * set it on its first child contact. Note that first child of a group
1232          * is its separator, that's why we actually set to the 2nd
1233          */
1234
1235         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1236         gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1237
1238         if (path == NULL) {
1239                 path = gtk_tree_path_new_from_string ("0:1");
1240                 set_cursor = TRUE;
1241         } else if (gtk_tree_path_get_depth (path) < 2) {
1242                 gboolean is_group;
1243
1244                 gtk_tree_model_get_iter (model, &iter, path);
1245                 gtk_tree_model_get (model, &iter,
1246                         EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1247                         -1);
1248
1249                 if (is_group) {
1250                         gtk_tree_path_down (path);
1251                         gtk_tree_path_next (path);
1252                         set_cursor = TRUE;
1253                 }
1254         }
1255
1256         if (set_cursor) {
1257                 /* FIXME: Workaround for GTK bug #621651, we have to make sure
1258                  * the path is valid. */
1259                 if (gtk_tree_model_get_iter (model, &iter, path)) {
1260                         gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path,
1261                                 focus_column, FALSE);
1262                 }
1263         }
1264
1265         gtk_tree_path_free (path);
1266 }
1267
1268 static void
1269 contact_list_view_search_activate_cb (GtkWidget *search,
1270                                       EmpathyContactListView *view)
1271 {
1272         GtkTreePath *path;
1273         GtkTreeViewColumn *focus_column;
1274
1275         gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1276         if (path != NULL) {
1277                 gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path,
1278                         focus_column);
1279                 gtk_tree_path_free (path);
1280
1281                 gtk_widget_hide (search);
1282         }
1283 }
1284
1285 static gboolean
1286 contact_list_view_search_key_navigation_cb (GtkWidget *search,
1287                                             GdkEvent *event,
1288                                             EmpathyContactListView *view)
1289 {
1290         GdkEventKey *eventkey = ((GdkEventKey *) event);
1291         gboolean ret = FALSE;
1292
1293         if (eventkey->keyval == GDK_KEY_Up || eventkey->keyval == GDK_KEY_Down) {
1294                 GdkEvent *new_event;
1295
1296                 new_event = gdk_event_copy (event);
1297                 gtk_widget_grab_focus (GTK_WIDGET (view));
1298                 ret = gtk_widget_event (GTK_WIDGET (view), new_event);
1299                 gtk_widget_grab_focus (search);
1300
1301                 gdk_event_free (new_event);
1302         }
1303
1304         return ret;
1305 }
1306
1307 static void
1308 contact_list_view_search_hide_cb (EmpathyLiveSearch      *search,
1309                                   EmpathyContactListView *view)
1310 {
1311         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1312         GtkTreeModel               *model;
1313         GtkTreeIter                 iter;
1314         gboolean                    valid = FALSE;
1315
1316         /* block expand or collapse handlers, they would write the
1317          * expand or collapsed setting to file otherwise */
1318         g_signal_handlers_block_by_func (view,
1319                 contact_list_view_row_expand_or_collapse_cb,
1320                 GINT_TO_POINTER (TRUE));
1321         g_signal_handlers_block_by_func (view,
1322                 contact_list_view_row_expand_or_collapse_cb,
1323                 GINT_TO_POINTER (FALSE));
1324
1325         /* restore which groups are expanded and which are not */
1326         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1327         for (valid = gtk_tree_model_get_iter_first (model, &iter);
1328              valid; valid = gtk_tree_model_iter_next (model, &iter)) {
1329                 gboolean      is_group;
1330                 gchar        *name = NULL;
1331                 GtkTreePath  *path;
1332
1333                 gtk_tree_model_get (model, &iter,
1334                         EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1335                         EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1336                         -1);
1337
1338                 if (!is_group) {
1339                         g_free (name);
1340                         continue;
1341                 }
1342
1343                 path = gtk_tree_model_get_path (model, &iter);
1344                 if ((priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) == 0 ||
1345                     empathy_contact_group_get_expanded (name)) {
1346                         gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path,
1347                                 TRUE);
1348                 } else {
1349                         gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1350                 }
1351
1352                 gtk_tree_path_free (path);
1353                 g_free (name);
1354         }
1355
1356         /* unblock expand or collapse handlers */
1357         g_signal_handlers_unblock_by_func (view,
1358                 contact_list_view_row_expand_or_collapse_cb,
1359                 GINT_TO_POINTER (TRUE));
1360         g_signal_handlers_unblock_by_func (view,
1361                 contact_list_view_row_expand_or_collapse_cb,
1362                 GINT_TO_POINTER (FALSE));
1363 }
1364
1365 static void
1366 contact_list_view_search_show_cb (EmpathyLiveSearch      *search,
1367                                   EmpathyContactListView *view)
1368 {
1369         /* block expand or collapse handlers during expand all, they would
1370          * write the expand or collapsed setting to file otherwise */
1371         g_signal_handlers_block_by_func (view,
1372                 contact_list_view_row_expand_or_collapse_cb,
1373                 GINT_TO_POINTER (TRUE));
1374
1375         gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
1376
1377         g_signal_handlers_unblock_by_func (view,
1378                 contact_list_view_row_expand_or_collapse_cb,
1379                 GINT_TO_POINTER (TRUE));
1380 }
1381
1382 typedef struct {
1383         EmpathyContactListView *view;
1384         GtkTreeRowReference *row_ref;
1385         gboolean expand;
1386 } ExpandData;
1387
1388 static gboolean
1389 contact_list_view_expand_idle_cb (gpointer user_data)
1390 {
1391         ExpandData *data = user_data;
1392         GtkTreePath *path;
1393
1394         path = gtk_tree_row_reference_get_path (data->row_ref);
1395         if (path == NULL)
1396                 goto done;
1397
1398         g_signal_handlers_block_by_func (data->view,
1399                 contact_list_view_row_expand_or_collapse_cb,
1400                 GINT_TO_POINTER (data->expand));
1401
1402         if (data->expand) {
1403                 gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), path,
1404                     TRUE);
1405         } else {
1406                 gtk_tree_view_collapse_row (GTK_TREE_VIEW (data->view), path);
1407         }
1408         gtk_tree_path_free (path);
1409
1410         g_signal_handlers_unblock_by_func (data->view,
1411                 contact_list_view_row_expand_or_collapse_cb,
1412                 GINT_TO_POINTER (data->expand));
1413
1414 done:
1415         g_object_unref (data->view);
1416         gtk_tree_row_reference_free (data->row_ref);
1417         g_slice_free (ExpandData, data);
1418
1419         return FALSE;
1420 }
1421
1422 static void
1423 contact_list_view_row_has_child_toggled_cb (GtkTreeModel           *model,
1424                                             GtkTreePath            *path,
1425                                             GtkTreeIter            *iter,
1426                                             EmpathyContactListView *view)
1427 {
1428         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1429         gboolean  is_group = FALSE;
1430         gchar    *name = NULL;
1431         ExpandData *data;
1432
1433         gtk_tree_model_get (model, iter,
1434                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1435                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1436                             -1);
1437
1438         if (!is_group || EMP_STR_EMPTY (name)) {
1439                 g_free (name);
1440                 return;
1441         }
1442
1443         data = g_slice_new0 (ExpandData);
1444         data->view = g_object_ref (view);
1445         data->row_ref = gtk_tree_row_reference_new (model, path);
1446         data->expand =
1447                 (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) == 0 ||
1448                 (priv->search_widget != NULL && gtk_widget_get_visible (priv->search_widget)) ||
1449                 empathy_contact_group_get_expanded (name);
1450
1451         /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1452          * gtk_tree_model_filter_refilter () */
1453         g_idle_add (contact_list_view_expand_idle_cb, data);
1454
1455         g_free (name);
1456 }
1457
1458 static void
1459 contact_list_view_verify_group_visibility (EmpathyContactListView *view,
1460                                            GtkTreePath            *path)
1461 {
1462         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1463         GtkTreeModel *model;
1464         GtkTreePath *parent_path;
1465         GtkTreeIter parent_iter;
1466
1467         if (gtk_tree_path_get_depth (path) < 2)
1468                 return;
1469
1470         /* A group row is visible if and only if at least one if its child is
1471          * visible. So when a row is inserted/deleted/changed in the base model,
1472          * that could modify the visibility of its parent in the filter model.
1473          */
1474
1475         model = GTK_TREE_MODEL (priv->store);
1476         parent_path = gtk_tree_path_copy (path);
1477         gtk_tree_path_up (parent_path);
1478         if (gtk_tree_model_get_iter (model, &parent_iter, parent_path)) {
1479                 /* This tells the filter to verify the visibility of that row,
1480                  * and show/hide it if necessary */
1481                 gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
1482                         parent_path, &parent_iter);
1483         }
1484         gtk_tree_path_free (parent_path);
1485 }
1486
1487 static void
1488 contact_list_view_store_row_changed_cb (GtkTreeModel           *model,
1489                                         GtkTreePath            *path,
1490                                         GtkTreeIter            *iter,
1491                                         EmpathyContactListView *view)
1492 {
1493         contact_list_view_verify_group_visibility (view, path);
1494 }
1495
1496 static void
1497 contact_list_view_store_row_deleted_cb (GtkTreeModel           *model,
1498                                         GtkTreePath            *path,
1499                                         EmpathyContactListView *view)
1500 {
1501         contact_list_view_verify_group_visibility (view, path);
1502 }
1503
1504 static void
1505 contact_list_view_constructed (GObject *object)
1506 {
1507         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
1508         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1509         GtkCellRenderer            *cell;
1510         GtkTreeViewColumn          *col;
1511         guint                       i;
1512
1513         priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
1514                         GTK_TREE_MODEL (priv->store), NULL));
1515         gtk_tree_model_filter_set_visible_func (priv->filter,
1516                         contact_list_view_filter_visible_func,
1517                         view, NULL);
1518
1519         g_signal_connect (priv->filter, "row-has-child-toggled",
1520                           G_CALLBACK (contact_list_view_row_has_child_toggled_cb),
1521                           view);
1522
1523         gtk_tree_view_set_model (GTK_TREE_VIEW (view),
1524                                  GTK_TREE_MODEL (priv->filter));
1525
1526         tp_g_signal_connect_object (priv->store, "row-changed",
1527                 G_CALLBACK (contact_list_view_store_row_changed_cb),
1528                 view, 0);
1529         tp_g_signal_connect_object (priv->store, "row-inserted",
1530                 G_CALLBACK (contact_list_view_store_row_changed_cb),
1531                 view, 0);
1532         tp_g_signal_connect_object (priv->store, "row-deleted",
1533                 G_CALLBACK (contact_list_view_store_row_deleted_cb),
1534                 view, 0);
1535
1536         /* Setup view */
1537         /* Setting reorderable is a hack that gets us row previews as drag icons
1538            for free.  We override all the drag handlers.  It's tricky to get the
1539            position of the drag icon right in drag_begin.  GtkTreeView has special
1540            voodoo for it, so we let it do the voodoo that he do.
1541          */
1542         g_object_set (view,
1543                       "headers-visible", FALSE,
1544                       "reorderable", TRUE,
1545                       "show-expanders", FALSE,
1546                       NULL);
1547
1548         col = gtk_tree_view_column_new ();
1549
1550         /* State */
1551         cell = gtk_cell_renderer_pixbuf_new ();
1552         gtk_tree_view_column_pack_start (col, cell, FALSE);
1553         gtk_tree_view_column_set_cell_data_func (
1554                 col, cell,
1555                 (GtkTreeCellDataFunc) contact_list_view_pixbuf_cell_data_func,
1556                 view, NULL);
1557
1558         g_object_set (cell,
1559                       "xpad", 5,
1560                       "ypad", 1,
1561                       "visible", FALSE,
1562                       NULL);
1563
1564         /* Group icon */
1565         cell = gtk_cell_renderer_pixbuf_new ();
1566         gtk_tree_view_column_pack_start (col, cell, FALSE);
1567         gtk_tree_view_column_set_cell_data_func (
1568                 col, cell,
1569                 (GtkTreeCellDataFunc) contact_list_view_group_icon_cell_data_func,
1570                 view, NULL);
1571
1572         g_object_set (cell,
1573                       "xpad", 0,
1574                       "ypad", 0,
1575                       "visible", FALSE,
1576                       "width", 16,
1577                       "height", 16,
1578                       NULL);
1579
1580         /* Name */
1581         cell = empathy_cell_renderer_text_new ();
1582         gtk_tree_view_column_pack_start (col, cell, TRUE);
1583         gtk_tree_view_column_set_cell_data_func (
1584                 col, cell,
1585                 (GtkTreeCellDataFunc) contact_list_view_text_cell_data_func,
1586                 view, NULL);
1587
1588         gtk_tree_view_column_add_attribute (col, cell,
1589                                             "name", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
1590         gtk_tree_view_column_add_attribute (col, cell,
1591                                             "text", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
1592         gtk_tree_view_column_add_attribute (col, cell,
1593                                             "presence-type", EMPATHY_CONTACT_LIST_STORE_COL_PRESENCE_TYPE);
1594         gtk_tree_view_column_add_attribute (col, cell,
1595                                             "status", EMPATHY_CONTACT_LIST_STORE_COL_STATUS);
1596         gtk_tree_view_column_add_attribute (col, cell,
1597                                             "is_group", EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP);
1598         gtk_tree_view_column_add_attribute (col, cell,
1599                                             "compact", EMPATHY_CONTACT_LIST_STORE_COL_COMPACT);
1600
1601         /* Audio Call Icon */
1602         cell = empathy_cell_renderer_activatable_new ();
1603         gtk_tree_view_column_pack_start (col, cell, FALSE);
1604         gtk_tree_view_column_set_cell_data_func (
1605                 col, cell,
1606                 (GtkTreeCellDataFunc) contact_list_view_audio_call_cell_data_func,
1607                 view, NULL);
1608
1609         g_object_set (cell,
1610                       "visible", FALSE,
1611                       NULL);
1612
1613         g_signal_connect (cell, "path-activated",
1614                           G_CALLBACK (contact_list_view_call_activated_cb),
1615                           view);
1616
1617         /* Avatar */
1618         cell = gtk_cell_renderer_pixbuf_new ();
1619         gtk_tree_view_column_pack_start (col, cell, FALSE);
1620         gtk_tree_view_column_set_cell_data_func (
1621                 col, cell,
1622                 (GtkTreeCellDataFunc) contact_list_view_avatar_cell_data_func,
1623                 view, NULL);
1624
1625         g_object_set (cell,
1626                       "xpad", 0,
1627                       "ypad", 0,
1628                       "visible", FALSE,
1629                       "width", 32,
1630                       "height", 32,
1631                       NULL);
1632
1633         /* Expander */
1634         cell = empathy_cell_renderer_expander_new ();
1635         gtk_tree_view_column_pack_end (col, cell, FALSE);
1636         gtk_tree_view_column_set_cell_data_func (
1637                 col, cell,
1638                 (GtkTreeCellDataFunc) contact_list_view_expander_cell_data_func,
1639                 view, NULL);
1640
1641         /* Actually add the column now we have added all cell renderers */
1642         gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1643
1644         /* Drag & Drop. */
1645         for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i) {
1646                 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target,
1647                                                       FALSE);
1648         }
1649
1650         for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i) {
1651                 drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1652                                                         FALSE);
1653         }
1654 }
1655
1656 static void
1657 contact_list_view_set_list_features (EmpathyContactListView         *view,
1658                                      EmpathyContactListFeatureFlags  features)
1659 {
1660         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1661         gboolean                    has_tooltip;
1662
1663         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view));
1664
1665         priv->list_features = features;
1666
1667         /* Update DnD source/dest */
1668         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DRAG) {
1669                 gtk_drag_source_set (GTK_WIDGET (view),
1670                                      GDK_BUTTON1_MASK,
1671                                      drag_types_source,
1672                                      G_N_ELEMENTS (drag_types_source),
1673                                      GDK_ACTION_MOVE | GDK_ACTION_COPY);
1674         } else {
1675                 gtk_drag_source_unset (GTK_WIDGET (view));
1676
1677         }
1678
1679         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DROP) {
1680                 gtk_drag_dest_set (GTK_WIDGET (view),
1681                                    GTK_DEST_DEFAULT_ALL,
1682                                    drag_types_dest,
1683                                    G_N_ELEMENTS (drag_types_dest),
1684                                    GDK_ACTION_MOVE | GDK_ACTION_COPY);
1685         } else {
1686                 /* FIXME: URI could still be droped depending on FT feature */
1687                 gtk_drag_dest_unset (GTK_WIDGET (view));
1688         }
1689
1690         /* Update has-tooltip */
1691         has_tooltip = (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_TOOLTIP) != 0;
1692         gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1693 }
1694
1695 static void
1696 contact_list_view_dispose (GObject *object)
1697 {
1698         EmpathyContactListView *view = EMPATHY_CONTACT_LIST_VIEW (object);
1699         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1700
1701         if (priv->store) {
1702                 g_object_unref (priv->store);
1703                 priv->store = NULL;
1704         }
1705         if (priv->filter) {
1706                 g_object_unref (priv->filter);
1707                 priv->filter = NULL;
1708         }
1709         if (priv->tooltip_widget) {
1710                 gtk_widget_destroy (priv->tooltip_widget);
1711                 priv->tooltip_widget = NULL;
1712         }
1713         if (priv->file_targets) {
1714                 gtk_target_list_unref (priv->file_targets);
1715                 priv->file_targets = NULL;
1716         }
1717
1718         empathy_contact_list_view_set_live_search (view, NULL);
1719
1720         G_OBJECT_CLASS (empathy_contact_list_view_parent_class)->dispose (object);
1721 }
1722
1723 static void
1724 contact_list_view_get_property (GObject    *object,
1725                                 guint       param_id,
1726                                 GValue     *value,
1727                                 GParamSpec *pspec)
1728 {
1729         EmpathyContactListViewPriv *priv;
1730
1731         priv = GET_PRIV (object);
1732
1733         switch (param_id) {
1734         case PROP_STORE:
1735                 g_value_set_object (value, priv->store);
1736                 break;
1737         case PROP_LIST_FEATURES:
1738                 g_value_set_flags (value, priv->list_features);
1739                 break;
1740         case PROP_CONTACT_FEATURES:
1741                 g_value_set_flags (value, priv->contact_features);
1742                 break;
1743         default:
1744                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1745                 break;
1746         };
1747 }
1748
1749 static void
1750 contact_list_view_set_property (GObject      *object,
1751                                 guint         param_id,
1752                                 const GValue *value,
1753                                 GParamSpec   *pspec)
1754 {
1755         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
1756         EmpathyContactListViewPriv *priv = GET_PRIV (object);
1757
1758         switch (param_id) {
1759         case PROP_STORE:
1760                 priv->store = g_value_dup_object (value);
1761                 break;
1762         case PROP_LIST_FEATURES:
1763                 contact_list_view_set_list_features (view, g_value_get_flags (value));
1764                 break;
1765         case PROP_CONTACT_FEATURES:
1766                 priv->contact_features = g_value_get_flags (value);
1767                 break;
1768         default:
1769                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1770                 break;
1771         };
1772 }
1773
1774 static void
1775 empathy_contact_list_view_class_init (EmpathyContactListViewClass *klass)
1776 {
1777         GObjectClass     *object_class = G_OBJECT_CLASS (klass);
1778         GtkWidgetClass   *widget_class = GTK_WIDGET_CLASS (klass);
1779         GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
1780
1781         object_class->constructed        = contact_list_view_constructed;
1782         object_class->dispose            = contact_list_view_dispose;
1783         object_class->get_property       = contact_list_view_get_property;
1784         object_class->set_property       = contact_list_view_set_property;
1785
1786         widget_class->drag_data_received = contact_list_view_drag_data_received;
1787         widget_class->drag_drop          = contact_list_view_drag_drop;
1788         widget_class->drag_begin         = contact_list_view_drag_begin;
1789         widget_class->drag_data_get      = contact_list_view_drag_data_get;
1790         widget_class->drag_end           = contact_list_view_drag_end;
1791         widget_class->drag_motion        = contact_list_view_drag_motion;
1792
1793         /* We use the class method to let user of this widget to connect to
1794          * the signal and stop emission of the signal so the default handler
1795          * won't be called. */
1796         tree_view_class->row_activated = contact_list_view_row_activated;
1797
1798         signals[DRAG_CONTACT_RECEIVED] =
1799                 g_signal_new ("drag-contact-received",
1800                               G_OBJECT_CLASS_TYPE (klass),
1801                               G_SIGNAL_RUN_LAST,
1802                               0,
1803                               NULL, NULL,
1804                               _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
1805                               G_TYPE_NONE,
1806                               3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
1807
1808         g_object_class_install_property (object_class,
1809                                          PROP_STORE,
1810                                          g_param_spec_object ("store",
1811                                                              "The store of the view",
1812                                                              "The store of the view",
1813                                                               EMPATHY_TYPE_CONTACT_LIST_STORE,
1814                                                               G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
1815         g_object_class_install_property (object_class,
1816                                          PROP_LIST_FEATURES,
1817                                          g_param_spec_flags ("list-features",
1818                                                              "Features of the view",
1819                                                              "Flags for all enabled features",
1820                                                               EMPATHY_TYPE_CONTACT_LIST_FEATURE_FLAGS,
1821                                                               EMPATHY_CONTACT_LIST_FEATURE_NONE,
1822                                                               G_PARAM_READWRITE));
1823         g_object_class_install_property (object_class,
1824                                          PROP_CONTACT_FEATURES,
1825                                          g_param_spec_flags ("contact-features",
1826                                                              "Features of the contact menu",
1827                                                              "Flags for all enabled features for the menu",
1828                                                               EMPATHY_TYPE_CONTACT_FEATURE_FLAGS,
1829                                                               EMPATHY_CONTACT_FEATURE_NONE,
1830                                                               G_PARAM_READWRITE));
1831
1832         g_type_class_add_private (object_class, sizeof (EmpathyContactListViewPriv));
1833 }
1834
1835 static void
1836 empathy_contact_list_view_init (EmpathyContactListView *view)
1837 {
1838         EmpathyContactListViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
1839                 EMPATHY_TYPE_CONTACT_LIST_VIEW, EmpathyContactListViewPriv);
1840
1841         view->priv = priv;
1842
1843         /* Get saved group states. */
1844         empathy_contact_groups_get_all ();
1845
1846         gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
1847                                               empathy_contact_list_store_row_separator_func,
1848                                               NULL, NULL);
1849
1850         /* Set up drag target lists. */
1851         priv->file_targets = gtk_target_list_new (drag_types_dest_file,
1852                                                   G_N_ELEMENTS (drag_types_dest_file));
1853
1854         /* Connect to tree view signals rather than override. */
1855         g_signal_connect (view, "button-press-event",
1856                           G_CALLBACK (contact_list_view_button_press_event_cb),
1857                           NULL);
1858         g_signal_connect (view, "key-press-event",
1859                           G_CALLBACK (contact_list_view_key_press_event_cb),
1860                           NULL);
1861         g_signal_connect (view, "row-expanded",
1862                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1863                           GINT_TO_POINTER (TRUE));
1864         g_signal_connect (view, "row-collapsed",
1865                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1866                           GINT_TO_POINTER (FALSE));
1867         g_signal_connect (view, "query-tooltip",
1868                           G_CALLBACK (contact_list_view_query_tooltip_cb),
1869                           NULL);
1870 }
1871
1872 EmpathyContactListView *
1873 empathy_contact_list_view_new (EmpathyContactListStore        *store,
1874                                EmpathyContactListFeatureFlags  list_features,
1875                                EmpathyContactFeatureFlags      contact_features)
1876 {
1877         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), NULL);
1878
1879         return g_object_new (EMPATHY_TYPE_CONTACT_LIST_VIEW,
1880                              "store", store,
1881                              "contact-features", contact_features,
1882                              "list-features", list_features,
1883                              NULL);
1884 }
1885
1886 EmpathyContact *
1887 empathy_contact_list_view_dup_selected (EmpathyContactListView *view)
1888 {
1889         EmpathyContactListViewPriv *priv;
1890         GtkTreeSelection          *selection;
1891         GtkTreeIter                iter;
1892         GtkTreeModel              *model;
1893         EmpathyContact             *contact;
1894
1895         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1896
1897         priv = GET_PRIV (view);
1898
1899         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1900         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1901                 return NULL;
1902         }
1903
1904         gtk_tree_model_get (model, &iter,
1905                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
1906                             -1);
1907
1908         return contact;
1909 }
1910
1911 EmpathyContactListFlags
1912 empathy_contact_list_view_get_flags (EmpathyContactListView *view)
1913 {
1914         EmpathyContactListViewPriv *priv;
1915         GtkTreeSelection          *selection;
1916         GtkTreeIter                iter;
1917         GtkTreeModel              *model;
1918         EmpathyContactListFlags    flags;
1919
1920         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), 0);
1921
1922         priv = GET_PRIV (view);
1923
1924         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1925         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1926                 return 0;
1927         }
1928
1929         gtk_tree_model_get (model, &iter,
1930                             EMPATHY_CONTACT_LIST_STORE_COL_FLAGS, &flags,
1931                             -1);
1932
1933         return flags;
1934 }
1935
1936 gchar *
1937 empathy_contact_list_view_get_selected_group (EmpathyContactListView *view,
1938                                               gboolean *is_fake_group)
1939 {
1940         EmpathyContactListViewPriv *priv;
1941         GtkTreeSelection          *selection;
1942         GtkTreeIter                iter;
1943         GtkTreeModel              *model;
1944         gboolean                   is_group;
1945         gchar                     *name;
1946         gboolean                   fake;
1947
1948         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1949
1950         priv = GET_PRIV (view);
1951
1952         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1953         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1954                 return NULL;
1955         }
1956
1957         gtk_tree_model_get (model, &iter,
1958                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1959                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1960                             EMPATHY_CONTACT_LIST_STORE_COL_IS_FAKE_GROUP, &fake,
1961                             -1);
1962
1963         if (!is_group) {
1964                 g_free (name);
1965                 return NULL;
1966         }
1967
1968         if (is_fake_group != NULL)
1969                 *is_fake_group = fake;
1970
1971         return name;
1972 }
1973
1974 static gboolean
1975 contact_list_view_remove_dialog_show (GtkWindow   *parent,
1976                                       const gchar *message,
1977                                       const gchar *secondary_text)
1978 {
1979         GtkWidget *dialog;
1980         gboolean res;
1981
1982         dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
1983                                          GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
1984                                          "%s", message);
1985         gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1986                                 GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1987                                 GTK_STOCK_DELETE, GTK_RESPONSE_YES,
1988                                 NULL);
1989         gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
1990                                                   "%s", secondary_text);
1991
1992         gtk_widget_show (dialog);
1993
1994         res = gtk_dialog_run (GTK_DIALOG (dialog));
1995         gtk_widget_destroy (dialog);
1996
1997         return (res == GTK_RESPONSE_YES);
1998 }
1999
2000 static void
2001 contact_list_view_group_remove_activate_cb (GtkMenuItem            *menuitem,
2002                                             EmpathyContactListView *view)
2003 {
2004         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2005         gchar                      *group;
2006
2007         group = empathy_contact_list_view_get_selected_group (view, NULL);
2008         if (group) {
2009                 gchar     *text;
2010                 GtkWindow *parent;
2011
2012                 text = g_strdup_printf (_("Do you really want to remove the group '%s'?"), group);
2013                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2014                 if (contact_list_view_remove_dialog_show (parent, _("Removing group"), text)) {
2015                         EmpathyContactList *list;
2016
2017                         list = empathy_contact_list_store_get_list_iface (priv->store);
2018                         empathy_contact_list_remove_group (list, group);
2019                 }
2020
2021                 g_free (text);
2022         }
2023
2024         g_free (group);
2025 }
2026
2027 GtkWidget *
2028 empathy_contact_list_view_get_group_menu (EmpathyContactListView *view)
2029 {
2030         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2031         gchar                      *group;
2032         GtkWidget                  *menu;
2033         GtkWidget                  *item;
2034         GtkWidget                  *image;
2035         gboolean                   is_fake_group;
2036
2037         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
2038
2039         if (!(priv->list_features & (EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME |
2040                                      EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE))) {
2041                 return NULL;
2042         }
2043
2044         group = empathy_contact_list_view_get_selected_group (view, &is_fake_group);
2045         if (!group || is_fake_group) {
2046                 /* We can't alter fake groups */
2047                 return NULL;
2048         }
2049
2050         menu = gtk_menu_new ();
2051
2052         /* FIXME: Not implemented yet
2053         if (priv->features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME) {
2054                 item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2055                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2056                 gtk_widget_show (item);
2057                 g_signal_connect (item, "activate",
2058                                   G_CALLBACK (contact_list_view_group_rename_activate_cb),
2059                                   view);
2060         }*/
2061
2062         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE) {
2063                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2064                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2065                                                       GTK_ICON_SIZE_MENU);
2066                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2067                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2068                 gtk_widget_show (item);
2069                 g_signal_connect (item, "activate",
2070                                   G_CALLBACK (contact_list_view_group_remove_activate_cb),
2071                                   view);
2072         }
2073
2074         g_free (group);
2075
2076         return menu;
2077 }
2078
2079 static void
2080 contact_list_view_remove_activate_cb (GtkMenuItem            *menuitem,
2081                                       EmpathyContactListView *view)
2082 {
2083         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2084         EmpathyContact             *contact;
2085
2086         contact = empathy_contact_list_view_dup_selected (view);
2087
2088         if (contact) {
2089                 gchar     *text;
2090                 GtkWindow *parent;
2091
2092                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2093                 text = g_strdup_printf (_("Do you really want to remove the contact '%s'?"),
2094                                         empathy_contact_get_alias (contact));
2095                 if (contact_list_view_remove_dialog_show (parent, _("Removing contact"), text)) {
2096                         EmpathyContactList *list;
2097
2098                         list = empathy_contact_list_store_get_list_iface (priv->store);
2099                         empathy_contact_list_remove (list, contact, "");
2100                 }
2101
2102                 g_free (text);
2103                 g_object_unref (contact);
2104         }
2105 }
2106
2107 GtkWidget *
2108 empathy_contact_list_view_get_contact_menu (EmpathyContactListView *view)
2109 {
2110         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2111         EmpathyContact             *contact;
2112         GtkWidget                  *menu;
2113         GtkWidget                  *item;
2114         GtkWidget                  *image;
2115         EmpathyContactListFlags     flags;
2116
2117         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
2118
2119         contact = empathy_contact_list_view_dup_selected (view);
2120         if (!contact) {
2121                 return NULL;
2122         }
2123         flags = empathy_contact_list_view_get_flags (view);
2124
2125         menu = empathy_contact_menu_new (contact, priv->contact_features);
2126
2127         /* Remove contact */
2128         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_REMOVE &&
2129             flags & EMPATHY_CONTACT_LIST_CAN_REMOVE) {
2130                 /* create the menu if required, or just add a separator */
2131                 if (!menu) {
2132                         menu = gtk_menu_new ();
2133                 } else {
2134                         item = gtk_separator_menu_item_new ();
2135                         gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2136                         gtk_widget_show (item);
2137                 }
2138
2139                 /* Remove */
2140                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2141                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2142                                                       GTK_ICON_SIZE_MENU);
2143                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2144                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2145                 gtk_widget_show (item);
2146                 g_signal_connect (item, "activate",
2147                                   G_CALLBACK (contact_list_view_remove_activate_cb),
2148                                   view);
2149         }
2150
2151         g_object_unref (contact);
2152
2153         return menu;
2154 }
2155
2156 void
2157 empathy_contact_list_view_set_live_search (EmpathyContactListView *view,
2158                                            EmpathyLiveSearch      *search)
2159 {
2160         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2161
2162         /* remove old handlers if old search was not null */
2163         if (priv->search_widget != NULL) {
2164                 g_signal_handlers_disconnect_by_func (view,
2165                         contact_list_view_start_search_cb,
2166                         NULL);
2167
2168                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2169                         contact_list_view_search_text_notify_cb,
2170                         view);
2171                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2172                         contact_list_view_search_activate_cb,
2173                         view);
2174                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2175                         contact_list_view_search_key_navigation_cb,
2176                         view);
2177                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2178                         contact_list_view_search_hide_cb,
2179                         view);
2180                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2181                         contact_list_view_search_show_cb,
2182                         view);
2183                 g_object_unref (priv->search_widget);
2184                 priv->search_widget = NULL;
2185         }
2186
2187         /* connect handlers if new search is not null */
2188         if (search != NULL) {
2189                 priv->search_widget = g_object_ref (search);
2190
2191                 g_signal_connect (view, "start-interactive-search",
2192                                   G_CALLBACK (contact_list_view_start_search_cb),
2193                                   NULL);
2194
2195                 g_signal_connect (priv->search_widget, "notify::text",
2196                         G_CALLBACK (contact_list_view_search_text_notify_cb),
2197                         view);
2198                 g_signal_connect (priv->search_widget, "activate",
2199                         G_CALLBACK (contact_list_view_search_activate_cb),
2200                         view);
2201                 g_signal_connect (priv->search_widget, "key-navigation",
2202                         G_CALLBACK (contact_list_view_search_key_navigation_cb),
2203                         view);
2204                 g_signal_connect (priv->search_widget, "hide",
2205                         G_CALLBACK (contact_list_view_search_hide_cb),
2206                         view);
2207                 g_signal_connect (priv->search_widget, "show",
2208                         G_CALLBACK (contact_list_view_search_show_cb),
2209                         view);
2210         }
2211 }