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