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