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