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