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