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