]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
Merge branch 'LOLOOOLOLOLOLLLOLLLOLOLLL'
[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 <telepathy-glib/account-manager.h>
36 #include <telepathy-glib/util.h>
37
38 #include <folks/folks.h>
39 #include <folks/folks-telepathy.h>
40
41 #include <libempathy/empathy-individual-manager.h>
42 #include <libempathy/empathy-contact-groups.h>
43 #include <libempathy/empathy-request-util.h>
44 #include <libempathy/empathy-utils.h>
45
46 #include "empathy-individual-view.h"
47 #include "empathy-individual-menu.h"
48 #include "empathy-individual-store.h"
49 #include "empathy-contact-dialogs.h"
50 #include "empathy-individual-dialogs.h"
51 #include "empathy-images.h"
52 #include "empathy-linking-dialog.h"
53 #include "empathy-cell-renderer-expander.h"
54 #include "empathy-cell-renderer-text.h"
55 #include "empathy-cell-renderer-activatable.h"
56 #include "empathy-ui-utils.h"
57 #include "empathy-gtk-enum-types.h"
58 #include "empathy-gtk-marshal.h"
59
60 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
61 #include <libempathy/empathy-debug.h>
62
63 /* Active users are those which have recently changed state
64  * (e.g. online, offline or from normal to a busy state).
65  */
66
67 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualView)
68 typedef struct
69 {
70   EmpathyIndividualStore *store;
71   GtkTreeRowReference *drag_row;
72   EmpathyIndividualViewFeatureFlags view_features;
73   EmpathyIndividualFeatureFlags individual_features;
74   GtkWidget *tooltip_widget;
75
76   gboolean show_offline;
77   gboolean show_untrusted;
78
79   GtkTreeModelFilter *filter;
80   GtkWidget *search_widget;
81
82   guint expand_groups_idle_handler;
83   /* owned string (group name) -> bool (whether to expand/contract) */
84   GHashTable *expand_groups;
85
86   /* Auto scroll */
87   guint auto_scroll_timeout_id;
88   /* Distance between mouse pointer and the nearby border. Negative when
89      scrolling updards.*/
90   gint distance;
91
92   GtkTreeModelFilterVisibleFunc custom_filter;
93   gpointer custom_filter_data;
94 } EmpathyIndividualViewPriv;
95
96 typedef struct
97 {
98   EmpathyIndividualView *view;
99   GtkTreePath *path;
100   guint timeout_id;
101 } DragMotionData;
102
103 typedef struct
104 {
105   EmpathyIndividualView *view;
106   FolksIndividual *individual;
107   gboolean remove;
108 } ShowActiveData;
109
110 enum
111 {
112   PROP_0,
113   PROP_STORE,
114   PROP_VIEW_FEATURES,
115   PROP_INDIVIDUAL_FEATURES,
116   PROP_SHOW_OFFLINE,
117   PROP_SHOW_UNTRUSTED,
118 };
119
120 /* TODO: re-add DRAG_TYPE_CONTACT_ID, for the case that we're dragging around
121  * specific EmpathyContacts (between/in/out of Individuals) */
122 typedef enum
123 {
124   DND_DRAG_TYPE_UNKNOWN = -1,
125   DND_DRAG_TYPE_INDIVIDUAL_ID = 0,
126   DND_DRAG_TYPE_PERSONA_ID,
127   DND_DRAG_TYPE_URI_LIST,
128   DND_DRAG_TYPE_STRING,
129 } DndDragType;
130
131 #define DRAG_TYPE(T,I) \
132   { (gchar *) T, 0, I }
133
134 static const GtkTargetEntry drag_types_dest[] = {
135   DRAG_TYPE ("text/individual-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
136   DRAG_TYPE ("text/persona-id", DND_DRAG_TYPE_PERSONA_ID),
137   DRAG_TYPE ("text/path-list", DND_DRAG_TYPE_URI_LIST),
138   DRAG_TYPE ("text/uri-list", DND_DRAG_TYPE_URI_LIST),
139   DRAG_TYPE ("text/plain", DND_DRAG_TYPE_STRING),
140   DRAG_TYPE ("STRING", DND_DRAG_TYPE_STRING),
141 };
142
143 static const GtkTargetEntry drag_types_source[] = {
144   DRAG_TYPE ("text/individual-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
145 };
146
147 #undef DRAG_TYPE
148
149 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
150
151 enum
152 {
153   DRAG_INDIVIDUAL_RECEIVED,
154   DRAG_PERSONA_RECEIVED,
155   LAST_SIGNAL
156 };
157
158 static guint signals[LAST_SIGNAL];
159
160 G_DEFINE_TYPE (EmpathyIndividualView, empathy_individual_view,
161     GTK_TYPE_TREE_VIEW);
162
163 static void
164 individual_view_tooltip_destroy_cb (GtkWidget *widget,
165     EmpathyIndividualView *view)
166 {
167   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
168
169   tp_clear_object (&priv->tooltip_widget);
170 }
171
172 static gboolean
173 individual_view_query_tooltip_cb (EmpathyIndividualView *view,
174     gint x,
175     gint y,
176     gboolean keyboard_mode,
177     GtkTooltip *tooltip,
178     gpointer user_data)
179 {
180   EmpathyIndividualViewPriv *priv;
181   FolksIndividual *individual;
182   GtkTreeModel *model;
183   GtkTreeIter iter;
184   GtkTreePath *path;
185   static gint running = 0;
186   gboolean ret = FALSE;
187
188   priv = GET_PRIV (view);
189
190   /* Avoid an infinite loop. See GNOME bug #574377 */
191   if (running > 0)
192     return FALSE;
193
194   running++;
195
196   /* Don't show the tooltip if there's already a popup menu */
197   if (gtk_menu_get_for_attach_widget (GTK_WIDGET (view)) != NULL)
198     goto OUT;
199
200   if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (view), &x, &y,
201           keyboard_mode, &model, &path, &iter))
202     goto OUT;
203
204   gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (view), tooltip, path);
205   gtk_tree_path_free (path);
206
207   gtk_tree_model_get (model, &iter,
208       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
209       -1);
210   if (individual == NULL)
211     goto OUT;
212
213   if (priv->tooltip_widget == NULL)
214     {
215       priv->tooltip_widget = empathy_individual_widget_new (individual,
216           EMPATHY_INDIVIDUAL_WIDGET_FOR_TOOLTIP |
217           EMPATHY_INDIVIDUAL_WIDGET_SHOW_LOCATION |
218           EMPATHY_INDIVIDUAL_WIDGET_SHOW_CLIENT_TYPES);
219       gtk_container_set_border_width (GTK_CONTAINER (priv->tooltip_widget), 8);
220       g_object_ref (priv->tooltip_widget);
221       g_signal_connect (priv->tooltip_widget, "destroy",
222           G_CALLBACK (individual_view_tooltip_destroy_cb), view);
223       gtk_widget_show (priv->tooltip_widget);
224     }
225   else
226     {
227       empathy_individual_widget_set_individual (
228         EMPATHY_INDIVIDUAL_WIDGET (priv->tooltip_widget), individual);
229     }
230
231   gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
232   ret = TRUE;
233
234   g_object_unref (individual);
235 OUT:
236   running--;
237
238   return ret;
239 }
240
241 static void
242 groups_change_group_cb (GObject *source,
243     GAsyncResult *result,
244     gpointer user_data)
245 {
246   FolksGroupDetails *group_details = FOLKS_GROUP_DETAILS (source);
247   GError *error = NULL;
248
249   folks_group_details_change_group_finish (group_details, result, &error);
250   if (error != NULL)
251     {
252       g_warning ("failed to change group: %s", error->message);
253       g_clear_error (&error);
254     }
255 }
256
257 static gboolean
258 group_can_be_modified (const gchar *name,
259     gboolean is_fake_group,
260     gboolean adding)
261 {
262   /* Real groups can always be modified */
263   if (!is_fake_group)
264     return TRUE;
265
266   /* The favorite fake group can be modified so users can
267    * add/remove favorites using DnD */
268   if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
269     return TRUE;
270
271   /* We can remove contacts from the 'ungrouped' fake group */
272   if (!adding && !tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_UNGROUPED))
273     return TRUE;
274
275   return FALSE;
276 }
277
278 static gboolean
279 individual_view_individual_drag_received (GtkWidget *self,
280     GdkDragContext *context,
281     GtkTreeModel *model,
282     GtkTreePath *path,
283     GtkSelectionData *selection)
284 {
285   EmpathyIndividualViewPriv *priv;
286   EmpathyIndividualManager *manager = NULL;
287   FolksIndividual *individual;
288   GtkTreePath *source_path;
289   const gchar *sel_data;
290   gchar *new_group = NULL;
291   gchar *old_group = NULL;
292   gboolean new_group_is_fake, old_group_is_fake = TRUE, retval = FALSE;
293
294   priv = GET_PRIV (self);
295
296   sel_data = (const gchar *) gtk_selection_data_get_data (selection);
297   new_group = empathy_individual_store_get_parent_group (model, path,
298       NULL, &new_group_is_fake);
299
300   if (!group_can_be_modified (new_group, new_group_is_fake, TRUE))
301     goto finished;
302
303   /* Get source group information iff the view has the FEATURE_GROUPS_CHANGE
304    * feature. Otherwise, we just add the dropped contact to whichever group
305    * they were dropped in, and don't remove them from their old group. This
306    * allows for Individual views which shouldn't allow Individuals to have
307    * their groups changed, and also for dragging Individuals between Individual
308    * views. */
309   if ((priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_CHANGE) &&
310       priv->drag_row != NULL)
311     {
312       source_path = gtk_tree_row_reference_get_path (priv->drag_row);
313       if (source_path)
314         {
315           old_group =
316               empathy_individual_store_get_parent_group (model, source_path,
317               NULL, &old_group_is_fake);
318           gtk_tree_path_free (source_path);
319         }
320
321       if (!group_can_be_modified (old_group, old_group_is_fake, FALSE))
322         goto finished;
323
324       if (!tp_strdiff (old_group, new_group))
325         goto finished;
326     }
327   else if (priv->drag_row != NULL)
328     {
329       /* We don't allow changing Individuals' groups, and this Individual was
330        * dragged from another group in *this* Individual view, so we disallow
331        * the drop. */
332       goto finished;
333     }
334
335   /* XXX: for contacts, we used to ensure the account, create the contact
336    * factory, and then wait on the contacts. But they should already be
337    * created by this point */
338
339   manager = empathy_individual_manager_dup_singleton ();
340   individual = empathy_individual_manager_lookup_member (manager, sel_data);
341
342   if (individual == NULL)
343     {
344       DEBUG ("failed to find drag event individual with ID '%s'", sel_data);
345       goto finished;
346     }
347
348   /* FIXME: We should probably wait for the cb before calling
349    * gtk_drag_finish */
350
351   /* Emit a signal notifying of the drag. We change the Individual's groups in
352    * the default signal handler. */
353   g_signal_emit (self, signals[DRAG_INDIVIDUAL_RECEIVED], 0,
354       gdk_drag_context_get_selected_action (context), individual, new_group,
355       old_group);
356
357   retval = TRUE;
358
359 finished:
360   tp_clear_object (&manager);
361   g_free (old_group);
362   g_free (new_group);
363
364   return retval;
365 }
366
367 static void
368 real_drag_individual_received_cb (EmpathyIndividualView *self,
369     GdkDragAction action,
370     FolksIndividual *individual,
371     const gchar *new_group,
372     const gchar *old_group)
373 {
374   DEBUG ("individual %s dragged from '%s' to '%s'",
375       folks_individual_get_id (individual), old_group, new_group);
376
377   if (!tp_strdiff (new_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
378     {
379       /* Mark contact as favourite */
380       folks_favourite_details_set_is_favourite (
381           FOLKS_FAVOURITE_DETAILS (individual), TRUE);
382       return;
383     }
384
385   if (!tp_strdiff (old_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
386     {
387       /* Remove contact as favourite */
388       folks_favourite_details_set_is_favourite (
389           FOLKS_FAVOURITE_DETAILS (individual), FALSE);
390
391       /* Don't try to remove it */
392       old_group = NULL;
393     }
394
395   if (new_group != NULL)
396     {
397       folks_group_details_change_group (FOLKS_GROUP_DETAILS (individual),
398           new_group, TRUE, groups_change_group_cb, NULL);
399     }
400
401   if (old_group != NULL && action == GDK_ACTION_MOVE)
402     {
403       folks_group_details_change_group (FOLKS_GROUP_DETAILS (individual),
404           old_group, FALSE, groups_change_group_cb, NULL);
405     }
406 }
407
408 static gboolean
409 individual_view_persona_drag_received (GtkWidget *self,
410     GdkDragContext *context,
411     GtkTreeModel *model,
412     GtkTreePath *path,
413     GtkSelectionData *selection)
414 {
415   EmpathyIndividualManager *manager = NULL;
416   FolksIndividual *individual = NULL;
417   FolksPersona *persona = NULL;
418   const gchar *persona_uid;
419   GList *individuals, *l;
420   gboolean retval = FALSE;
421
422   persona_uid = (const gchar *) gtk_selection_data_get_data (selection);
423
424   /* FIXME: This is slow, but the only way to find the Persona we're having
425    * dropped on us. */
426   manager = empathy_individual_manager_dup_singleton ();
427   individuals = empathy_individual_manager_get_members (manager);
428
429   for (l = individuals; l != NULL; l = l->next)
430     {
431       GList *personas, *p;
432
433       personas = folks_individual_get_personas (FOLKS_INDIVIDUAL (l->data));
434
435       for (p = personas; p != NULL; p = p->next)
436         {
437           if (!tp_strdiff (folks_persona_get_uid (FOLKS_PERSONA (p->data)),
438               persona_uid))
439             {
440               persona = g_object_ref (p->data);
441               individual = g_object_ref (l->data);
442               goto got_persona;
443             }
444         }
445     }
446
447 got_persona:
448   g_list_free (individuals);
449
450   if (persona == NULL || individual == NULL)
451     {
452       DEBUG ("Failed to find drag event persona with UID '%s'", persona_uid);
453     }
454   else
455     {
456       /* Emit a signal notifying of the drag. We change the Individual's groups in
457        * the default signal handler. */
458       g_signal_emit (self, signals[DRAG_PERSONA_RECEIVED], 0,
459           gdk_drag_context_get_selected_action (context), persona, individual,
460           &retval);
461     }
462
463   tp_clear_object (&manager);
464   tp_clear_object (&persona);
465   tp_clear_object (&individual);
466
467   return retval;
468 }
469
470 static gboolean
471 individual_view_file_drag_received (GtkWidget *view,
472     GdkDragContext *context,
473     GtkTreeModel *model,
474     GtkTreePath *path,
475     GtkSelectionData *selection)
476 {
477   GtkTreeIter iter;
478   const gchar *sel_data;
479   FolksIndividual *individual;
480   EmpathyContact *contact;
481
482   sel_data = (const gchar *) gtk_selection_data_get_data (selection);
483
484   gtk_tree_model_get_iter (model, &iter, path);
485   gtk_tree_model_get (model, &iter,
486       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
487   if (individual == NULL)
488     return FALSE;
489
490   contact = empathy_contact_dup_from_folks_individual (individual);
491   empathy_send_file_from_uri_list (contact, sel_data);
492
493   g_object_unref (individual);
494   tp_clear_object (&contact);
495
496   return TRUE;
497 }
498
499 static void
500 individual_view_drag_data_received (GtkWidget *view,
501     GdkDragContext *context,
502     gint x,
503     gint y,
504     GtkSelectionData *selection,
505     guint info,
506     guint time_)
507 {
508   GtkTreeModel *model;
509   gboolean is_row;
510   GtkTreeViewDropPosition position;
511   GtkTreePath *path;
512   gboolean success = TRUE;
513
514   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
515
516   /* Get destination group information. */
517   is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (view),
518       x, y, &path, &position);
519   if (!is_row)
520     {
521       success = FALSE;
522     }
523   else if (info == DND_DRAG_TYPE_INDIVIDUAL_ID)
524     {
525       success = individual_view_individual_drag_received (view,
526           context, model, path, selection);
527     }
528   else if (info == DND_DRAG_TYPE_PERSONA_ID)
529     {
530       success = individual_view_persona_drag_received (view, context, model,
531           path, selection);
532     }
533   else if (info == DND_DRAG_TYPE_URI_LIST || info == DND_DRAG_TYPE_STRING)
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   if (data->view != NULL)
547     {
548       gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), data->path, FALSE);
549       g_object_remove_weak_pointer (G_OBJECT (data->view),
550           (gpointer *) &data->view);
551     }
552
553   data->timeout_id = 0;
554
555   return FALSE;
556 }
557
558 /* Minimum distance between the mouse pointer and a horizontal border when we
559    start auto scrolling. */
560 #define AUTO_SCROLL_MARGIN_SIZE 20
561 /* How far to scroll per one tick. */
562 #define AUTO_SCROLL_PITCH       10
563
564 static gboolean
565 individual_view_auto_scroll_cb (EmpathyIndividualView *self)
566 {
567         EmpathyIndividualViewPriv *priv = GET_PRIV (self);
568         GtkAdjustment         *adj;
569         gdouble                new_value;
570
571         adj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self));
572
573         if (priv->distance < 0)
574                 new_value = gtk_adjustment_get_value (adj) - AUTO_SCROLL_PITCH;
575         else
576                 new_value = gtk_adjustment_get_value (adj) + AUTO_SCROLL_PITCH;
577
578         new_value = CLAMP (new_value, gtk_adjustment_get_lower (adj),
579                 gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj));
580
581         gtk_adjustment_set_value (adj, new_value);
582
583         return TRUE;
584 }
585
586 static gboolean
587 individual_view_drag_motion (GtkWidget *widget,
588     GdkDragContext *context,
589     gint x,
590     gint y,
591     guint time_)
592 {
593   EmpathyIndividualViewPriv *priv;
594   GtkTreeModel *model;
595   GdkAtom target;
596   GtkTreeIter iter;
597   static DragMotionData *dm = NULL;
598   GtkTreePath *path;
599   gboolean is_row;
600   gboolean is_different = FALSE;
601   gboolean cleanup = TRUE;
602   gboolean retval = TRUE;
603   GtkAllocation allocation;
604   guint i;
605   DndDragType drag_type = DND_DRAG_TYPE_UNKNOWN;
606
607   priv = GET_PRIV (EMPATHY_INDIVIDUAL_VIEW (widget));
608   model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
609
610
611   if (priv->auto_scroll_timeout_id != 0)
612     {
613       g_source_remove (priv->auto_scroll_timeout_id);
614       priv->auto_scroll_timeout_id = 0;
615     }
616
617   gtk_widget_get_allocation (widget, &allocation);
618
619   if (y < AUTO_SCROLL_MARGIN_SIZE ||
620       y > (allocation.height - AUTO_SCROLL_MARGIN_SIZE))
621     {
622       if (y < AUTO_SCROLL_MARGIN_SIZE)
623         priv->distance = MIN (-y, -1);
624       else
625         priv->distance = MAX (allocation.height - y, 1);
626
627       priv->auto_scroll_timeout_id = g_timeout_add (10 * ABS (priv->distance),
628           (GSourceFunc) individual_view_auto_scroll_cb, widget);
629     }
630
631   is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
632       x, y, &path, NULL, NULL, NULL);
633
634   cleanup &= (dm == NULL);
635
636   if (is_row)
637     {
638       cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
639       is_different = ((dm == NULL) || ((dm != NULL)
640               && gtk_tree_path_compare (dm->path, path) != 0));
641     }
642   else
643     cleanup &= FALSE;
644
645   if (path == NULL)
646     {
647       /* Coordinates don't point to an actual row, so make sure the pointer
648          and highlighting don't indicate that a drag is possible.
649        */
650       gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
651       gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
652       return FALSE;
653     }
654   target = gtk_drag_dest_find_target (widget, context, NULL);
655   gtk_tree_model_get_iter (model, &iter, path);
656
657   /* Determine the DndDragType of the data */
658   for (i = 0; i < G_N_ELEMENTS (drag_atoms_dest); i++)
659     {
660       if (target == drag_atoms_dest[i])
661         {
662           drag_type = drag_types_dest[i].info;
663           break;
664         }
665     }
666
667   if (drag_type == DND_DRAG_TYPE_URI_LIST ||
668       drag_type == DND_DRAG_TYPE_STRING)
669     {
670       /* This is a file drag, and it can only be dropped on contacts,
671        * not groups.
672        * If we don't have FEATURE_FILE_DROP, disallow the drop completely,
673        * even if we have a valid target. */
674       FolksIndividual *individual = NULL;
675       EmpathyCapabilities caps = EMPATHY_CAPABILITIES_NONE;
676
677       if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_FILE_DROP)
678         {
679           gtk_tree_model_get (model, &iter,
680               EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
681               -1);
682         }
683
684       if (individual != NULL)
685         {
686           EmpathyContact *contact = NULL;
687
688           contact = empathy_contact_dup_from_folks_individual (individual);
689           caps = empathy_contact_get_capabilities (contact);
690
691           tp_clear_object (&contact);
692         }
693
694       if (individual != NULL &&
695           folks_presence_details_is_online (
696               FOLKS_PRESENCE_DETAILS (individual)) &&
697           (caps & EMPATHY_CAPABILITIES_FT))
698         {
699           gdk_drag_status (context, GDK_ACTION_COPY, time_);
700           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
701               path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
702         }
703       else
704         {
705           gdk_drag_status (context, 0, time_);
706           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
707           retval = FALSE;
708         }
709
710       if (individual != NULL)
711         g_object_unref (individual);
712     }
713   else if ((drag_type == DND_DRAG_TYPE_INDIVIDUAL_ID &&
714       (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_CHANGE ||
715        priv->drag_row == NULL)) ||
716       (drag_type == DND_DRAG_TYPE_PERSONA_ID &&
717        priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_PERSONA_DROP))
718     {
719       /* If target != GDK_NONE, then we have a contact (individual or persona)
720          drag.  If we're pointing to a group, highlight it.  Otherwise, if the
721          contact we're pointing to is in a group, highlight that.  Otherwise,
722          set the drag position to before the first row for a drag into
723          the "non-group" at the top.
724          If it's an Individual:
725            We only highlight things if the contact is from a different
726            Individual view, or if this Individual view has
727            FEATURE_GROUPS_CHANGE. This prevents highlighting in Individual views
728            which don't have FEATURE_GROUPS_CHANGE, but do have
729            FEATURE_INDIVIDUAL_DRAG and FEATURE_INDIVIDUAL_DROP.
730          If it's a Persona:
731            We only highlight things if we have FEATURE_PERSONA_DROP.
732        */
733       GtkTreeIter group_iter;
734       gboolean is_group;
735       GtkTreePath *group_path;
736       gtk_tree_model_get (model, &iter,
737           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
738       if (is_group)
739         {
740           group_iter = iter;
741         }
742       else
743         {
744           if (gtk_tree_model_iter_parent (model, &group_iter, &iter))
745             gtk_tree_model_get (model, &group_iter,
746                 EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
747         }
748       if (is_group)
749         {
750           gdk_drag_status (context, GDK_ACTION_MOVE, time_);
751           group_path = gtk_tree_model_get_path (model, &group_iter);
752           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
753               group_path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
754           gtk_tree_path_free (group_path);
755         }
756       else
757         {
758           group_path = gtk_tree_path_new_first ();
759           gdk_drag_status (context, GDK_ACTION_MOVE, time_);
760           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
761               group_path, GTK_TREE_VIEW_DROP_BEFORE);
762         }
763     }
764
765   if (!is_different && !cleanup)
766     return retval;
767
768   if (dm)
769     {
770       gtk_tree_path_free (dm->path);
771       if (dm->timeout_id)
772         {
773           g_source_remove (dm->timeout_id);
774         }
775
776       g_free (dm);
777
778       dm = NULL;
779     }
780
781   if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path))
782     {
783       dm = g_new0 (DragMotionData, 1);
784
785       dm->view = EMPATHY_INDIVIDUAL_VIEW (widget);
786       g_object_add_weak_pointer (G_OBJECT (widget), (gpointer *) &dm->view);
787       dm->path = gtk_tree_path_copy (path);
788
789       dm->timeout_id = g_timeout_add_seconds (1,
790           (GSourceFunc) individual_view_drag_motion_cb, dm);
791     }
792
793   return retval;
794 }
795
796 static void
797 individual_view_drag_begin (GtkWidget *widget,
798     GdkDragContext *context)
799 {
800   EmpathyIndividualViewPriv *priv;
801   GtkTreeSelection *selection;
802   GtkTreeModel *model;
803   GtkTreePath *path;
804   GtkTreeIter iter;
805
806   priv = GET_PRIV (widget);
807
808   GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_begin (widget,
809       context);
810
811   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
812   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
813     return;
814
815   path = gtk_tree_model_get_path (model, &iter);
816   priv->drag_row = gtk_tree_row_reference_new (model, path);
817   gtk_tree_path_free (path);
818 }
819
820 static void
821 individual_view_drag_data_get (GtkWidget *widget,
822     GdkDragContext *context,
823     GtkSelectionData *selection,
824     guint info,
825     guint time_)
826 {
827   EmpathyIndividualViewPriv *priv;
828   GtkTreePath *src_path;
829   GtkTreeIter iter;
830   GtkTreeModel *model;
831   FolksIndividual *individual;
832   const gchar *individual_id;
833
834   priv = GET_PRIV (widget);
835
836   model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
837   if (priv->drag_row == NULL)
838     return;
839
840   src_path = gtk_tree_row_reference_get_path (priv->drag_row);
841   if (src_path == NULL)
842     return;
843
844   if (!gtk_tree_model_get_iter (model, &iter, src_path))
845     {
846       gtk_tree_path_free (src_path);
847       return;
848     }
849
850   gtk_tree_path_free (src_path);
851
852   individual =
853       empathy_individual_view_dup_selected (EMPATHY_INDIVIDUAL_VIEW (widget));
854   if (individual == NULL)
855     return;
856
857   individual_id = folks_individual_get_id (individual);
858
859   if (info == DND_DRAG_TYPE_INDIVIDUAL_ID)
860     {
861       gtk_selection_data_set (selection,
862           gdk_atom_intern ("text/individual-id", FALSE), 8,
863           (guchar *) individual_id, strlen (individual_id) + 1);
864     }
865
866   g_object_unref (individual);
867 }
868
869 static void
870 individual_view_drag_end (GtkWidget *widget,
871     GdkDragContext *context)
872 {
873   EmpathyIndividualViewPriv *priv;
874
875   priv = GET_PRIV (widget);
876
877   GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_end (widget,
878       context);
879
880   if (priv->drag_row)
881     {
882       gtk_tree_row_reference_free (priv->drag_row);
883       priv->drag_row = NULL;
884     }
885 }
886
887 static gboolean
888 individual_view_drag_drop (GtkWidget *widget,
889     GdkDragContext *drag_context,
890     gint x,
891     gint y,
892     guint time_)
893 {
894   return FALSE;
895 }
896
897 typedef struct
898 {
899   EmpathyIndividualView *view;
900   guint button;
901   guint32 time;
902 } MenuPopupData;
903
904 static void
905 menu_deactivate_cb (GtkMenuShell *menushell,
906     gpointer user_data)
907 {
908   /* FIXME: we shouldn't have to disconnec the signal (bgo #641327) */
909   g_signal_handlers_disconnect_by_func (menushell,
910       menu_deactivate_cb, user_data);
911
912   gtk_menu_detach (GTK_MENU (menushell));
913 }
914
915 static gboolean
916 individual_view_popup_menu_idle_cb (gpointer user_data)
917 {
918   MenuPopupData *data = user_data;
919   GtkWidget *menu;
920
921   menu = empathy_individual_view_get_individual_menu (data->view);
922   if (menu == NULL)
923     menu = empathy_individual_view_get_group_menu (data->view);
924
925   if (menu != NULL)
926     {
927       gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (data->view),
928           NULL);
929       gtk_widget_show (menu);
930       gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, data->button,
931           data->time);
932
933       /* menu is initially unowned but gtk_menu_attach_to_widget() taked its
934        * floating ref. We can either wait that the treeview releases its ref
935        * when it will be destroyed (when leaving Empathy) or explicitely
936        * detach the menu when it's not displayed any more.
937        * We go for the latter as we don't want to keep useless menus in memory
938        * during the whole lifetime of Empathy. */
939       g_signal_connect (menu, "deactivate", G_CALLBACK (menu_deactivate_cb),
940           NULL);
941     }
942
943   g_slice_free (MenuPopupData, data);
944
945   return FALSE;
946 }
947
948 static gboolean
949 individual_view_button_press_event_cb (EmpathyIndividualView *view,
950     GdkEventButton *event,
951     gpointer user_data)
952 {
953   if (event->button == 3)
954     {
955       MenuPopupData *data;
956
957       data = g_slice_new (MenuPopupData);
958       data->view = view;
959       data->button = event->button;
960       data->time = event->time;
961       g_idle_add (individual_view_popup_menu_idle_cb, data);
962     }
963
964   return FALSE;
965 }
966
967 static gboolean
968 individual_view_key_press_event_cb (EmpathyIndividualView *view,
969     GdkEventKey *event,
970     gpointer user_data)
971 {
972   if (event->keyval == GDK_KEY_Menu)
973     {
974       MenuPopupData *data;
975
976       data = g_slice_new (MenuPopupData);
977       data->view = view;
978       data->button = 0;
979       data->time = event->time;
980       g_idle_add (individual_view_popup_menu_idle_cb, data);
981     } else if (event->keyval == GDK_KEY_F2) {
982         FolksIndividual *individual;
983         EmpathyContact *contact;
984
985         g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), FALSE);
986
987         individual = empathy_individual_view_dup_selected (view);
988         if (individual == NULL)
989             return FALSE;
990
991         contact = empathy_contact_dup_from_folks_individual (individual);
992         if (contact == NULL) {
993             g_object_unref (individual);
994             return FALSE;
995         }
996         empathy_contact_edit_dialog_show (contact, NULL);
997
998         g_object_unref (individual);
999         g_object_unref (contact);
1000     }
1001
1002   return FALSE;
1003 }
1004
1005 static void
1006 individual_view_row_activated (GtkTreeView *view,
1007     GtkTreePath *path,
1008     GtkTreeViewColumn *column)
1009 {
1010   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1011   FolksIndividual *individual;
1012   EmpathyContact *contact;
1013   GtkTreeModel *model;
1014   GtkTreeIter iter;
1015
1016   if (!(priv->individual_features & EMPATHY_INDIVIDUAL_FEATURE_CHAT))
1017     return;
1018
1019   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1020   gtk_tree_model_get_iter (model, &iter, path);
1021   gtk_tree_model_get (model, &iter,
1022       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
1023
1024   if (individual == NULL)
1025     return;
1026
1027   /* Determine which Persona to chat to, by choosing the most available one. */
1028   contact = empathy_contact_dup_best_for_action (individual,
1029       EMPATHY_ACTION_CHAT);
1030
1031   if (contact != NULL)
1032     {
1033       DEBUG ("Starting a chat");
1034
1035       empathy_chat_with_contact (contact,
1036           gtk_get_current_event_time ());
1037     }
1038
1039   g_object_unref (individual);
1040   tp_clear_object (&contact);
1041 }
1042
1043 static void
1044 individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
1045     const gchar *path_string,
1046     EmpathyIndividualView *view)
1047 {
1048   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1049   GtkWidget *menu;
1050   GtkTreeModel *model;
1051   GtkTreeIter iter;
1052   FolksIndividual *individual;
1053   GdkEventButton *event;
1054   GtkMenuShell *shell;
1055   GtkWidget *item;
1056
1057   if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_CALL))
1058     return;
1059
1060   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1061   if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
1062     return;
1063
1064   gtk_tree_model_get (model, &iter,
1065       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
1066   if (individual == NULL)
1067     return;
1068
1069   event = (GdkEventButton *) gtk_get_current_event ();
1070
1071   menu = empathy_context_menu_new (GTK_WIDGET (view));
1072   shell = GTK_MENU_SHELL (menu);
1073
1074   /* audio */
1075   item = empathy_individual_audio_call_menu_item_new (individual, NULL);
1076   gtk_menu_shell_append (shell, item);
1077   gtk_widget_show (item);
1078
1079   /* video */
1080   item = empathy_individual_video_call_menu_item_new (individual, NULL);
1081   gtk_menu_shell_append (shell, item);
1082   gtk_widget_show (item);
1083
1084   gtk_widget_show (menu);
1085   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1086       event->button, event->time);
1087
1088   g_object_unref (individual);
1089 }
1090
1091 static void
1092 individual_view_cell_set_background (EmpathyIndividualView *view,
1093     GtkCellRenderer *cell,
1094     gboolean is_group,
1095     gboolean is_active)
1096 {
1097   if (!is_group && is_active)
1098     {
1099       GtkStyleContext *style;
1100       GdkRGBA color;
1101
1102       style = gtk_widget_get_style_context (GTK_WIDGET (view));
1103
1104       gtk_style_context_get_background_color (style, GTK_STATE_FLAG_SELECTED,
1105           &color);
1106
1107       /* Here we take the current theme colour and add it to
1108        * the colour for white and average the two. This
1109        * gives a colour which is inline with the theme but
1110        * slightly whiter.
1111        */
1112       empathy_make_color_whiter (&color);
1113
1114       g_object_set (cell, "cell-background-rgba", &color, NULL);
1115     }
1116   else
1117     g_object_set (cell, "cell-background-rgba", NULL, NULL);
1118 }
1119
1120 static void
1121 individual_view_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
1122     GtkCellRenderer *cell,
1123     GtkTreeModel *model,
1124     GtkTreeIter *iter,
1125     EmpathyIndividualView *view)
1126 {
1127   GdkPixbuf *pixbuf;
1128   gboolean is_group;
1129   gboolean is_active;
1130
1131   gtk_tree_model_get (model, iter,
1132       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1133       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
1134       EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, &pixbuf, -1);
1135
1136   g_object_set (cell,
1137       "visible", !is_group,
1138       "pixbuf", pixbuf,
1139       NULL);
1140
1141   tp_clear_object (&pixbuf);
1142
1143   individual_view_cell_set_background (view, cell, is_group, is_active);
1144 }
1145
1146 static void
1147 individual_view_group_icon_cell_data_func (GtkTreeViewColumn *tree_column,
1148     GtkCellRenderer *cell,
1149     GtkTreeModel *model,
1150     GtkTreeIter *iter,
1151     EmpathyIndividualView *view)
1152 {
1153   GdkPixbuf *pixbuf = NULL;
1154   gboolean is_group;
1155   gchar *name;
1156
1157   gtk_tree_model_get (model, iter,
1158       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1159       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
1160
1161   if (!is_group)
1162     goto out;
1163
1164   if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
1165     {
1166       pixbuf = empathy_pixbuf_from_icon_name ("emblem-favorite",
1167           GTK_ICON_SIZE_MENU);
1168     }
1169   else if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_PEOPLE_NEARBY))
1170     {
1171       pixbuf = empathy_pixbuf_from_icon_name ("im-local-xmpp",
1172           GTK_ICON_SIZE_MENU);
1173     }
1174
1175 out:
1176   g_object_set (cell,
1177       "visible", pixbuf != NULL,
1178       "pixbuf", pixbuf,
1179       NULL);
1180
1181   tp_clear_object (&pixbuf);
1182
1183   g_free (name);
1184 }
1185
1186 static void
1187 individual_view_audio_call_cell_data_func (GtkTreeViewColumn *tree_column,
1188     GtkCellRenderer *cell,
1189     GtkTreeModel *model,
1190     GtkTreeIter *iter,
1191     EmpathyIndividualView *view)
1192 {
1193   gboolean is_group;
1194   gboolean is_active;
1195   gboolean can_audio, can_video;
1196
1197   gtk_tree_model_get (model, iter,
1198       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1199       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
1200       EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL, &can_audio,
1201       EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL, &can_video, -1);
1202
1203   g_object_set (cell,
1204       "visible", !is_group && (can_audio || can_video),
1205       "icon-name", can_video ? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
1206       NULL);
1207
1208   individual_view_cell_set_background (view, cell, is_group, is_active);
1209 }
1210
1211 static void
1212 individual_view_avatar_cell_data_func (GtkTreeViewColumn *tree_column,
1213     GtkCellRenderer *cell,
1214     GtkTreeModel *model,
1215     GtkTreeIter *iter,
1216     EmpathyIndividualView *view)
1217 {
1218   GdkPixbuf *pixbuf;
1219   gboolean show_avatar;
1220   gboolean is_group;
1221   gboolean is_active;
1222
1223   gtk_tree_model_get (model, iter,
1224       EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR, &pixbuf,
1225       EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
1226       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1227       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1228
1229   g_object_set (cell,
1230       "visible", !is_group && show_avatar,
1231       "pixbuf", pixbuf,
1232       NULL);
1233
1234   tp_clear_object (&pixbuf);
1235
1236   individual_view_cell_set_background (view, cell, is_group, is_active);
1237 }
1238
1239 static void
1240 individual_view_text_cell_data_func (GtkTreeViewColumn *tree_column,
1241     GtkCellRenderer *cell,
1242     GtkTreeModel *model,
1243     GtkTreeIter *iter,
1244     EmpathyIndividualView *view)
1245 {
1246   gboolean is_group;
1247   gboolean is_active;
1248
1249   gtk_tree_model_get (model, iter,
1250       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1251       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1252
1253   individual_view_cell_set_background (view, cell, is_group, is_active);
1254 }
1255
1256 static void
1257 individual_view_expander_cell_data_func (GtkTreeViewColumn *column,
1258     GtkCellRenderer *cell,
1259     GtkTreeModel *model,
1260     GtkTreeIter *iter,
1261     EmpathyIndividualView *view)
1262 {
1263   gboolean is_group;
1264   gboolean is_active;
1265
1266   gtk_tree_model_get (model, iter,
1267       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1268       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1269
1270   if (gtk_tree_model_iter_has_child (model, iter))
1271     {
1272       GtkTreePath *path;
1273       gboolean row_expanded;
1274
1275       path = gtk_tree_model_get_path (model, iter);
1276       row_expanded =
1277           gtk_tree_view_row_expanded (GTK_TREE_VIEW
1278           (gtk_tree_view_column_get_tree_view (column)), path);
1279       gtk_tree_path_free (path);
1280
1281       g_object_set (cell,
1282           "visible", TRUE,
1283           "expander-style",
1284           row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
1285           NULL);
1286     }
1287   else
1288     g_object_set (cell, "visible", FALSE, NULL);
1289
1290   individual_view_cell_set_background (view, cell, is_group, is_active);
1291 }
1292
1293 static void
1294 individual_view_row_expand_or_collapse_cb (EmpathyIndividualView *view,
1295     GtkTreeIter *iter,
1296     GtkTreePath *path,
1297     gpointer user_data)
1298 {
1299   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1300   GtkTreeModel *model;
1301   gchar *name;
1302   gboolean expanded;
1303
1304   if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE))
1305     return;
1306
1307   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1308
1309   gtk_tree_model_get (model, iter,
1310       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
1311
1312   expanded = GPOINTER_TO_INT (user_data);
1313   empathy_contact_group_set_expanded (name, expanded);
1314
1315   g_free (name);
1316 }
1317
1318 static gboolean
1319 individual_view_start_search_cb (EmpathyIndividualView *view,
1320     gpointer data)
1321 {
1322   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1323
1324   if (priv->search_widget == NULL)
1325     return FALSE;
1326
1327   empathy_individual_view_start_search (view);
1328
1329   return TRUE;
1330 }
1331
1332 static void
1333 individual_view_search_text_notify_cb (EmpathyLiveSearch *search,
1334     GParamSpec *pspec,
1335     EmpathyIndividualView *view)
1336 {
1337   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1338   GtkTreePath *path;
1339   GtkTreeViewColumn *focus_column;
1340   GtkTreeModel *model;
1341   GtkTreeIter iter;
1342   gboolean set_cursor = FALSE;
1343
1344   gtk_tree_model_filter_refilter (priv->filter);
1345
1346   /* Set cursor on the first contact. If it is already set on a group,
1347    * set it on its first child contact. Note that first child of a group
1348    * is its separator, that's why we actually set to the 2nd
1349    */
1350
1351   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1352   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1353
1354   if (path == NULL)
1355     {
1356       path = gtk_tree_path_new_from_string ("0:1");
1357       set_cursor = TRUE;
1358     }
1359   else if (gtk_tree_path_get_depth (path) < 2)
1360     {
1361       gboolean is_group;
1362
1363       gtk_tree_model_get_iter (model, &iter, path);
1364       gtk_tree_model_get (model, &iter,
1365           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1366           -1);
1367
1368       if (is_group)
1369         {
1370           gtk_tree_path_down (path);
1371           gtk_tree_path_next (path);
1372           set_cursor = TRUE;
1373         }
1374     }
1375
1376   if (set_cursor)
1377     {
1378       /* FIXME: Workaround for GTK bug #621651, we have to make sure the path is
1379        * valid. */
1380       if (gtk_tree_model_get_iter (model, &iter, path))
1381         {
1382           gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path, focus_column,
1383               FALSE);
1384         }
1385     }
1386
1387   gtk_tree_path_free (path);
1388 }
1389
1390 static void
1391 individual_view_search_activate_cb (GtkWidget *search,
1392   EmpathyIndividualView *view)
1393 {
1394   GtkTreePath *path;
1395   GtkTreeViewColumn *focus_column;
1396
1397   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1398   if (path != NULL)
1399     {
1400       gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path, focus_column);
1401       gtk_tree_path_free (path);
1402
1403       gtk_widget_hide (search);
1404     }
1405 }
1406
1407 static gboolean
1408 individual_view_search_key_navigation_cb (GtkWidget *search,
1409   GdkEvent *event,
1410   EmpathyIndividualView *view)
1411 {
1412   GdkEventKey *eventkey = ((GdkEventKey *) event);
1413   gboolean ret = FALSE;
1414
1415   if (eventkey->keyval == GDK_KEY_Up || eventkey->keyval == GDK_KEY_Down
1416       || eventkey->keyval == GDK_KEY_F2)
1417     {
1418       GdkEvent *new_event;
1419
1420       new_event = gdk_event_copy (event);
1421       gtk_widget_grab_focus (GTK_WIDGET (view));
1422       ret = gtk_widget_event (GTK_WIDGET (view), new_event);
1423       gtk_widget_grab_focus (search);
1424
1425       gdk_event_free (new_event);
1426     }
1427
1428   return ret;
1429 }
1430
1431 static void
1432 individual_view_search_hide_cb (EmpathyLiveSearch *search,
1433     EmpathyIndividualView *view)
1434 {
1435   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1436   GtkTreeModel *model;
1437   GtkTreePath *cursor_path;
1438   GtkTreeIter iter;
1439   gboolean valid = FALSE;
1440
1441   /* block expand or collapse handlers, they would write the
1442    * expand or collapsed setting to file otherwise */
1443   g_signal_handlers_block_by_func (view,
1444       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1445   g_signal_handlers_block_by_func (view,
1446     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1447
1448   /* restore which groups are expanded and which are not */
1449   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1450   for (valid = gtk_tree_model_get_iter_first (model, &iter);
1451        valid; valid = gtk_tree_model_iter_next (model, &iter))
1452     {
1453       gboolean is_group;
1454       gchar *name = NULL;
1455       GtkTreePath *path;
1456
1457       gtk_tree_model_get (model, &iter,
1458           EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1459           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1460           -1);
1461
1462       if (!is_group)
1463         {
1464           g_free (name);
1465           continue;
1466         }
1467
1468       path = gtk_tree_model_get_path (model, &iter);
1469       if ((priv->view_features &
1470             EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1471           empathy_contact_group_get_expanded (name))
1472         {
1473           gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
1474         }
1475       else
1476         {
1477           gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1478         }
1479
1480       gtk_tree_path_free (path);
1481       g_free (name);
1482     }
1483
1484   /* unblock expand or collapse handlers */
1485   g_signal_handlers_unblock_by_func (view,
1486       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1487   g_signal_handlers_unblock_by_func (view,
1488       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1489
1490   /* keep the selected contact visible */
1491   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &cursor_path, NULL);
1492
1493   if (cursor_path != NULL)
1494     gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view), cursor_path, NULL,
1495         FALSE, 0, 0);
1496
1497   gtk_tree_path_free (cursor_path);
1498 }
1499
1500 static void
1501 individual_view_search_show_cb (EmpathyLiveSearch *search,
1502     EmpathyIndividualView *view)
1503 {
1504   /* block expand or collapse handlers during expand all, they would
1505    * write the expand or collapsed setting to file otherwise */
1506   g_signal_handlers_block_by_func (view,
1507       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1508
1509   gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
1510
1511   g_signal_handlers_unblock_by_func (view,
1512       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1513 }
1514
1515 static gboolean
1516 expand_idle_foreach_cb (GtkTreeModel *model,
1517     GtkTreePath *path,
1518     GtkTreeIter *iter,
1519     EmpathyIndividualView *self)
1520 {
1521   EmpathyIndividualViewPriv *priv;
1522   gboolean is_group;
1523   gpointer should_expand;
1524   gchar *name;
1525
1526   /* We only want groups */
1527   if (gtk_tree_path_get_depth (path) > 1)
1528     return FALSE;
1529
1530   gtk_tree_model_get (model, iter,
1531       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1532       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1533       -1);
1534
1535   if (is_group == FALSE)
1536     {
1537       g_free (name);
1538       return FALSE;
1539     }
1540
1541   priv = GET_PRIV (self);
1542
1543   if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1544       &should_expand) == TRUE)
1545     {
1546       if (GPOINTER_TO_INT (should_expand) == TRUE)
1547         gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
1548       else
1549         gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
1550
1551       g_hash_table_remove (priv->expand_groups, name);
1552     }
1553
1554   g_free (name);
1555
1556   return FALSE;
1557 }
1558
1559 static gboolean
1560 individual_view_expand_idle_cb (EmpathyIndividualView *self)
1561 {
1562   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1563
1564   DEBUG ("individual_view_expand_idle_cb");
1565
1566   g_signal_handlers_block_by_func (self,
1567     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1568   g_signal_handlers_block_by_func (self,
1569     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1570
1571   /* The store/filter could've been removed while we were in the idle queue */
1572   if (priv->filter != NULL)
1573     {
1574       gtk_tree_model_foreach (GTK_TREE_MODEL (priv->filter),
1575           (GtkTreeModelForeachFunc) expand_idle_foreach_cb, self);
1576     }
1577
1578   g_signal_handlers_unblock_by_func (self,
1579       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1580   g_signal_handlers_unblock_by_func (self,
1581       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1582
1583   /* Empty the table of groups to expand/contract, since it may contain groups
1584    * which no longer exist in the tree view. This can happen after going
1585    * offline, for example. */
1586   g_hash_table_remove_all (priv->expand_groups);
1587   priv->expand_groups_idle_handler = 0;
1588   g_object_unref (self);
1589
1590   return FALSE;
1591 }
1592
1593 static void
1594 individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
1595     GtkTreePath *path,
1596     GtkTreeIter *iter,
1597     EmpathyIndividualView *view)
1598 {
1599   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1600   gboolean should_expand, is_group = FALSE;
1601   gchar *name = NULL;
1602   gpointer will_expand;
1603
1604   gtk_tree_model_get (model, iter,
1605       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1606       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1607       -1);
1608
1609   if (!is_group || EMP_STR_EMPTY (name))
1610     {
1611       g_free (name);
1612       return;
1613     }
1614
1615   should_expand = (priv->view_features &
1616           EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1617       (priv->search_widget != NULL &&
1618           gtk_widget_get_visible (priv->search_widget)) ||
1619       empathy_contact_group_get_expanded (name);
1620
1621   /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1622    * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
1623    * a hash table, and expand or contract them as appropriate all at once in
1624    * an idle handler which iterates over all the group rows. */
1625   if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1626       &will_expand) == FALSE ||
1627       GPOINTER_TO_INT (will_expand) != should_expand)
1628     {
1629       g_hash_table_insert (priv->expand_groups, g_strdup (name),
1630           GINT_TO_POINTER (should_expand));
1631
1632       if (priv->expand_groups_idle_handler == 0)
1633         {
1634           priv->expand_groups_idle_handler =
1635               g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
1636                   g_object_ref (view));
1637         }
1638     }
1639
1640   g_free (name);
1641 }
1642
1643 /* FIXME: This is a workaround for bgo#621076 */
1644 static void
1645 individual_view_verify_group_visibility (EmpathyIndividualView *view,
1646     GtkTreePath *path)
1647 {
1648   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1649   GtkTreeModel *model;
1650   GtkTreePath *parent_path;
1651   GtkTreeIter parent_iter;
1652
1653   if (gtk_tree_path_get_depth (path) < 2)
1654     return;
1655
1656   /* A group row is visible if and only if at least one if its child is visible.
1657    * So when a row is inserted/deleted/changed in the base model, that could
1658    * modify the visibility of its parent in the filter model.
1659   */
1660
1661   model = GTK_TREE_MODEL (priv->store);
1662   parent_path = gtk_tree_path_copy (path);
1663   gtk_tree_path_up (parent_path);
1664   if (gtk_tree_model_get_iter (model, &parent_iter, parent_path))
1665     {
1666       /* This tells the filter to verify the visibility of that row, and
1667        * show/hide it if necessary */
1668       gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
1669               parent_path, &parent_iter);
1670     }
1671   gtk_tree_path_free (parent_path);
1672 }
1673
1674 static void
1675 individual_view_store_row_changed_cb (GtkTreeModel *model,
1676   GtkTreePath *path,
1677   GtkTreeIter *iter,
1678   EmpathyIndividualView *view)
1679 {
1680   individual_view_verify_group_visibility (view, path);
1681 }
1682
1683 static void
1684 individual_view_store_row_deleted_cb (GtkTreeModel *model,
1685   GtkTreePath *path,
1686   EmpathyIndividualView *view)
1687 {
1688   individual_view_verify_group_visibility (view, path);
1689 }
1690
1691 static gboolean
1692 individual_view_is_visible_individual (EmpathyIndividualView *self,
1693     FolksIndividual *individual,
1694     gboolean is_online,
1695     gboolean is_searching,
1696     const gchar *group,
1697     gboolean is_fake_group)
1698 {
1699   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1700   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
1701   const gchar *str;
1702   GList *personas, *l;
1703   gboolean is_favorite, contains_interesting_persona = FALSE;
1704
1705   /* We're only giving the visibility wrt filtering here, not things like
1706    * presence. */
1707   if (priv->show_untrusted == FALSE &&
1708       folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
1709     {
1710       return FALSE;
1711     }
1712
1713   /* Hide all individuals which consist entirely of uninteresting personas */
1714   personas = folks_individual_get_personas (individual);
1715   for (l = personas; l; l = l->next)
1716     {
1717       if (empathy_folks_persona_is_interesting (FOLKS_PERSONA (l->data)))
1718         {
1719           contains_interesting_persona = TRUE;
1720           break;
1721         }
1722     }
1723
1724   if (contains_interesting_persona == FALSE)
1725     return FALSE;
1726
1727   is_favorite = folks_favourite_details_get_is_favourite (
1728       FOLKS_FAVOURITE_DETAILS (individual));
1729   if (is_searching == FALSE) {
1730     if (is_favorite && is_fake_group &&
1731         !tp_strdiff (group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
1732         /* Always display favorite contacts in the favorite group */
1733         return TRUE;
1734
1735     return (priv->show_offline || is_online);
1736   }
1737
1738   /* check alias name */
1739   str = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual));
1740
1741   if (empathy_live_search_match (live, str))
1742     return TRUE;
1743
1744   /* check contact id, remove the @server.com part */
1745   for (l = personas; l; l = l->next)
1746     {
1747       const gchar *p;
1748       gchar *dup_str = NULL;
1749       gboolean visible;
1750
1751       if (!empathy_folks_persona_is_interesting (FOLKS_PERSONA (l->data)))
1752         continue;
1753
1754       str = folks_persona_get_display_id (l->data);
1755       p = strstr (str, "@");
1756       if (p != NULL)
1757         str = dup_str = g_strndup (str, p - str);
1758
1759       visible = empathy_live_search_match (live, str);
1760       g_free (dup_str);
1761       if (visible)
1762         return TRUE;
1763     }
1764
1765   /* FIXME: Add more rules here, we could check phone numbers in
1766    * contact's vCard for example. */
1767
1768   return FALSE;
1769 }
1770
1771 static gchar *
1772 get_group (GtkTreeModel *model,
1773     GtkTreeIter *iter,
1774     gboolean *is_fake)
1775 {
1776   GtkTreeIter parent_iter;
1777   gchar *name = NULL;
1778
1779   *is_fake = FALSE;
1780
1781   if (!gtk_tree_model_iter_parent (model, &parent_iter, iter))
1782     return NULL;
1783
1784   gtk_tree_model_get (model, &parent_iter,
1785       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1786       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake,
1787       -1);
1788
1789   return name;
1790 }
1791
1792
1793 static gboolean
1794 individual_view_filter_visible_func (GtkTreeModel *model,
1795     GtkTreeIter *iter,
1796     gpointer user_data)
1797 {
1798   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1799   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1800   FolksIndividual *individual = NULL;
1801   gboolean is_group, is_separator, valid;
1802   GtkTreeIter child_iter;
1803   gboolean visible, is_online;
1804   gboolean is_searching = TRUE;
1805
1806   if (priv->custom_filter != NULL)
1807     return priv->custom_filter (model, iter, priv->custom_filter_data);
1808
1809   if (priv->search_widget == NULL ||
1810       !gtk_widget_get_visible (priv->search_widget))
1811      is_searching = FALSE;
1812
1813   gtk_tree_model_get (model, iter,
1814       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1815       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1816       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1817       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1818       -1);
1819
1820   if (individual != NULL)
1821     {
1822       gchar *group;
1823       gboolean is_fake_group;
1824
1825       group = get_group (model, iter, &is_fake_group);
1826
1827       visible = individual_view_is_visible_individual (self, individual,
1828           is_online, is_searching, group, is_fake_group);
1829
1830       g_object_unref (individual);
1831       g_free (group);
1832
1833       /* FIXME: Work around bgo#626552/bgo#621076 */
1834       if (visible == TRUE)
1835         {
1836           GtkTreePath *path = gtk_tree_model_get_path (model, iter);
1837           individual_view_verify_group_visibility (self, path);
1838           gtk_tree_path_free (path);
1839         }
1840
1841       return visible;
1842     }
1843
1844   if (is_separator)
1845     return TRUE;
1846
1847   /* Not a contact, not a separator, must be a group */
1848   g_return_val_if_fail (is_group, FALSE);
1849
1850   /* only show groups which are not empty */
1851   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1852        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1853     {
1854       gchar *group;
1855       gboolean is_fake_group;
1856
1857       gtk_tree_model_get (model, &child_iter,
1858         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1859         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1860         -1);
1861
1862       if (individual == NULL)
1863         continue;
1864
1865       group = get_group (model, &child_iter, &is_fake_group);
1866
1867       visible = individual_view_is_visible_individual (self, individual,
1868           is_online, is_searching, group, is_fake_group);
1869
1870       g_object_unref (individual);
1871       g_free (group);
1872
1873       /* show group if it has at least one visible contact in it */
1874       if (visible == TRUE)
1875         return TRUE;
1876     }
1877
1878   return FALSE;
1879 }
1880
1881 static void
1882 individual_view_constructed (GObject *object)
1883 {
1884   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1885   GtkCellRenderer *cell;
1886   GtkTreeViewColumn *col;
1887   guint i;
1888
1889   /* Setup view */
1890   g_object_set (view,
1891       "headers-visible", FALSE,
1892       "show-expanders", FALSE,
1893       NULL);
1894
1895   col = gtk_tree_view_column_new ();
1896
1897   /* State */
1898   cell = gtk_cell_renderer_pixbuf_new ();
1899   gtk_tree_view_column_pack_start (col, cell, FALSE);
1900   gtk_tree_view_column_set_cell_data_func (col, cell,
1901       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1902       view, NULL);
1903
1904   g_object_set (cell,
1905       "xpad", 5,
1906       "ypad", 1,
1907       "visible", FALSE,
1908       NULL);
1909
1910   /* Group icon */
1911   cell = gtk_cell_renderer_pixbuf_new ();
1912   gtk_tree_view_column_pack_start (col, cell, FALSE);
1913   gtk_tree_view_column_set_cell_data_func (col, cell,
1914       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1915       view, NULL);
1916
1917   g_object_set (cell,
1918       "xpad", 0,
1919       "ypad", 0,
1920       "visible", FALSE,
1921       "width", 16,
1922       "height", 16,
1923       NULL);
1924
1925   /* Name */
1926   cell = empathy_cell_renderer_text_new ();
1927   gtk_tree_view_column_pack_start (col, cell, TRUE);
1928   gtk_tree_view_column_set_cell_data_func (col, cell,
1929       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1930
1931   gtk_tree_view_column_add_attribute (col, cell,
1932       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1933   gtk_tree_view_column_add_attribute (col, cell,
1934       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1935   gtk_tree_view_column_add_attribute (col, cell,
1936       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1937   gtk_tree_view_column_add_attribute (col, cell,
1938       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1939   gtk_tree_view_column_add_attribute (col, cell,
1940       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1941   gtk_tree_view_column_add_attribute (col, cell,
1942       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1943   gtk_tree_view_column_add_attribute (col, cell,
1944       "client-types", EMPATHY_INDIVIDUAL_STORE_COL_CLIENT_TYPES);
1945
1946   /* Audio Call Icon */
1947   cell = empathy_cell_renderer_activatable_new ();
1948   gtk_tree_view_column_pack_start (col, cell, FALSE);
1949   gtk_tree_view_column_set_cell_data_func (col, cell,
1950       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1951       view, NULL);
1952
1953   g_object_set (cell, "visible", FALSE, NULL);
1954
1955   g_signal_connect (cell, "path-activated",
1956       G_CALLBACK (individual_view_call_activated_cb), view);
1957
1958   /* Avatar */
1959   cell = gtk_cell_renderer_pixbuf_new ();
1960   gtk_tree_view_column_pack_start (col, cell, FALSE);
1961   gtk_tree_view_column_set_cell_data_func (col, cell,
1962       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1963       view, NULL);
1964
1965   g_object_set (cell,
1966       "xpad", 0,
1967       "ypad", 0,
1968       "visible", FALSE,
1969       "width", 32,
1970       "height", 32,
1971       NULL);
1972
1973   /* Expander */
1974   cell = empathy_cell_renderer_expander_new ();
1975   gtk_tree_view_column_pack_end (col, cell, FALSE);
1976   gtk_tree_view_column_set_cell_data_func (col, cell,
1977       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1978       view, NULL);
1979
1980   /* Actually add the column now we have added all cell renderers */
1981   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1982
1983   /* Drag & Drop. */
1984   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1985     {
1986       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1987     }
1988 }
1989
1990 static void
1991 individual_view_set_view_features (EmpathyIndividualView *view,
1992     EmpathyIndividualFeatureFlags features)
1993 {
1994   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1995   gboolean has_tooltip;
1996
1997   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1998
1999   priv->view_features = features;
2000
2001   /* Setting reorderable is a hack that gets us row previews as drag icons
2002      for free.  We override all the drag handlers.  It's tricky to get the
2003      position of the drag icon right in drag_begin.  GtkTreeView has special
2004      voodoo for it, so we let it do the voodoo that he do (but only if dragging
2005      is enabled).
2006    */
2007   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
2008       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
2009
2010   /* Update DnD source/dest */
2011   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
2012     {
2013       gtk_drag_source_set (GTK_WIDGET (view),
2014           GDK_BUTTON1_MASK,
2015           drag_types_source,
2016           G_N_ELEMENTS (drag_types_source),
2017           GDK_ACTION_MOVE | GDK_ACTION_COPY);
2018     }
2019   else
2020     {
2021       gtk_drag_source_unset (GTK_WIDGET (view));
2022
2023     }
2024
2025   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
2026     {
2027       gtk_drag_dest_set (GTK_WIDGET (view),
2028           GTK_DEST_DEFAULT_ALL,
2029           drag_types_dest,
2030           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
2031     }
2032   else
2033     {
2034       /* FIXME: URI could still be droped depending on FT feature */
2035       gtk_drag_dest_unset (GTK_WIDGET (view));
2036     }
2037
2038   /* Update has-tooltip */
2039   has_tooltip =
2040       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
2041   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
2042 }
2043
2044 static void
2045 individual_view_dispose (GObject *object)
2046 {
2047   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2048   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2049
2050   tp_clear_object (&priv->store);
2051   tp_clear_object (&priv->filter);
2052   tp_clear_object (&priv->tooltip_widget);
2053
2054   empathy_individual_view_set_live_search (view, NULL);
2055
2056   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
2057 }
2058
2059 static void
2060 individual_view_finalize (GObject *object)
2061 {
2062   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2063
2064   if (priv->expand_groups_idle_handler != 0)
2065     g_source_remove (priv->expand_groups_idle_handler);
2066   g_hash_table_destroy (priv->expand_groups);
2067
2068   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
2069 }
2070
2071 static void
2072 individual_view_get_property (GObject *object,
2073     guint param_id,
2074     GValue *value,
2075     GParamSpec *pspec)
2076 {
2077   EmpathyIndividualViewPriv *priv;
2078
2079   priv = GET_PRIV (object);
2080
2081   switch (param_id)
2082     {
2083     case PROP_STORE:
2084       g_value_set_object (value, priv->store);
2085       break;
2086     case PROP_VIEW_FEATURES:
2087       g_value_set_flags (value, priv->view_features);
2088       break;
2089     case PROP_INDIVIDUAL_FEATURES:
2090       g_value_set_flags (value, priv->individual_features);
2091       break;
2092     case PROP_SHOW_OFFLINE:
2093       g_value_set_boolean (value, priv->show_offline);
2094       break;
2095     case PROP_SHOW_UNTRUSTED:
2096       g_value_set_boolean (value, priv->show_untrusted);
2097       break;
2098     default:
2099       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2100       break;
2101     };
2102 }
2103
2104 static void
2105 individual_view_set_property (GObject *object,
2106     guint param_id,
2107     const GValue *value,
2108     GParamSpec *pspec)
2109 {
2110   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2111   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2112
2113   switch (param_id)
2114     {
2115     case PROP_STORE:
2116       empathy_individual_view_set_store (view, g_value_get_object (value));
2117       break;
2118     case PROP_VIEW_FEATURES:
2119       individual_view_set_view_features (view, g_value_get_flags (value));
2120       break;
2121     case PROP_INDIVIDUAL_FEATURES:
2122       priv->individual_features = g_value_get_flags (value);
2123       break;
2124     case PROP_SHOW_OFFLINE:
2125       empathy_individual_view_set_show_offline (view,
2126           g_value_get_boolean (value));
2127       break;
2128     case PROP_SHOW_UNTRUSTED:
2129       empathy_individual_view_set_show_untrusted (view,
2130           g_value_get_boolean (value));
2131       break;
2132     default:
2133       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2134       break;
2135     };
2136 }
2137
2138 static void
2139 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
2140 {
2141   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2142   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2143   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
2144
2145   object_class->constructed = individual_view_constructed;
2146   object_class->dispose = individual_view_dispose;
2147   object_class->finalize = individual_view_finalize;
2148   object_class->get_property = individual_view_get_property;
2149   object_class->set_property = individual_view_set_property;
2150
2151   widget_class->drag_data_received = individual_view_drag_data_received;
2152   widget_class->drag_drop = individual_view_drag_drop;
2153   widget_class->drag_begin = individual_view_drag_begin;
2154   widget_class->drag_data_get = individual_view_drag_data_get;
2155   widget_class->drag_end = individual_view_drag_end;
2156   widget_class->drag_motion = individual_view_drag_motion;
2157
2158   /* We use the class method to let user of this widget to connect to
2159    * the signal and stop emission of the signal so the default handler
2160    * won't be called. */
2161   tree_view_class->row_activated = individual_view_row_activated;
2162
2163   klass->drag_individual_received = real_drag_individual_received_cb;
2164
2165   signals[DRAG_INDIVIDUAL_RECEIVED] =
2166       g_signal_new ("drag-individual-received",
2167       G_OBJECT_CLASS_TYPE (klass),
2168       G_SIGNAL_RUN_LAST,
2169       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
2170       NULL, NULL,
2171       _empathy_gtk_marshal_VOID__UINT_OBJECT_STRING_STRING,
2172       G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
2173       G_TYPE_STRING, G_TYPE_STRING);
2174
2175   signals[DRAG_PERSONA_RECEIVED] =
2176       g_signal_new ("drag-persona-received",
2177       G_OBJECT_CLASS_TYPE (klass),
2178       G_SIGNAL_RUN_LAST,
2179       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
2180       NULL, NULL,
2181       _empathy_gtk_marshal_BOOLEAN__UINT_OBJECT_OBJECT,
2182       G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
2183
2184   g_object_class_install_property (object_class,
2185       PROP_STORE,
2186       g_param_spec_object ("store",
2187           "The store of the view",
2188           "The store of the view",
2189           EMPATHY_TYPE_INDIVIDUAL_STORE,
2190           G_PARAM_READWRITE));
2191   g_object_class_install_property (object_class,
2192       PROP_VIEW_FEATURES,
2193       g_param_spec_flags ("view-features",
2194           "Features of the view",
2195           "Flags for all enabled features",
2196           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
2197           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
2198   g_object_class_install_property (object_class,
2199       PROP_INDIVIDUAL_FEATURES,
2200       g_param_spec_flags ("individual-features",
2201           "Features of the individual menu",
2202           "Flags for all enabled features for the menu",
2203           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
2204           EMPATHY_INDIVIDUAL_FEATURE_NONE, G_PARAM_READWRITE));
2205   g_object_class_install_property (object_class,
2206       PROP_SHOW_OFFLINE,
2207       g_param_spec_boolean ("show-offline",
2208           "Show Offline",
2209           "Whether contact list should display "
2210           "offline contacts", FALSE, G_PARAM_READWRITE));
2211   g_object_class_install_property (object_class,
2212       PROP_SHOW_UNTRUSTED,
2213       g_param_spec_boolean ("show-untrusted",
2214           "Show Untrusted Individuals",
2215           "Whether the view should display untrusted individuals; "
2216           "those who could not be who they say they are.",
2217           TRUE, G_PARAM_READWRITE));
2218
2219   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
2220 }
2221
2222 static void
2223 empathy_individual_view_init (EmpathyIndividualView *view)
2224 {
2225   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
2226       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
2227
2228   view->priv = priv;
2229
2230   priv->show_untrusted = TRUE;
2231
2232   /* Get saved group states. */
2233   empathy_contact_groups_get_all ();
2234
2235   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
2236       (GDestroyNotify) g_free, NULL);
2237
2238   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
2239       empathy_individual_store_row_separator_func, NULL, NULL);
2240
2241   /* Connect to tree view signals rather than override. */
2242   g_signal_connect (view, "button-press-event",
2243       G_CALLBACK (individual_view_button_press_event_cb), NULL);
2244   g_signal_connect (view, "key-press-event",
2245       G_CALLBACK (individual_view_key_press_event_cb), NULL);
2246   g_signal_connect (view, "row-expanded",
2247       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2248       GINT_TO_POINTER (TRUE));
2249   g_signal_connect (view, "row-collapsed",
2250       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2251       GINT_TO_POINTER (FALSE));
2252   g_signal_connect (view, "query-tooltip",
2253       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
2254 }
2255
2256 EmpathyIndividualView *
2257 empathy_individual_view_new (EmpathyIndividualStore *store,
2258     EmpathyIndividualViewFeatureFlags view_features,
2259     EmpathyIndividualFeatureFlags individual_features)
2260 {
2261   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
2262
2263   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
2264       "store", store,
2265       "individual-features", individual_features,
2266       "view-features", view_features, NULL);
2267 }
2268
2269 FolksIndividual *
2270 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
2271 {
2272   GtkTreeSelection *selection;
2273   GtkTreeIter iter;
2274   GtkTreeModel *model;
2275   FolksIndividual *individual;
2276
2277   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2278
2279   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2280   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2281     return NULL;
2282
2283   gtk_tree_model_get (model, &iter,
2284       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
2285
2286   return individual;
2287 }
2288
2289 static gchar *
2290 empathy_individual_view_dup_selected_group (EmpathyIndividualView *view,
2291     gboolean *is_fake_group)
2292 {
2293   GtkTreeSelection *selection;
2294   GtkTreeIter iter;
2295   GtkTreeModel *model;
2296   gboolean is_group;
2297   gchar *name;
2298   gboolean fake;
2299
2300   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2301
2302   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2303   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2304     return NULL;
2305
2306   gtk_tree_model_get (model, &iter,
2307       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2308       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2309       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2310
2311   if (!is_group)
2312     {
2313       g_free (name);
2314       return NULL;
2315     }
2316
2317   if (is_fake_group != NULL)
2318     *is_fake_group = fake;
2319
2320   return name;
2321 }
2322
2323 enum
2324 {
2325   REMOVE_DIALOG_RESPONSE_CANCEL = 0,
2326   REMOVE_DIALOG_RESPONSE_DELETE,
2327   REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK,
2328 };
2329
2330 static int
2331 individual_view_remove_dialog_show (GtkWindow *parent,
2332     const gchar *message,
2333     const gchar *secondary_text,
2334     gboolean block_button,
2335     GdkPixbuf *avatar)
2336 {
2337   GtkWidget *dialog;
2338   gboolean res;
2339
2340   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2341       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2342
2343   if (avatar != NULL)
2344     {
2345       GtkWidget *image = gtk_image_new_from_pixbuf (avatar);
2346       gtk_message_dialog_set_image (GTK_MESSAGE_DIALOG (dialog), image);
2347       gtk_widget_show (image);
2348     }
2349
2350   if (block_button)
2351     {
2352       GtkWidget *button;
2353
2354       /* gtk_dialog_add_button() doesn't allow us to pass a string with a
2355        * mnemonic so we have to create the button manually. */
2356       button = gtk_button_new_with_mnemonic (
2357           _("Delete and _Block"));
2358
2359       gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
2360           REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK);
2361
2362       gtk_widget_show (button);
2363     }
2364
2365   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2366       GTK_STOCK_CANCEL, REMOVE_DIALOG_RESPONSE_CANCEL,
2367       GTK_STOCK_DELETE, REMOVE_DIALOG_RESPONSE_DELETE, NULL);
2368   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2369       "%s", secondary_text);
2370
2371   gtk_widget_show (dialog);
2372
2373   res = gtk_dialog_run (GTK_DIALOG (dialog));
2374   gtk_widget_destroy (dialog);
2375
2376   return res;
2377 }
2378
2379 static void
2380 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2381     EmpathyIndividualView *view)
2382 {
2383   gchar *group;
2384
2385   group = empathy_individual_view_dup_selected_group (view, NULL);
2386   if (group != NULL)
2387     {
2388       gchar *text;
2389       GtkWindow *parent;
2390
2391       text =
2392           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2393           group);
2394       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2395       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2396               text, FALSE, NULL) == REMOVE_DIALOG_RESPONSE_DELETE)
2397         {
2398           EmpathyIndividualManager *manager =
2399               empathy_individual_manager_dup_singleton ();
2400           empathy_individual_manager_remove_group (manager, group);
2401           g_object_unref (G_OBJECT (manager));
2402         }
2403
2404       g_free (text);
2405     }
2406
2407   g_free (group);
2408 }
2409
2410 GtkWidget *
2411 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2412 {
2413   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2414   gchar *group;
2415   GtkWidget *menu;
2416   GtkWidget *item;
2417   GtkWidget *image;
2418   gboolean is_fake_group;
2419
2420   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2421
2422   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2423               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2424     return NULL;
2425
2426   group = empathy_individual_view_dup_selected_group (view, &is_fake_group);
2427   if (!group || is_fake_group)
2428     {
2429       /* We can't alter fake groups */
2430       g_free (group);
2431       return NULL;
2432     }
2433
2434   menu = gtk_menu_new ();
2435
2436   /* TODO: implement
2437      if (priv->view_features &
2438      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2439      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2440      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2441      gtk_widget_show (item);
2442      g_signal_connect (item, "activate",
2443      G_CALLBACK (individual_view_group_rename_activate_cb),
2444      view);
2445      }
2446    */
2447
2448   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2449     {
2450       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2451       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2452           GTK_ICON_SIZE_MENU);
2453       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2454       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2455       gtk_widget_show (item);
2456       g_signal_connect (item, "activate",
2457           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2458     }
2459
2460   g_free (group);
2461
2462   return menu;
2463 }
2464
2465 static void
2466 got_avatar (GObject *source_object,
2467     GAsyncResult *result,
2468     gpointer user_data)
2469 {
2470   FolksIndividual *individual = FOLKS_INDIVIDUAL (source_object);
2471   EmpathyIndividualView *view = user_data;
2472   GdkPixbuf *avatar;
2473   EmpathyIndividualManager *manager;
2474   gchar *text;
2475   GtkWindow *parent;
2476   GList *l, *personas;
2477   guint persona_count = 0;
2478   gboolean can_block;
2479   GError *error = NULL;
2480   gint res;
2481
2482   avatar = empathy_pixbuf_avatar_from_individual_scaled_finish (individual,
2483       result, &error);
2484
2485   if (error != NULL)
2486     {
2487       DEBUG ("Could not get avatar: %s", error->message);
2488       g_error_free (error);
2489     }
2490
2491   /* We couldn't retrieve the avatar, but that isn't a fatal error,
2492    * so we still display the remove dialog. */
2493
2494   personas = folks_individual_get_personas (individual);
2495
2496   /* If we have more than one TpfPersona, display a different message
2497    * ensuring the user knows that *all* of the meta-contacts' personas will
2498    * be removed. */
2499   for (l = personas; l != NULL; l = l->next)
2500     {
2501       if (!empathy_folks_persona_is_interesting (FOLKS_PERSONA (l->data)))
2502         continue;
2503
2504       persona_count++;
2505       if (persona_count >= 2)
2506         break;
2507     }
2508
2509   if (persona_count < 2)
2510     {
2511       /* Not a meta-contact */
2512       text =
2513           g_strdup_printf (
2514               _("Do you really want to remove the contact '%s'?"),
2515               folks_alias_details_get_alias (
2516                   FOLKS_ALIAS_DETAILS (individual)));
2517     }
2518   else
2519     {
2520       /* Meta-contact */
2521       text =
2522           g_strdup_printf (
2523               _("Do you really want to remove the linked contact '%s'? "
2524                 "Note that this will remove all the contacts which make up "
2525                 "this linked contact."),
2526               folks_alias_details_get_alias (
2527                   FOLKS_ALIAS_DETAILS (individual)));
2528     }
2529
2530
2531   manager = empathy_individual_manager_dup_singleton ();
2532   can_block = empathy_individual_manager_supports_blocking (manager,
2533       individual);
2534   parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2535   res = individual_view_remove_dialog_show (parent, _("Removing contact"),
2536           text, can_block, avatar);
2537
2538   if (res == REMOVE_DIALOG_RESPONSE_DELETE ||
2539       res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2540     {
2541       gboolean abusive;
2542
2543       if (res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2544         {
2545           if (!empathy_block_individual_dialog_show (parent, individual,
2546                 avatar, &abusive))
2547             goto finally;
2548
2549           empathy_individual_manager_set_blocked (manager, individual,
2550               TRUE, abusive);
2551         }
2552
2553       empathy_individual_manager_remove (manager, individual, "");
2554     }
2555
2556  finally:
2557   g_free (text);
2558   g_object_unref (manager);
2559 }
2560
2561 static void
2562 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2563     EmpathyIndividualView *view)
2564 {
2565   FolksIndividual *individual;
2566
2567   individual = empathy_individual_view_dup_selected (view);
2568
2569   if (individual != NULL)
2570     {
2571       empathy_pixbuf_avatar_from_individual_scaled_async (individual,
2572           48, 48, NULL, got_avatar, view);
2573       g_object_unref (individual);
2574     }
2575 }
2576
2577 static void
2578 individual_menu_link_contacts_activated_cb (EmpathyIndividualMenu *menu,
2579     EmpathyLinkingDialog *linking_dialog,
2580     EmpathyIndividualView *self)
2581 {
2582   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2583   EmpathyIndividualLinker *linker;
2584
2585   linker = empathy_linking_dialog_get_individual_linker (linking_dialog);
2586   empathy_individual_linker_set_search_text (linker,
2587       empathy_live_search_get_text (EMPATHY_LIVE_SEARCH (priv->search_widget)));
2588 }
2589
2590 GtkWidget *
2591 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2592 {
2593   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2594   FolksIndividual *individual;
2595   GtkWidget *menu = NULL;
2596   GtkWidget *item;
2597   GtkWidget *image;
2598   gboolean can_remove = FALSE;
2599   GList *l;
2600
2601   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2602
2603   individual = empathy_individual_view_dup_selected (view);
2604   if (individual == NULL)
2605     return NULL;
2606
2607   /* If any of the Individual's personas can be removed, add an option to
2608    * remove. This will act as a best-effort option. If any Personas cannot be
2609    * removed from the server, then this option will just be inactive upon
2610    * subsequent menu openings */
2611   for (l = folks_individual_get_personas (individual); l != NULL; l = l->next)
2612     {
2613       FolksPersona *persona = FOLKS_PERSONA (l->data);
2614       FolksPersonaStore *store = folks_persona_get_store (persona);
2615       FolksMaybeBool maybe_can_remove =
2616           folks_persona_store_get_can_remove_personas (store);
2617
2618       if (maybe_can_remove == FOLKS_MAYBE_BOOL_TRUE)
2619         {
2620           can_remove = TRUE;
2621           break;
2622         }
2623     }
2624
2625   menu = empathy_individual_menu_new (individual, priv->individual_features);
2626
2627   /* Remove contact */
2628   if ((priv->view_features &
2629       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE) &&
2630       can_remove)
2631     {
2632       /* create the menu if required, or just add a separator */
2633       if (menu == NULL)
2634         menu = gtk_menu_new ();
2635       else
2636         {
2637           item = gtk_separator_menu_item_new ();
2638           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2639           gtk_widget_show (item);
2640         }
2641
2642       /* Remove */
2643       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2644       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2645           GTK_ICON_SIZE_MENU);
2646       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2647       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2648       gtk_widget_show (item);
2649       g_signal_connect (item, "activate",
2650           G_CALLBACK (individual_view_remove_activate_cb), view);
2651     }
2652
2653   /* Connect to EmpathyIndividualMenu::link-contacts-activated so that we can
2654    * set the live search text on the new linking dialogue to be the same as
2655    * our own. */
2656   g_signal_connect (menu, "link-contacts-activated",
2657       (GCallback) individual_menu_link_contacts_activated_cb, view);
2658
2659   g_object_unref (individual);
2660
2661   return menu;
2662 }
2663
2664 void
2665 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2666     EmpathyLiveSearch *search)
2667 {
2668   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2669
2670   /* remove old handlers if old search was not null */
2671   if (priv->search_widget != NULL)
2672     {
2673       g_signal_handlers_disconnect_by_func (view,
2674           individual_view_start_search_cb, NULL);
2675
2676       g_signal_handlers_disconnect_by_func (priv->search_widget,
2677           individual_view_search_text_notify_cb, view);
2678       g_signal_handlers_disconnect_by_func (priv->search_widget,
2679           individual_view_search_activate_cb, view);
2680       g_signal_handlers_disconnect_by_func (priv->search_widget,
2681           individual_view_search_key_navigation_cb, view);
2682       g_signal_handlers_disconnect_by_func (priv->search_widget,
2683           individual_view_search_hide_cb, view);
2684       g_signal_handlers_disconnect_by_func (priv->search_widget,
2685           individual_view_search_show_cb, view);
2686       g_object_unref (priv->search_widget);
2687       priv->search_widget = NULL;
2688     }
2689
2690   /* connect handlers if new search is not null */
2691   if (search != NULL)
2692     {
2693       priv->search_widget = g_object_ref (search);
2694
2695       g_signal_connect (view, "start-interactive-search",
2696           G_CALLBACK (individual_view_start_search_cb), NULL);
2697
2698       g_signal_connect (priv->search_widget, "notify::text",
2699           G_CALLBACK (individual_view_search_text_notify_cb), view);
2700       g_signal_connect (priv->search_widget, "activate",
2701           G_CALLBACK (individual_view_search_activate_cb), view);
2702       g_signal_connect (priv->search_widget, "key-navigation",
2703           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2704       g_signal_connect (priv->search_widget, "hide",
2705           G_CALLBACK (individual_view_search_hide_cb), view);
2706       g_signal_connect (priv->search_widget, "show",
2707           G_CALLBACK (individual_view_search_show_cb), view);
2708     }
2709 }
2710
2711 gboolean
2712 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2713 {
2714   EmpathyIndividualViewPriv *priv;
2715
2716   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2717
2718   priv = GET_PRIV (self);
2719
2720   return (priv->search_widget != NULL &&
2721           gtk_widget_get_visible (priv->search_widget));
2722 }
2723
2724 gboolean
2725 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2726 {
2727   EmpathyIndividualViewPriv *priv;
2728
2729   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2730
2731   priv = GET_PRIV (self);
2732
2733   return priv->show_offline;
2734 }
2735
2736 void
2737 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2738     gboolean show_offline)
2739 {
2740   EmpathyIndividualViewPriv *priv;
2741
2742   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2743
2744   priv = GET_PRIV (self);
2745
2746   priv->show_offline = show_offline;
2747
2748   g_object_notify (G_OBJECT (self), "show-offline");
2749   gtk_tree_model_filter_refilter (priv->filter);
2750 }
2751
2752 gboolean
2753 empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
2754 {
2755   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2756
2757   return GET_PRIV (self)->show_untrusted;
2758 }
2759
2760 void
2761 empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
2762     gboolean show_untrusted)
2763 {
2764   EmpathyIndividualViewPriv *priv;
2765
2766   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2767
2768   priv = GET_PRIV (self);
2769
2770   priv->show_untrusted = show_untrusted;
2771
2772   g_object_notify (G_OBJECT (self), "show-untrusted");
2773   gtk_tree_model_filter_refilter (priv->filter);
2774 }
2775
2776 EmpathyIndividualStore *
2777 empathy_individual_view_get_store (EmpathyIndividualView *self)
2778 {
2779   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2780
2781   return GET_PRIV (self)->store;
2782 }
2783
2784 void
2785 empathy_individual_view_set_store (EmpathyIndividualView *self,
2786     EmpathyIndividualStore *store)
2787 {
2788   EmpathyIndividualViewPriv *priv;
2789
2790   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2791   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2792
2793   priv = GET_PRIV (self);
2794
2795   /* Destroy the old filter and remove the old store */
2796   if (priv->store != NULL)
2797     {
2798       g_signal_handlers_disconnect_by_func (priv->store,
2799           individual_view_store_row_changed_cb, self);
2800       g_signal_handlers_disconnect_by_func (priv->store,
2801           individual_view_store_row_deleted_cb, self);
2802
2803       g_signal_handlers_disconnect_by_func (priv->filter,
2804           individual_view_row_has_child_toggled_cb, self);
2805
2806       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2807     }
2808
2809   tp_clear_object (&priv->filter);
2810   tp_clear_object (&priv->store);
2811
2812   /* Set the new store */
2813   priv->store = store;
2814
2815   if (store != NULL)
2816     {
2817       g_object_ref (store);
2818
2819       /* Create a new filter */
2820       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2821           GTK_TREE_MODEL (priv->store), NULL));
2822       gtk_tree_model_filter_set_visible_func (priv->filter,
2823           individual_view_filter_visible_func, self, NULL);
2824
2825       g_signal_connect (priv->filter, "row-has-child-toggled",
2826           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2827       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2828           GTK_TREE_MODEL (priv->filter));
2829
2830       tp_g_signal_connect_object (priv->store, "row-changed",
2831           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2832       tp_g_signal_connect_object (priv->store, "row-inserted",
2833           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2834       tp_g_signal_connect_object (priv->store, "row-deleted",
2835           G_CALLBACK (individual_view_store_row_deleted_cb), self, 0);
2836     }
2837 }
2838
2839 void
2840 empathy_individual_view_start_search (EmpathyIndividualView *self)
2841 {
2842   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2843
2844   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2845   g_return_if_fail (priv->search_widget != NULL);
2846
2847   if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
2848     gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
2849   else
2850     gtk_widget_show (GTK_WIDGET (priv->search_widget));
2851 }
2852
2853 void
2854 empathy_individual_view_set_custom_filter (EmpathyIndividualView *self,
2855     GtkTreeModelFilterVisibleFunc filter,
2856     gpointer data)
2857 {
2858   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2859
2860   priv->custom_filter = filter;
2861   priv->custom_filter_data = data;
2862 }