]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
Merge remote-tracking branch 'glassrose/add-All-service-selection-in-debug-window'
[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   DEBUG ("individual_view_expand_idle_cb");
1567
1568   g_signal_handlers_block_by_func (self,
1569     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1570   g_signal_handlers_block_by_func (self,
1571     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1572
1573   /* The store/filter could've been removed while we were in the idle queue */
1574   if (priv->filter != NULL)
1575     {
1576       gtk_tree_model_foreach (GTK_TREE_MODEL (priv->filter),
1577           (GtkTreeModelForeachFunc) expand_idle_foreach_cb, self);
1578     }
1579
1580   g_signal_handlers_unblock_by_func (self,
1581       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1582   g_signal_handlers_unblock_by_func (self,
1583       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1584
1585   /* Empty the table of groups to expand/contract, since it may contain groups
1586    * which no longer exist in the tree view. This can happen after going
1587    * offline, for example. */
1588   g_hash_table_remove_all (priv->expand_groups);
1589   priv->expand_groups_idle_handler = 0;
1590   g_object_unref (self);
1591
1592   return FALSE;
1593 }
1594
1595 static void
1596 individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
1597     GtkTreePath *path,
1598     GtkTreeIter *iter,
1599     EmpathyIndividualView *view)
1600 {
1601   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1602   gboolean should_expand, is_group = FALSE;
1603   gchar *name = NULL;
1604   gpointer will_expand;
1605
1606   gtk_tree_model_get (model, iter,
1607       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1608       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1609       -1);
1610
1611   if (!is_group || EMP_STR_EMPTY (name))
1612     {
1613       g_free (name);
1614       return;
1615     }
1616
1617   should_expand = (priv->view_features &
1618           EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1619       (priv->search_widget != NULL &&
1620           gtk_widget_get_visible (priv->search_widget)) ||
1621       empathy_contact_group_get_expanded (name);
1622
1623   /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1624    * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
1625    * a hash table, and expand or contract them as appropriate all at once in
1626    * an idle handler which iterates over all the group rows. */
1627   if (!g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1628       &will_expand) ||
1629       GPOINTER_TO_INT (will_expand) != should_expand)
1630     {
1631       g_hash_table_insert (priv->expand_groups, g_strdup (name),
1632           GINT_TO_POINTER (should_expand));
1633
1634       if (priv->expand_groups_idle_handler == 0)
1635         {
1636           priv->expand_groups_idle_handler =
1637               g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
1638                   g_object_ref (view));
1639         }
1640     }
1641
1642   g_free (name);
1643 }
1644
1645 static gboolean
1646 individual_view_is_visible_individual (EmpathyIndividualView *self,
1647     FolksIndividual *individual,
1648     gboolean is_online,
1649     gboolean is_searching,
1650     const gchar *group,
1651     gboolean is_fake_group,
1652     guint event_count)
1653 {
1654   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1655   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
1656   GeeSet *personas;
1657   GeeIterator *iter;
1658   gboolean is_favorite;
1659
1660   /* Always display individuals having pending events */
1661   if (event_count > 0)
1662     return TRUE;
1663
1664   /* We're only giving the visibility wrt filtering here, not things like
1665    * presence. */
1666   if (!priv->show_untrusted &&
1667       folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
1668     {
1669       return FALSE;
1670     }
1671
1672   if (!priv->show_uninteresting)
1673     {
1674       gboolean contains_interesting_persona = FALSE;
1675
1676       /* Hide all individuals which consist entirely of uninteresting
1677        * personas */
1678       personas = folks_individual_get_personas (individual);
1679       iter = gee_iterable_iterator (GEE_ITERABLE (personas));
1680       while (!contains_interesting_persona && gee_iterator_next (iter))
1681         {
1682           FolksPersona *persona = gee_iterator_get (iter);
1683
1684           if (empathy_folks_persona_is_interesting (persona))
1685             contains_interesting_persona = TRUE;
1686
1687           g_clear_object (&persona);
1688         }
1689       g_clear_object (&iter);
1690
1691       if (!contains_interesting_persona)
1692         return FALSE;
1693     }
1694
1695   is_favorite = folks_favourite_details_get_is_favourite (
1696       FOLKS_FAVOURITE_DETAILS (individual));
1697   if (!is_searching) {
1698     if (is_favorite && is_fake_group &&
1699         !tp_strdiff (group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
1700         /* Always display favorite contacts in the favorite group */
1701         return TRUE;
1702
1703     return (priv->show_offline || is_online);
1704   }
1705
1706   return empathy_individual_match_string (individual,
1707       empathy_live_search_get_text (live),
1708       empathy_live_search_get_words (live));
1709 }
1710
1711 static gchar *
1712 get_group (GtkTreeModel *model,
1713     GtkTreeIter *iter,
1714     gboolean *is_fake)
1715 {
1716   GtkTreeIter parent_iter;
1717   gchar *name = NULL;
1718
1719   *is_fake = FALSE;
1720
1721   if (!gtk_tree_model_iter_parent (model, &parent_iter, iter))
1722     return NULL;
1723
1724   gtk_tree_model_get (model, &parent_iter,
1725       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1726       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake,
1727       -1);
1728
1729   return name;
1730 }
1731
1732
1733 static gboolean
1734 individual_view_filter_visible_func (GtkTreeModel *model,
1735     GtkTreeIter *iter,
1736     gpointer user_data)
1737 {
1738   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1739   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1740   FolksIndividual *individual = NULL;
1741   gboolean is_group, is_separator, valid;
1742   GtkTreeIter child_iter;
1743   gboolean visible, is_online;
1744   gboolean is_searching = TRUE;
1745   guint event_count;
1746
1747   if (priv->custom_filter != NULL)
1748     return priv->custom_filter (model, iter, priv->custom_filter_data);
1749
1750   if (priv->search_widget == NULL ||
1751       !gtk_widget_get_visible (priv->search_widget))
1752      is_searching = FALSE;
1753
1754   gtk_tree_model_get (model, iter,
1755       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1756       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1757       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1758       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1759       EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
1760       -1);
1761
1762   if (individual != NULL)
1763     {
1764       gchar *group;
1765       gboolean is_fake_group;
1766
1767       group = get_group (model, iter, &is_fake_group);
1768
1769       visible = individual_view_is_visible_individual (self, individual,
1770           is_online, is_searching, group, is_fake_group, event_count);
1771
1772       g_object_unref (individual);
1773       g_free (group);
1774
1775       return visible;
1776     }
1777
1778   if (is_separator)
1779     return TRUE;
1780
1781   /* Not a contact, not a separator, must be a group */
1782   g_return_val_if_fail (is_group, FALSE);
1783
1784   /* only show groups which are not empty */
1785   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1786        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1787     {
1788       gchar *group;
1789       gboolean is_fake_group;
1790
1791       gtk_tree_model_get (model, &child_iter,
1792         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1793         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1794         EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
1795         -1);
1796
1797       if (individual == NULL)
1798         continue;
1799
1800       group = get_group (model, &child_iter, &is_fake_group);
1801
1802       visible = individual_view_is_visible_individual (self, individual,
1803           is_online, is_searching, group, is_fake_group, event_count);
1804
1805       g_object_unref (individual);
1806       g_free (group);
1807
1808       /* show group if it has at least one visible contact in it */
1809       if (visible)
1810         return TRUE;
1811     }
1812
1813   return FALSE;
1814 }
1815
1816 static void
1817 individual_view_constructed (GObject *object)
1818 {
1819   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1820   GtkCellRenderer *cell;
1821   GtkTreeViewColumn *col;
1822   guint i;
1823
1824   /* Setup view */
1825   g_object_set (view,
1826       "headers-visible", FALSE,
1827       "show-expanders", FALSE,
1828       NULL);
1829
1830   col = gtk_tree_view_column_new ();
1831
1832   /* State */
1833   cell = gtk_cell_renderer_pixbuf_new ();
1834   gtk_tree_view_column_pack_start (col, cell, FALSE);
1835   gtk_tree_view_column_set_cell_data_func (col, cell,
1836       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1837       view, NULL);
1838
1839   g_object_set (cell,
1840       "xpad", 5,
1841       "ypad", 1,
1842       "visible", FALSE,
1843       NULL);
1844
1845   /* Group icon */
1846   cell = gtk_cell_renderer_pixbuf_new ();
1847   gtk_tree_view_column_pack_start (col, cell, FALSE);
1848   gtk_tree_view_column_set_cell_data_func (col, cell,
1849       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1850       view, NULL);
1851
1852   g_object_set (cell,
1853       "xpad", 0,
1854       "ypad", 0,
1855       "visible", FALSE,
1856       "width", 16,
1857       "height", 16,
1858       NULL);
1859
1860   /* Name */
1861   cell = empathy_cell_renderer_text_new ();
1862   gtk_tree_view_column_pack_start (col, cell, TRUE);
1863   gtk_tree_view_column_set_cell_data_func (col, cell,
1864       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1865
1866   gtk_tree_view_column_add_attribute (col, cell,
1867       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1868   gtk_tree_view_column_add_attribute (col, cell,
1869       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1870   gtk_tree_view_column_add_attribute (col, cell,
1871       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1872   gtk_tree_view_column_add_attribute (col, cell,
1873       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1874   gtk_tree_view_column_add_attribute (col, cell,
1875       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1876   gtk_tree_view_column_add_attribute (col, cell,
1877       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1878   gtk_tree_view_column_add_attribute (col, cell,
1879       "client-types", EMPATHY_INDIVIDUAL_STORE_COL_CLIENT_TYPES);
1880
1881   /* Audio Call Icon */
1882   cell = empathy_cell_renderer_activatable_new ();
1883   gtk_tree_view_column_pack_start (col, cell, FALSE);
1884   gtk_tree_view_column_set_cell_data_func (col, cell,
1885       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1886       view, NULL);
1887
1888   g_object_set (cell, "visible", FALSE, NULL);
1889
1890   g_signal_connect (cell, "path-activated",
1891       G_CALLBACK (individual_view_call_activated_cb), view);
1892
1893   /* Avatar */
1894   cell = gtk_cell_renderer_pixbuf_new ();
1895   gtk_tree_view_column_pack_start (col, cell, FALSE);
1896   gtk_tree_view_column_set_cell_data_func (col, cell,
1897       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1898       view, NULL);
1899
1900   g_object_set (cell,
1901       "xpad", 0,
1902       "ypad", 0,
1903       "visible", FALSE,
1904       "width", 32,
1905       "height", 32,
1906       NULL);
1907
1908   /* Expander */
1909   cell = empathy_cell_renderer_expander_new ();
1910   gtk_tree_view_column_pack_end (col, cell, FALSE);
1911   gtk_tree_view_column_set_cell_data_func (col, cell,
1912       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1913       view, NULL);
1914
1915   /* Actually add the column now we have added all cell renderers */
1916   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1917
1918   /* Drag & Drop. */
1919   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1920     {
1921       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1922     }
1923 }
1924
1925 static void
1926 individual_view_set_view_features (EmpathyIndividualView *view,
1927     EmpathyIndividualFeatureFlags features)
1928 {
1929   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1930   gboolean has_tooltip;
1931
1932   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1933
1934   priv->view_features = features;
1935
1936   /* Setting reorderable is a hack that gets us row previews as drag icons
1937      for free.  We override all the drag handlers.  It's tricky to get the
1938      position of the drag icon right in drag_begin.  GtkTreeView has special
1939      voodoo for it, so we let it do the voodoo that he do (but only if dragging
1940      is enabled).
1941    */
1942   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
1943       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
1944
1945   /* Update DnD source/dest */
1946   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
1947     {
1948       gtk_drag_source_set (GTK_WIDGET (view),
1949           GDK_BUTTON1_MASK,
1950           drag_types_source,
1951           G_N_ELEMENTS (drag_types_source),
1952           GDK_ACTION_MOVE | GDK_ACTION_COPY);
1953     }
1954   else
1955     {
1956       gtk_drag_source_unset (GTK_WIDGET (view));
1957
1958     }
1959
1960   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
1961     {
1962       gtk_drag_dest_set (GTK_WIDGET (view),
1963           GTK_DEST_DEFAULT_ALL,
1964           drag_types_dest,
1965           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
1966     }
1967   else
1968     {
1969       /* FIXME: URI could still be droped depending on FT feature */
1970       gtk_drag_dest_unset (GTK_WIDGET (view));
1971     }
1972
1973   /* Update has-tooltip */
1974   has_tooltip =
1975       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
1976   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1977 }
1978
1979 static void
1980 individual_view_dispose (GObject *object)
1981 {
1982   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1983   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1984
1985   tp_clear_object (&priv->store);
1986   tp_clear_object (&priv->filter);
1987   tp_clear_object (&priv->tooltip_widget);
1988
1989   empathy_individual_view_set_live_search (view, NULL);
1990
1991   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
1992 }
1993
1994 static void
1995 individual_view_finalize (GObject *object)
1996 {
1997   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1998
1999   if (priv->expand_groups_idle_handler != 0)
2000     g_source_remove (priv->expand_groups_idle_handler);
2001   g_hash_table_unref (priv->expand_groups);
2002
2003   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
2004 }
2005
2006 static void
2007 individual_view_get_property (GObject *object,
2008     guint param_id,
2009     GValue *value,
2010     GParamSpec *pspec)
2011 {
2012   EmpathyIndividualViewPriv *priv;
2013
2014   priv = GET_PRIV (object);
2015
2016   switch (param_id)
2017     {
2018     case PROP_STORE:
2019       g_value_set_object (value, priv->store);
2020       break;
2021     case PROP_VIEW_FEATURES:
2022       g_value_set_flags (value, priv->view_features);
2023       break;
2024     case PROP_INDIVIDUAL_FEATURES:
2025       g_value_set_flags (value, priv->individual_features);
2026       break;
2027     case PROP_SHOW_OFFLINE:
2028       g_value_set_boolean (value, priv->show_offline);
2029       break;
2030     case PROP_SHOW_UNTRUSTED:
2031       g_value_set_boolean (value, priv->show_untrusted);
2032       break;
2033     case PROP_SHOW_UNINTERESTING:
2034       g_value_set_boolean (value, priv->show_uninteresting);
2035       break;
2036     default:
2037       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2038       break;
2039     };
2040 }
2041
2042 static void
2043 individual_view_set_property (GObject *object,
2044     guint param_id,
2045     const GValue *value,
2046     GParamSpec *pspec)
2047 {
2048   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2049   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2050
2051   switch (param_id)
2052     {
2053     case PROP_STORE:
2054       empathy_individual_view_set_store (view, g_value_get_object (value));
2055       break;
2056     case PROP_VIEW_FEATURES:
2057       individual_view_set_view_features (view, g_value_get_flags (value));
2058       break;
2059     case PROP_INDIVIDUAL_FEATURES:
2060       priv->individual_features = g_value_get_flags (value);
2061       break;
2062     case PROP_SHOW_OFFLINE:
2063       empathy_individual_view_set_show_offline (view,
2064           g_value_get_boolean (value));
2065       break;
2066     case PROP_SHOW_UNTRUSTED:
2067       empathy_individual_view_set_show_untrusted (view,
2068           g_value_get_boolean (value));
2069       break;
2070     case PROP_SHOW_UNINTERESTING:
2071       empathy_individual_view_set_show_uninteresting (view,
2072           g_value_get_boolean (value));
2073     default:
2074       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2075       break;
2076     };
2077 }
2078
2079 static void
2080 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
2081 {
2082   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2083   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2084   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
2085
2086   object_class->constructed = individual_view_constructed;
2087   object_class->dispose = individual_view_dispose;
2088   object_class->finalize = individual_view_finalize;
2089   object_class->get_property = individual_view_get_property;
2090   object_class->set_property = individual_view_set_property;
2091
2092   widget_class->drag_data_received = individual_view_drag_data_received;
2093   widget_class->drag_drop = individual_view_drag_drop;
2094   widget_class->drag_begin = individual_view_drag_begin;
2095   widget_class->drag_data_get = individual_view_drag_data_get;
2096   widget_class->drag_end = individual_view_drag_end;
2097   widget_class->drag_motion = individual_view_drag_motion;
2098
2099   /* We use the class method to let user of this widget to connect to
2100    * the signal and stop emission of the signal so the default handler
2101    * won't be called. */
2102   tree_view_class->row_activated = individual_view_row_activated;
2103
2104   klass->drag_individual_received = real_drag_individual_received_cb;
2105
2106   signals[DRAG_INDIVIDUAL_RECEIVED] =
2107       g_signal_new ("drag-individual-received",
2108       G_OBJECT_CLASS_TYPE (klass),
2109       G_SIGNAL_RUN_LAST,
2110       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
2111       NULL, NULL,
2112       g_cclosure_marshal_generic,
2113       G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
2114       G_TYPE_STRING, G_TYPE_STRING);
2115
2116   signals[DRAG_PERSONA_RECEIVED] =
2117       g_signal_new ("drag-persona-received",
2118       G_OBJECT_CLASS_TYPE (klass),
2119       G_SIGNAL_RUN_LAST,
2120       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
2121       NULL, NULL,
2122       g_cclosure_marshal_generic,
2123       G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
2124
2125   g_object_class_install_property (object_class,
2126       PROP_STORE,
2127       g_param_spec_object ("store",
2128           "The store of the view",
2129           "The store of the view",
2130           EMPATHY_TYPE_INDIVIDUAL_STORE,
2131           G_PARAM_READWRITE));
2132   g_object_class_install_property (object_class,
2133       PROP_VIEW_FEATURES,
2134       g_param_spec_flags ("view-features",
2135           "Features of the view",
2136           "Flags for all enabled features",
2137           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
2138           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
2139   g_object_class_install_property (object_class,
2140       PROP_INDIVIDUAL_FEATURES,
2141       g_param_spec_flags ("individual-features",
2142           "Features of the individual menu",
2143           "Flags for all enabled features for the menu",
2144           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
2145           EMPATHY_INDIVIDUAL_FEATURE_NONE, G_PARAM_READWRITE));
2146   g_object_class_install_property (object_class,
2147       PROP_SHOW_OFFLINE,
2148       g_param_spec_boolean ("show-offline",
2149           "Show Offline",
2150           "Whether contact list should display "
2151           "offline contacts", FALSE, G_PARAM_READWRITE));
2152   g_object_class_install_property (object_class,
2153       PROP_SHOW_UNTRUSTED,
2154       g_param_spec_boolean ("show-untrusted",
2155           "Show Untrusted Individuals",
2156           "Whether the view should display untrusted individuals; "
2157           "those who could not be who they say they are.",
2158           TRUE, G_PARAM_READWRITE));
2159   g_object_class_install_property (object_class,
2160       PROP_SHOW_UNINTERESTING,
2161       g_param_spec_boolean ("show-uninteresting",
2162           "Show Uninteresting Individuals",
2163           "Whether the view should not filter out individuals using "
2164           "empathy_folks_persona_is_interesting.",
2165           FALSE, G_PARAM_READWRITE));
2166
2167   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
2168 }
2169
2170 static void
2171 empathy_individual_view_init (EmpathyIndividualView *view)
2172 {
2173   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
2174       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
2175
2176   view->priv = priv;
2177
2178   priv->show_untrusted = TRUE;
2179   priv->show_uninteresting = FALSE;
2180
2181   /* Get saved group states. */
2182   empathy_contact_groups_get_all ();
2183
2184   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
2185       (GDestroyNotify) g_free, NULL);
2186
2187   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
2188       empathy_individual_store_row_separator_func, NULL, NULL);
2189
2190   /* Connect to tree view signals rather than override. */
2191   g_signal_connect (view, "button-press-event",
2192       G_CALLBACK (individual_view_button_press_event_cb), NULL);
2193   g_signal_connect (view, "key-press-event",
2194       G_CALLBACK (individual_view_key_press_event_cb), NULL);
2195   g_signal_connect (view, "row-expanded",
2196       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2197       GINT_TO_POINTER (TRUE));
2198   g_signal_connect (view, "row-collapsed",
2199       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2200       GINT_TO_POINTER (FALSE));
2201   g_signal_connect (view, "query-tooltip",
2202       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
2203 }
2204
2205 EmpathyIndividualView *
2206 empathy_individual_view_new (EmpathyIndividualStore *store,
2207     EmpathyIndividualViewFeatureFlags view_features,
2208     EmpathyIndividualFeatureFlags individual_features)
2209 {
2210   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
2211
2212   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
2213       "store", store,
2214       "individual-features", individual_features,
2215       "view-features", view_features, NULL);
2216 }
2217
2218 FolksIndividual *
2219 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
2220 {
2221   GtkTreeSelection *selection;
2222   GtkTreeIter iter;
2223   GtkTreeModel *model;
2224   FolksIndividual *individual;
2225
2226   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2227
2228   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2229   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2230     return NULL;
2231
2232   gtk_tree_model_get (model, &iter,
2233       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
2234
2235   return individual;
2236 }
2237
2238 static gchar *
2239 empathy_individual_view_dup_selected_group (EmpathyIndividualView *view,
2240     gboolean *is_fake_group)
2241 {
2242   GtkTreeSelection *selection;
2243   GtkTreeIter iter;
2244   GtkTreeModel *model;
2245   gboolean is_group;
2246   gchar *name;
2247   gboolean fake;
2248
2249   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2250
2251   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2252   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2253     return NULL;
2254
2255   gtk_tree_model_get (model, &iter,
2256       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2257       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2258       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2259
2260   if (!is_group)
2261     {
2262       g_free (name);
2263       return NULL;
2264     }
2265
2266   if (is_fake_group != NULL)
2267     *is_fake_group = fake;
2268
2269   return name;
2270 }
2271
2272 enum
2273 {
2274   REMOVE_DIALOG_RESPONSE_CANCEL = 0,
2275   REMOVE_DIALOG_RESPONSE_DELETE,
2276   REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK,
2277 };
2278
2279 static int
2280 individual_view_remove_dialog_show (GtkWindow *parent,
2281     const gchar *message,
2282     const gchar *secondary_text,
2283     gboolean block_button,
2284     GdkPixbuf *avatar)
2285 {
2286   GtkWidget *dialog;
2287   gboolean res;
2288
2289   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2290       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2291
2292   if (avatar != NULL)
2293     {
2294       GtkWidget *image = gtk_image_new_from_pixbuf (avatar);
2295       gtk_message_dialog_set_image (GTK_MESSAGE_DIALOG (dialog), image);
2296       gtk_widget_show (image);
2297     }
2298
2299   if (block_button)
2300     {
2301       GtkWidget *button;
2302
2303       /* gtk_dialog_add_button() doesn't allow us to pass a string with a
2304        * mnemonic so we have to create the button manually. */
2305       button = gtk_button_new_with_mnemonic (
2306           _("Delete and _Block"));
2307
2308       gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
2309           REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK);
2310
2311       gtk_widget_show (button);
2312     }
2313
2314   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2315       GTK_STOCK_CANCEL, REMOVE_DIALOG_RESPONSE_CANCEL,
2316       GTK_STOCK_DELETE, REMOVE_DIALOG_RESPONSE_DELETE, NULL);
2317   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2318       "%s", secondary_text);
2319
2320   gtk_widget_show (dialog);
2321
2322   res = gtk_dialog_run (GTK_DIALOG (dialog));
2323   gtk_widget_destroy (dialog);
2324
2325   return res;
2326 }
2327
2328 static void
2329 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2330     EmpathyIndividualView *view)
2331 {
2332   gchar *group;
2333
2334   group = empathy_individual_view_dup_selected_group (view, NULL);
2335   if (group != NULL)
2336     {
2337       gchar *text;
2338       GtkWindow *parent;
2339
2340       text =
2341           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2342           group);
2343       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2344       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2345               text, FALSE, NULL) == REMOVE_DIALOG_RESPONSE_DELETE)
2346         {
2347           EmpathyIndividualManager *manager =
2348               empathy_individual_manager_dup_singleton ();
2349           empathy_individual_manager_remove_group (manager, group);
2350           g_object_unref (G_OBJECT (manager));
2351         }
2352
2353       g_free (text);
2354     }
2355
2356   g_free (group);
2357 }
2358
2359 GtkWidget *
2360 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2361 {
2362   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2363   gchar *group;
2364   GtkWidget *menu;
2365   GtkWidget *item;
2366   GtkWidget *image;
2367   gboolean is_fake_group;
2368
2369   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2370
2371   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2372               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2373     return NULL;
2374
2375   group = empathy_individual_view_dup_selected_group (view, &is_fake_group);
2376   if (!group || is_fake_group)
2377     {
2378       /* We can't alter fake groups */
2379       g_free (group);
2380       return NULL;
2381     }
2382
2383   menu = gtk_menu_new ();
2384
2385   /* TODO: implement
2386      if (priv->view_features &
2387      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2388      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2389      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2390      gtk_widget_show (item);
2391      g_signal_connect (item, "activate",
2392      G_CALLBACK (individual_view_group_rename_activate_cb),
2393      view);
2394      }
2395    */
2396
2397   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2398     {
2399       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2400       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2401           GTK_ICON_SIZE_MENU);
2402       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2403       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2404       gtk_widget_show (item);
2405       g_signal_connect (item, "activate",
2406           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2407     }
2408
2409   g_free (group);
2410
2411   return menu;
2412 }
2413
2414 static void
2415 got_avatar (GObject *source_object,
2416     GAsyncResult *result,
2417     gpointer user_data)
2418 {
2419   FolksIndividual *individual = FOLKS_INDIVIDUAL (source_object);
2420   EmpathyIndividualView *view = user_data;
2421   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2422   GdkPixbuf *avatar;
2423   EmpathyIndividualManager *manager;
2424   gchar *text;
2425   GtkWindow *parent;
2426   GeeSet *personas;
2427   guint persona_count = 0;
2428   gboolean can_block;
2429   GError *error = NULL;
2430   gint res;
2431
2432   avatar = empathy_pixbuf_avatar_from_individual_scaled_finish (individual,
2433       result, &error);
2434
2435   if (error != NULL)
2436     {
2437       DEBUG ("Could not get avatar: %s", error->message);
2438       g_error_free (error);
2439     }
2440
2441   /* We couldn't retrieve the avatar, but that isn't a fatal error,
2442    * so we still display the remove dialog. */
2443
2444   personas = folks_individual_get_personas (individual);
2445
2446   if (priv->show_uninteresting)
2447     {
2448       persona_count = gee_collection_get_size (GEE_COLLECTION (personas));
2449     }
2450   else
2451     {
2452       GeeIterator *iter;
2453
2454       iter = gee_iterable_iterator (GEE_ITERABLE (personas));
2455       while (persona_count < 2 && gee_iterator_next (iter))
2456         {
2457           FolksPersona *persona = gee_iterator_get (iter);
2458
2459           if (empathy_folks_persona_is_interesting (persona))
2460             persona_count++;
2461
2462           g_clear_object (&persona);
2463         }
2464       g_clear_object (&iter);
2465     }
2466
2467   /* If we have more than one TpfPersona, display a different message
2468    * ensuring the user knows that *all* of the meta-contacts' personas will
2469    * be removed. */
2470
2471   if (persona_count < 2)
2472     {
2473       /* Not a meta-contact */
2474       text =
2475           g_strdup_printf (
2476               _("Do you really want to remove the contact '%s'?"),
2477               folks_alias_details_get_alias (
2478                   FOLKS_ALIAS_DETAILS (individual)));
2479     }
2480   else
2481     {
2482       /* Meta-contact */
2483       text =
2484           g_strdup_printf (
2485               _("Do you really want to remove the linked contact '%s'? "
2486                 "Note that this will remove all the contacts which make up "
2487                 "this linked contact."),
2488               folks_alias_details_get_alias (
2489                   FOLKS_ALIAS_DETAILS (individual)));
2490     }
2491
2492
2493   manager = empathy_individual_manager_dup_singleton ();
2494   can_block = empathy_individual_manager_supports_blocking (manager,
2495       individual);
2496   parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2497   res = individual_view_remove_dialog_show (parent, _("Removing contact"),
2498           text, can_block, avatar);
2499
2500   if (res == REMOVE_DIALOG_RESPONSE_DELETE ||
2501       res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2502     {
2503       gboolean abusive;
2504
2505       if (res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2506         {
2507           if (!empathy_block_individual_dialog_show (parent, individual,
2508                 avatar, &abusive))
2509             goto finally;
2510
2511           empathy_individual_manager_set_blocked (manager, individual,
2512               TRUE, abusive);
2513         }
2514
2515       empathy_individual_manager_remove (manager, individual, "");
2516     }
2517
2518  finally:
2519   g_free (text);
2520   g_object_unref (manager);
2521 }
2522
2523 static void
2524 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2525     EmpathyIndividualView *view)
2526 {
2527   FolksIndividual *individual;
2528
2529   individual = empathy_individual_view_dup_selected (view);
2530
2531   if (individual != NULL)
2532     {
2533       empathy_pixbuf_avatar_from_individual_scaled_async (individual,
2534           48, 48, NULL, got_avatar, view);
2535       g_object_unref (individual);
2536     }
2537 }
2538
2539 GtkWidget *
2540 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2541 {
2542   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2543   FolksIndividual *individual;
2544   GtkWidget *menu = NULL;
2545   GtkWidget *item;
2546   GtkWidget *image;
2547   gboolean can_remove = FALSE;
2548   GeeSet *personas;
2549   GeeIterator *iter;
2550
2551   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2552
2553   if (priv->individual_features == EMPATHY_INDIVIDUAL_FEATURE_NONE)
2554     /* No need to create a context menu */
2555     return NULL;
2556
2557   individual = empathy_individual_view_dup_selected (view);
2558   if (individual == NULL)
2559     return NULL;
2560
2561   if (!empathy_folks_individual_contains_contact (individual))
2562     goto out;
2563
2564   /* If any of the Individual's personas can be removed, add an option to
2565    * remove. This will act as a best-effort option. If any Personas cannot be
2566    * removed from the server, then this option will just be inactive upon
2567    * subsequent menu openings */
2568   personas = folks_individual_get_personas (individual);
2569   iter = gee_iterable_iterator (GEE_ITERABLE (personas));
2570   while (!can_remove && gee_iterator_next (iter))
2571     {
2572       FolksPersona *persona = gee_iterator_get (iter);
2573       FolksPersonaStore *store = folks_persona_get_store (persona);
2574       FolksMaybeBool maybe_can_remove =
2575           folks_persona_store_get_can_remove_personas (store);
2576
2577       if (maybe_can_remove == FOLKS_MAYBE_BOOL_TRUE)
2578         can_remove = TRUE;
2579
2580       g_clear_object (&persona);
2581     }
2582   g_clear_object (&iter);
2583
2584   menu = empathy_individual_menu_new (individual, priv->individual_features,
2585       priv->store);
2586
2587   /* Remove contact */
2588   if ((priv->view_features &
2589       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE) &&
2590       can_remove)
2591     {
2592       /* create the menu if required, or just add a separator */
2593       if (menu == NULL)
2594         menu = gtk_menu_new ();
2595       else
2596         {
2597           item = gtk_separator_menu_item_new ();
2598           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2599           gtk_widget_show (item);
2600         }
2601
2602       /* Remove */
2603       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2604       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2605           GTK_ICON_SIZE_MENU);
2606       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2607       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2608       gtk_widget_show (item);
2609       g_signal_connect (item, "activate",
2610           G_CALLBACK (individual_view_remove_activate_cb), view);
2611     }
2612
2613 out:
2614   g_object_unref (individual);
2615
2616   return menu;
2617 }
2618
2619 void
2620 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2621     EmpathyLiveSearch *search)
2622 {
2623   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2624
2625   /* remove old handlers if old search was not null */
2626   if (priv->search_widget != NULL)
2627     {
2628       g_signal_handlers_disconnect_by_func (view,
2629           individual_view_start_search_cb, NULL);
2630
2631       g_signal_handlers_disconnect_by_func (priv->search_widget,
2632           individual_view_search_text_notify_cb, view);
2633       g_signal_handlers_disconnect_by_func (priv->search_widget,
2634           individual_view_search_activate_cb, view);
2635       g_signal_handlers_disconnect_by_func (priv->search_widget,
2636           individual_view_search_key_navigation_cb, view);
2637       g_signal_handlers_disconnect_by_func (priv->search_widget,
2638           individual_view_search_hide_cb, view);
2639       g_signal_handlers_disconnect_by_func (priv->search_widget,
2640           individual_view_search_show_cb, view);
2641       g_object_unref (priv->search_widget);
2642       priv->search_widget = NULL;
2643     }
2644
2645   /* connect handlers if new search is not null */
2646   if (search != NULL)
2647     {
2648       priv->search_widget = g_object_ref (search);
2649
2650       g_signal_connect (view, "start-interactive-search",
2651           G_CALLBACK (individual_view_start_search_cb), NULL);
2652
2653       g_signal_connect (priv->search_widget, "notify::text",
2654           G_CALLBACK (individual_view_search_text_notify_cb), view);
2655       g_signal_connect (priv->search_widget, "activate",
2656           G_CALLBACK (individual_view_search_activate_cb), view);
2657       g_signal_connect (priv->search_widget, "key-navigation",
2658           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2659       g_signal_connect (priv->search_widget, "hide",
2660           G_CALLBACK (individual_view_search_hide_cb), view);
2661       g_signal_connect (priv->search_widget, "show",
2662           G_CALLBACK (individual_view_search_show_cb), view);
2663     }
2664 }
2665
2666 gboolean
2667 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2668 {
2669   EmpathyIndividualViewPriv *priv;
2670
2671   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2672
2673   priv = GET_PRIV (self);
2674
2675   return (priv->search_widget != NULL &&
2676           gtk_widget_get_visible (priv->search_widget));
2677 }
2678
2679 gboolean
2680 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2681 {
2682   EmpathyIndividualViewPriv *priv;
2683
2684   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2685
2686   priv = GET_PRIV (self);
2687
2688   return priv->show_offline;
2689 }
2690
2691 void
2692 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2693     gboolean show_offline)
2694 {
2695   EmpathyIndividualViewPriv *priv;
2696
2697   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2698
2699   priv = GET_PRIV (self);
2700
2701   priv->show_offline = show_offline;
2702
2703   g_object_notify (G_OBJECT (self), "show-offline");
2704   gtk_tree_model_filter_refilter (priv->filter);
2705 }
2706
2707 gboolean
2708 empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
2709 {
2710   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2711
2712   return GET_PRIV (self)->show_untrusted;
2713 }
2714
2715 void
2716 empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
2717     gboolean show_untrusted)
2718 {
2719   EmpathyIndividualViewPriv *priv;
2720
2721   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2722
2723   priv = GET_PRIV (self);
2724
2725   priv->show_untrusted = show_untrusted;
2726
2727   g_object_notify (G_OBJECT (self), "show-untrusted");
2728   gtk_tree_model_filter_refilter (priv->filter);
2729 }
2730
2731 EmpathyIndividualStore *
2732 empathy_individual_view_get_store (EmpathyIndividualView *self)
2733 {
2734   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2735
2736   return GET_PRIV (self)->store;
2737 }
2738
2739 void
2740 empathy_individual_view_set_store (EmpathyIndividualView *self,
2741     EmpathyIndividualStore *store)
2742 {
2743   EmpathyIndividualViewPriv *priv;
2744
2745   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2746   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2747
2748   priv = GET_PRIV (self);
2749
2750   /* Destroy the old filter and remove the old store */
2751   if (priv->store != NULL)
2752     {
2753       g_signal_handlers_disconnect_by_func (priv->filter,
2754           individual_view_row_has_child_toggled_cb, self);
2755
2756       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2757     }
2758
2759   tp_clear_object (&priv->filter);
2760   tp_clear_object (&priv->store);
2761
2762   /* Set the new store */
2763   priv->store = store;
2764
2765   if (store != NULL)
2766     {
2767       g_object_ref (store);
2768
2769       /* Create a new filter */
2770       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2771           GTK_TREE_MODEL (priv->store), NULL));
2772       gtk_tree_model_filter_set_visible_func (priv->filter,
2773           individual_view_filter_visible_func, self, NULL);
2774
2775       g_signal_connect (priv->filter, "row-has-child-toggled",
2776           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2777       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2778           GTK_TREE_MODEL (priv->filter));
2779     }
2780 }
2781
2782 void
2783 empathy_individual_view_start_search (EmpathyIndividualView *self)
2784 {
2785   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2786
2787   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2788   g_return_if_fail (priv->search_widget != NULL);
2789
2790   if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
2791     gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
2792   else
2793     gtk_widget_show (GTK_WIDGET (priv->search_widget));
2794 }
2795
2796 void
2797 empathy_individual_view_set_custom_filter (EmpathyIndividualView *self,
2798     GtkTreeModelFilterVisibleFunc filter,
2799     gpointer data)
2800 {
2801   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2802
2803   priv->custom_filter = filter;
2804   priv->custom_filter_data = data;
2805 }
2806
2807 void
2808 empathy_individual_view_refilter (EmpathyIndividualView *self)
2809 {
2810   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2811
2812   gtk_tree_model_filter_refilter (priv->filter);
2813 }
2814
2815 void
2816 empathy_individual_view_select_first (EmpathyIndividualView *self)
2817 {
2818   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2819   GtkTreeIter iter;
2820
2821   gtk_tree_model_filter_refilter (priv->filter);
2822
2823   if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->filter), &iter))
2824     {
2825       GtkTreeSelection *selection = gtk_tree_view_get_selection (
2826           GTK_TREE_VIEW (self));
2827
2828       gtk_tree_selection_select_iter (selection, &iter);
2829     }
2830 }
2831
2832 void
2833 empathy_individual_view_set_show_uninteresting (EmpathyIndividualView *self,
2834     gboolean show_uninteresting)
2835 {
2836   EmpathyIndividualViewPriv *priv;
2837
2838   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2839
2840   priv = GET_PRIV (self);
2841
2842   priv->show_uninteresting = show_uninteresting;
2843
2844   g_object_notify (G_OBJECT (self), "show-uninteresting");
2845   gtk_tree_model_filter_refilter (priv->filter);
2846 }