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