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