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