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