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