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