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