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