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