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