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