]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
disconnect the activate cb before detaching the menu
[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
1685   /* We're only giving the visibility wrt filtering here, not things like
1686    * presence. */
1687   if (priv->show_untrusted == FALSE &&
1688       folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
1689     {
1690       return FALSE;
1691     }
1692
1693   if (is_searching == FALSE)
1694     return (priv->show_offline || is_online);
1695
1696   /* check alias name */
1697   str = folks_aliasable_get_alias (FOLKS_ALIASABLE (individual));
1698
1699   if (empathy_live_search_match (live, str))
1700     return TRUE;
1701
1702   /* check contact id, remove the @server.com part */
1703   personas = folks_individual_get_personas (individual);
1704   for (l = personas; l; l = l->next)
1705     {
1706       const gchar *p;
1707       gchar *dup_str = NULL;
1708       gboolean visible;
1709
1710       if (!TPF_IS_PERSONA (l->data))
1711         continue;
1712
1713       str = folks_persona_get_display_id (l->data);
1714       p = strstr (str, "@");
1715       if (p != NULL)
1716         str = dup_str = g_strndup (str, p - str);
1717
1718       visible = empathy_live_search_match (live, str);
1719       g_free (dup_str);
1720       if (visible)
1721         return TRUE;
1722     }
1723
1724   /* FIXME: Add more rules here, we could check phone numbers in
1725    * contact's vCard for example. */
1726
1727   return FALSE;
1728 }
1729
1730 static gboolean
1731 individual_view_filter_visible_func (GtkTreeModel *model,
1732     GtkTreeIter *iter,
1733     gpointer user_data)
1734 {
1735   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1736   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1737   FolksIndividual *individual = NULL;
1738   gboolean is_group, is_separator, valid;
1739   GtkTreeIter child_iter;
1740   gboolean visible, is_online;
1741   gboolean is_searching = TRUE;
1742
1743   if (priv->search_widget == NULL ||
1744       !gtk_widget_get_visible (priv->search_widget))
1745      is_searching = FALSE;
1746
1747   gtk_tree_model_get (model, iter,
1748       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1749       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1750       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1751       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1752       -1);
1753
1754   if (individual != NULL)
1755     {
1756       visible = individual_view_is_visible_individual (self, individual,
1757           is_online, is_searching);
1758
1759       g_object_unref (individual);
1760
1761       /* FIXME: Work around bgo#626552/bgo#621076 */
1762       if (visible == TRUE)
1763         {
1764           GtkTreePath *path = gtk_tree_model_get_path (model, iter);
1765           individual_view_verify_group_visibility (self, path);
1766           gtk_tree_path_free (path);
1767         }
1768
1769       return visible;
1770     }
1771
1772   if (is_separator)
1773     return TRUE;
1774
1775   /* Not a contact, not a separator, must be a group */
1776   g_return_val_if_fail (is_group, FALSE);
1777
1778   /* only show groups which are not empty */
1779   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1780        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1781     {
1782       gtk_tree_model_get (model, &child_iter,
1783         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1784         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1785         -1);
1786
1787       if (individual == NULL)
1788         continue;
1789
1790       visible = individual_view_is_visible_individual (self, individual,
1791           is_online, is_searching);
1792       g_object_unref (individual);
1793
1794       /* show group if it has at least one visible contact in it */
1795       if (visible == TRUE)
1796         return TRUE;
1797     }
1798
1799   return FALSE;
1800 }
1801
1802 static void
1803 individual_view_constructed (GObject *object)
1804 {
1805   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1806   GtkCellRenderer *cell;
1807   GtkTreeViewColumn *col;
1808   guint i;
1809
1810   /* Setup view */
1811   g_object_set (view,
1812       "headers-visible", FALSE,
1813       "show-expanders", FALSE,
1814       NULL);
1815
1816   col = gtk_tree_view_column_new ();
1817
1818   /* State */
1819   cell = gtk_cell_renderer_pixbuf_new ();
1820   gtk_tree_view_column_pack_start (col, cell, FALSE);
1821   gtk_tree_view_column_set_cell_data_func (col, cell,
1822       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1823       view, NULL);
1824
1825   g_object_set (cell,
1826       "xpad", 5,
1827       "ypad", 1,
1828       "visible", FALSE,
1829       NULL);
1830
1831   /* Group icon */
1832   cell = gtk_cell_renderer_pixbuf_new ();
1833   gtk_tree_view_column_pack_start (col, cell, FALSE);
1834   gtk_tree_view_column_set_cell_data_func (col, cell,
1835       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1836       view, NULL);
1837
1838   g_object_set (cell,
1839       "xpad", 0,
1840       "ypad", 0,
1841       "visible", FALSE,
1842       "width", 16,
1843       "height", 16,
1844       NULL);
1845
1846   /* Name */
1847   cell = empathy_cell_renderer_text_new ();
1848   gtk_tree_view_column_pack_start (col, cell, TRUE);
1849   gtk_tree_view_column_set_cell_data_func (col, cell,
1850       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1851
1852   gtk_tree_view_column_add_attribute (col, cell,
1853       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1854   gtk_tree_view_column_add_attribute (col, cell,
1855       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1856   gtk_tree_view_column_add_attribute (col, cell,
1857       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1858   gtk_tree_view_column_add_attribute (col, cell,
1859       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1860   gtk_tree_view_column_add_attribute (col, cell,
1861       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1862   gtk_tree_view_column_add_attribute (col, cell,
1863       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1864   gtk_tree_view_column_add_attribute (col, cell,
1865       "client-types", EMPATHY_INDIVIDUAL_STORE_COL_CLIENT_TYPES);
1866
1867   /* Audio Call Icon */
1868   cell = empathy_cell_renderer_activatable_new ();
1869   gtk_tree_view_column_pack_start (col, cell, FALSE);
1870   gtk_tree_view_column_set_cell_data_func (col, cell,
1871       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1872       view, NULL);
1873
1874   g_object_set (cell, "visible", FALSE, NULL);
1875
1876   g_signal_connect (cell, "path-activated",
1877       G_CALLBACK (individual_view_call_activated_cb), view);
1878
1879   /* Avatar */
1880   cell = gtk_cell_renderer_pixbuf_new ();
1881   gtk_tree_view_column_pack_start (col, cell, FALSE);
1882   gtk_tree_view_column_set_cell_data_func (col, cell,
1883       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1884       view, NULL);
1885
1886   g_object_set (cell,
1887       "xpad", 0,
1888       "ypad", 0,
1889       "visible", FALSE,
1890       "width", 32,
1891       "height", 32,
1892       NULL);
1893
1894   /* Expander */
1895   cell = empathy_cell_renderer_expander_new ();
1896   gtk_tree_view_column_pack_end (col, cell, FALSE);
1897   gtk_tree_view_column_set_cell_data_func (col, cell,
1898       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1899       view, NULL);
1900
1901   /* Actually add the column now we have added all cell renderers */
1902   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1903
1904   /* Drag & Drop. */
1905   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1906     {
1907       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1908     }
1909
1910   for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i)
1911     {
1912       drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1913           FALSE);
1914     }
1915 }
1916
1917 static void
1918 individual_view_set_view_features (EmpathyIndividualView *view,
1919     EmpathyIndividualFeatureFlags features)
1920 {
1921   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1922   gboolean has_tooltip;
1923
1924   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1925
1926   priv->view_features = features;
1927
1928   /* Setting reorderable is a hack that gets us row previews as drag icons
1929      for free.  We override all the drag handlers.  It's tricky to get the
1930      position of the drag icon right in drag_begin.  GtkTreeView has special
1931      voodoo for it, so we let it do the voodoo that he do (but only if dragging
1932      is enabled).
1933    */
1934   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
1935       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
1936
1937   /* Update DnD source/dest */
1938   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
1939     {
1940       gtk_drag_source_set (GTK_WIDGET (view),
1941           GDK_BUTTON1_MASK,
1942           drag_types_source,
1943           G_N_ELEMENTS (drag_types_source),
1944           GDK_ACTION_MOVE | GDK_ACTION_COPY);
1945     }
1946   else
1947     {
1948       gtk_drag_source_unset (GTK_WIDGET (view));
1949
1950     }
1951
1952   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
1953     {
1954       gtk_drag_dest_set (GTK_WIDGET (view),
1955           GTK_DEST_DEFAULT_ALL,
1956           drag_types_dest,
1957           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
1958     }
1959   else
1960     {
1961       /* FIXME: URI could still be droped depending on FT feature */
1962       gtk_drag_dest_unset (GTK_WIDGET (view));
1963     }
1964
1965   /* Update has-tooltip */
1966   has_tooltip =
1967       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
1968   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1969 }
1970
1971 static void
1972 individual_view_dispose (GObject *object)
1973 {
1974   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1975   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1976
1977   tp_clear_object (&priv->store);
1978   tp_clear_object (&priv->filter);
1979   tp_clear_object (&priv->tooltip_widget);
1980
1981   empathy_individual_view_set_live_search (view, NULL);
1982
1983   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
1984 }
1985
1986 static void
1987 individual_view_finalize (GObject *object)
1988 {
1989   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1990
1991   if (priv->expand_groups_idle_handler != 0)
1992     g_source_remove (priv->expand_groups_idle_handler);
1993   g_hash_table_destroy (priv->expand_groups);
1994
1995   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
1996 }
1997
1998 static void
1999 individual_view_get_property (GObject *object,
2000     guint param_id,
2001     GValue *value,
2002     GParamSpec *pspec)
2003 {
2004   EmpathyIndividualViewPriv *priv;
2005
2006   priv = GET_PRIV (object);
2007
2008   switch (param_id)
2009     {
2010     case PROP_STORE:
2011       g_value_set_object (value, priv->store);
2012       break;
2013     case PROP_VIEW_FEATURES:
2014       g_value_set_flags (value, priv->view_features);
2015       break;
2016     case PROP_INDIVIDUAL_FEATURES:
2017       g_value_set_flags (value, priv->individual_features);
2018       break;
2019     case PROP_SHOW_OFFLINE:
2020       g_value_set_boolean (value, priv->show_offline);
2021       break;
2022     case PROP_SHOW_UNTRUSTED:
2023       g_value_set_boolean (value, priv->show_untrusted);
2024       break;
2025     default:
2026       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2027       break;
2028     };
2029 }
2030
2031 static void
2032 individual_view_set_property (GObject *object,
2033     guint param_id,
2034     const GValue *value,
2035     GParamSpec *pspec)
2036 {
2037   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2038   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2039
2040   switch (param_id)
2041     {
2042     case PROP_STORE:
2043       empathy_individual_view_set_store (view, g_value_get_object (value));
2044       break;
2045     case PROP_VIEW_FEATURES:
2046       individual_view_set_view_features (view, g_value_get_flags (value));
2047       break;
2048     case PROP_INDIVIDUAL_FEATURES:
2049       priv->individual_features = g_value_get_flags (value);
2050       break;
2051     case PROP_SHOW_OFFLINE:
2052       empathy_individual_view_set_show_offline (view,
2053           g_value_get_boolean (value));
2054       break;
2055     case PROP_SHOW_UNTRUSTED:
2056       empathy_individual_view_set_show_untrusted (view,
2057           g_value_get_boolean (value));
2058       break;
2059     default:
2060       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2061       break;
2062     };
2063 }
2064
2065 static void
2066 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
2067 {
2068   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2069   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2070   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
2071
2072   object_class->constructed = individual_view_constructed;
2073   object_class->dispose = individual_view_dispose;
2074   object_class->finalize = individual_view_finalize;
2075   object_class->get_property = individual_view_get_property;
2076   object_class->set_property = individual_view_set_property;
2077
2078   widget_class->drag_data_received = individual_view_drag_data_received;
2079   widget_class->drag_drop = individual_view_drag_drop;
2080   widget_class->drag_begin = individual_view_drag_begin;
2081   widget_class->drag_data_get = individual_view_drag_data_get;
2082   widget_class->drag_end = individual_view_drag_end;
2083   widget_class->drag_motion = individual_view_drag_motion;
2084
2085   /* We use the class method to let user of this widget to connect to
2086    * the signal and stop emission of the signal so the default handler
2087    * won't be called. */
2088   tree_view_class->row_activated = individual_view_row_activated;
2089
2090   klass->drag_individual_received = real_drag_individual_received_cb;
2091
2092   signals[DRAG_INDIVIDUAL_RECEIVED] =
2093       g_signal_new ("drag-individual-received",
2094       G_OBJECT_CLASS_TYPE (klass),
2095       G_SIGNAL_RUN_LAST,
2096       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
2097       NULL, NULL,
2098       _empathy_gtk_marshal_VOID__UINT_OBJECT_STRING_STRING,
2099       G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
2100       G_TYPE_STRING, G_TYPE_STRING);
2101
2102   signals[DRAG_PERSONA_RECEIVED] =
2103       g_signal_new ("drag-persona-received",
2104       G_OBJECT_CLASS_TYPE (klass),
2105       G_SIGNAL_RUN_LAST,
2106       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
2107       NULL, NULL,
2108       _empathy_gtk_marshal_BOOLEAN__UINT_OBJECT_OBJECT,
2109       G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
2110
2111   g_object_class_install_property (object_class,
2112       PROP_STORE,
2113       g_param_spec_object ("store",
2114           "The store of the view",
2115           "The store of the view",
2116           EMPATHY_TYPE_INDIVIDUAL_STORE,
2117           G_PARAM_READWRITE));
2118   g_object_class_install_property (object_class,
2119       PROP_VIEW_FEATURES,
2120       g_param_spec_flags ("view-features",
2121           "Features of the view",
2122           "Flags for all enabled features",
2123           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
2124           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
2125   g_object_class_install_property (object_class,
2126       PROP_INDIVIDUAL_FEATURES,
2127       g_param_spec_flags ("individual-features",
2128           "Features of the individual menu",
2129           "Flags for all enabled features for the menu",
2130           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
2131           EMPATHY_INDIVIDUAL_FEATURE_NONE, G_PARAM_READWRITE));
2132   g_object_class_install_property (object_class,
2133       PROP_SHOW_OFFLINE,
2134       g_param_spec_boolean ("show-offline",
2135           "Show Offline",
2136           "Whether contact list should display "
2137           "offline contacts", FALSE, G_PARAM_READWRITE));
2138   g_object_class_install_property (object_class,
2139       PROP_SHOW_UNTRUSTED,
2140       g_param_spec_boolean ("show-untrusted",
2141           "Show Untrusted Individuals",
2142           "Whether the view should display untrusted individuals; "
2143           "those who could not be who they say they are.",
2144           TRUE, G_PARAM_READWRITE));
2145
2146   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
2147 }
2148
2149 static void
2150 empathy_individual_view_init (EmpathyIndividualView *view)
2151 {
2152   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
2153       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
2154
2155   view->priv = priv;
2156
2157   priv->show_untrusted = TRUE;
2158
2159   /* Get saved group states. */
2160   empathy_contact_groups_get_all ();
2161
2162   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
2163       (GDestroyNotify) g_free, NULL);
2164
2165   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
2166       empathy_individual_store_row_separator_func, NULL, NULL);
2167
2168   /* Connect to tree view signals rather than override. */
2169   g_signal_connect (view, "button-press-event",
2170       G_CALLBACK (individual_view_button_press_event_cb), NULL);
2171   g_signal_connect (view, "key-press-event",
2172       G_CALLBACK (individual_view_key_press_event_cb), NULL);
2173   g_signal_connect (view, "row-expanded",
2174       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2175       GINT_TO_POINTER (TRUE));
2176   g_signal_connect (view, "row-collapsed",
2177       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2178       GINT_TO_POINTER (FALSE));
2179   g_signal_connect (view, "query-tooltip",
2180       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
2181 }
2182
2183 EmpathyIndividualView *
2184 empathy_individual_view_new (EmpathyIndividualStore *store,
2185     EmpathyIndividualViewFeatureFlags view_features,
2186     EmpathyIndividualFeatureFlags individual_features)
2187 {
2188   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
2189
2190   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
2191       "store", store,
2192       "individual-features", individual_features,
2193       "view-features", view_features, NULL);
2194 }
2195
2196 FolksIndividual *
2197 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
2198 {
2199   EmpathyIndividualViewPriv *priv;
2200   GtkTreeSelection *selection;
2201   GtkTreeIter iter;
2202   GtkTreeModel *model;
2203   FolksIndividual *individual;
2204
2205   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2206
2207   priv = GET_PRIV (view);
2208
2209   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2210   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2211     return NULL;
2212
2213   gtk_tree_model_get (model, &iter,
2214       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
2215
2216   return individual;
2217 }
2218
2219 static gchar *
2220 empathy_individual_view_dup_selected_group (EmpathyIndividualView *view,
2221     gboolean *is_fake_group)
2222 {
2223   EmpathyIndividualViewPriv *priv;
2224   GtkTreeSelection *selection;
2225   GtkTreeIter iter;
2226   GtkTreeModel *model;
2227   gboolean is_group;
2228   gchar *name;
2229   gboolean fake;
2230
2231   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2232
2233   priv = GET_PRIV (view);
2234
2235   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2236   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2237     return NULL;
2238
2239   gtk_tree_model_get (model, &iter,
2240       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2241       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2242       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2243
2244   if (!is_group)
2245     {
2246       g_free (name);
2247       return NULL;
2248     }
2249
2250   if (is_fake_group != NULL)
2251     *is_fake_group = fake;
2252
2253   return name;
2254 }
2255
2256 static gboolean
2257 individual_view_remove_dialog_show (GtkWindow *parent,
2258     const gchar *message,
2259     const gchar *secondary_text)
2260 {
2261   GtkWidget *dialog;
2262   gboolean res;
2263
2264   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2265       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2266   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2267       GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
2268       GTK_STOCK_DELETE, GTK_RESPONSE_YES, NULL);
2269   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2270       "%s", secondary_text);
2271
2272   gtk_widget_show (dialog);
2273
2274   res = gtk_dialog_run (GTK_DIALOG (dialog));
2275   gtk_widget_destroy (dialog);
2276
2277   return (res == GTK_RESPONSE_YES);
2278 }
2279
2280 static void
2281 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2282     EmpathyIndividualView *view)
2283 {
2284   gchar *group;
2285
2286   group = empathy_individual_view_dup_selected_group (view, NULL);
2287   if (group != NULL)
2288     {
2289       gchar *text;
2290       GtkWindow *parent;
2291
2292       text =
2293           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2294           group);
2295       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2296       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2297               text))
2298         {
2299           EmpathyIndividualManager *manager =
2300               empathy_individual_manager_dup_singleton ();
2301           empathy_individual_manager_remove_group (manager, group);
2302           g_object_unref (G_OBJECT (manager));
2303         }
2304
2305       g_free (text);
2306     }
2307
2308   g_free (group);
2309 }
2310
2311 GtkWidget *
2312 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2313 {
2314   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2315   gchar *group;
2316   GtkWidget *menu;
2317   GtkWidget *item;
2318   GtkWidget *image;
2319   gboolean is_fake_group;
2320
2321   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2322
2323   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2324               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2325     return NULL;
2326
2327   group = empathy_individual_view_dup_selected_group (view, &is_fake_group);
2328   if (!group || is_fake_group)
2329     {
2330       /* We can't alter fake groups */
2331       g_free (group);
2332       return NULL;
2333     }
2334
2335   menu = gtk_menu_new ();
2336
2337   /* TODO: implement
2338      if (priv->view_features &
2339      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2340      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2341      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2342      gtk_widget_show (item);
2343      g_signal_connect (item, "activate",
2344      G_CALLBACK (individual_view_group_rename_activate_cb),
2345      view);
2346      }
2347    */
2348
2349   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2350     {
2351       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2352       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2353           GTK_ICON_SIZE_MENU);
2354       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2355       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2356       gtk_widget_show (item);
2357       g_signal_connect (item, "activate",
2358           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2359     }
2360
2361   g_free (group);
2362
2363   return menu;
2364 }
2365
2366 static void
2367 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2368     EmpathyIndividualView *view)
2369 {
2370   FolksIndividual *individual;
2371
2372   individual = empathy_individual_view_dup_selected (view);
2373
2374   if (individual != NULL)
2375     {
2376       gchar *text;
2377       GtkWindow *parent;
2378       GList *l, *personas;
2379       guint persona_count = 0;
2380
2381       personas = folks_individual_get_personas (individual);
2382
2383       /* If we have more than one TpfPersona, display a different message
2384        * ensuring the user knows that *all* of the meta-contacts' personas will
2385        * be removed. */
2386       for (l = personas; l != NULL; l = l->next)
2387         {
2388           if (!TPF_IS_PERSONA (l->data))
2389             continue;
2390
2391           persona_count++;
2392           if (persona_count >= 2)
2393             break;
2394         }
2395
2396       if (persona_count < 2)
2397         {
2398           /* Not a meta-contact */
2399           text =
2400               g_strdup_printf (
2401                   _("Do you really want to remove the contact '%s'?"),
2402                   folks_aliasable_get_alias (FOLKS_ALIASABLE (individual)));
2403         }
2404       else
2405         {
2406           /* Meta-contact */
2407           text =
2408               g_strdup_printf (
2409                   _("Do you really want to remove the linked contact '%s'? "
2410                     "Note that this will remove all the contacts which make up "
2411                     "this linked contact."),
2412                   folks_aliasable_get_alias (FOLKS_ALIASABLE (individual)));
2413         }
2414
2415       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2416
2417       if (individual_view_remove_dialog_show (parent, _("Removing contact"),
2418               text))
2419         {
2420           EmpathyIndividualManager *manager;
2421
2422           manager = empathy_individual_manager_dup_singleton ();
2423           empathy_individual_manager_remove (manager, individual, "");
2424           g_object_unref (G_OBJECT (manager));
2425         }
2426
2427       g_free (text);
2428       g_object_unref (individual);
2429     }
2430 }
2431
2432 static void
2433 individual_menu_link_contacts_activated_cb (EmpathyIndividualMenu *menu,
2434     EmpathyLinkingDialog *linking_dialog,
2435     EmpathyIndividualView *self)
2436 {
2437   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2438   EmpathyIndividualLinker *linker;
2439
2440   linker = empathy_linking_dialog_get_individual_linker (linking_dialog);
2441   empathy_individual_linker_set_search_text (linker,
2442       empathy_live_search_get_text (EMPATHY_LIVE_SEARCH (priv->search_widget)));
2443 }
2444
2445 GtkWidget *
2446 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2447 {
2448   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2449   FolksIndividual *individual;
2450   GtkWidget *menu = NULL;
2451   GtkWidget *item;
2452   GtkWidget *image;
2453   gboolean can_remove = FALSE;
2454   GList *l;
2455
2456   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2457
2458   individual = empathy_individual_view_dup_selected (view);
2459   if (individual == NULL)
2460     return NULL;
2461
2462   /* If any of the Individual's personas can be removed, add an option to
2463    * remove. This will act as a best-effort option. If any Personas cannot be
2464    * removed from the server, then this option will just be inactive upon
2465    * subsequent menu openings */
2466   for (l = folks_individual_get_personas (individual); l != NULL; l = l->next)
2467     {
2468       FolksPersona *persona = FOLKS_PERSONA (l->data);
2469       FolksPersonaStore *store = folks_persona_get_store (persona);
2470       FolksMaybeBool maybe_can_remove =
2471           folks_persona_store_get_can_remove_personas (store);
2472
2473       if (maybe_can_remove == FOLKS_MAYBE_BOOL_TRUE)
2474         {
2475           can_remove = TRUE;
2476           break;
2477         }
2478     }
2479
2480   menu = empathy_individual_menu_new (individual, priv->individual_features);
2481
2482   /* Remove contact */
2483   if ((priv->view_features &
2484       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE) &&
2485       can_remove)
2486     {
2487       /* create the menu if required, or just add a separator */
2488       if (menu == NULL)
2489         menu = gtk_menu_new ();
2490       else
2491         {
2492           item = gtk_separator_menu_item_new ();
2493           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2494           gtk_widget_show (item);
2495         }
2496
2497       /* Remove */
2498       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2499       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2500           GTK_ICON_SIZE_MENU);
2501       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2502       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2503       gtk_widget_show (item);
2504       g_signal_connect (item, "activate",
2505           G_CALLBACK (individual_view_remove_activate_cb), view);
2506     }
2507
2508   /* Connect to EmpathyIndividualMenu::link-contacts-activated so that we can
2509    * set the live search text on the new linking dialogue to be the same as
2510    * our own. */
2511   g_signal_connect (menu, "link-contacts-activated",
2512       (GCallback) individual_menu_link_contacts_activated_cb, view);
2513
2514   g_object_unref (individual);
2515
2516   return menu;
2517 }
2518
2519 void
2520 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2521     EmpathyLiveSearch *search)
2522 {
2523   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2524
2525   /* remove old handlers if old search was not null */
2526   if (priv->search_widget != NULL)
2527     {
2528       g_signal_handlers_disconnect_by_func (view,
2529           individual_view_start_search_cb, NULL);
2530
2531       g_signal_handlers_disconnect_by_func (priv->search_widget,
2532           individual_view_search_text_notify_cb, view);
2533       g_signal_handlers_disconnect_by_func (priv->search_widget,
2534           individual_view_search_activate_cb, view);
2535       g_signal_handlers_disconnect_by_func (priv->search_widget,
2536           individual_view_search_key_navigation_cb, view);
2537       g_signal_handlers_disconnect_by_func (priv->search_widget,
2538           individual_view_search_hide_cb, view);
2539       g_signal_handlers_disconnect_by_func (priv->search_widget,
2540           individual_view_search_show_cb, view);
2541       g_object_unref (priv->search_widget);
2542       priv->search_widget = NULL;
2543     }
2544
2545   /* connect handlers if new search is not null */
2546   if (search != NULL)
2547     {
2548       priv->search_widget = g_object_ref (search);
2549
2550       g_signal_connect (view, "start-interactive-search",
2551           G_CALLBACK (individual_view_start_search_cb), NULL);
2552
2553       g_signal_connect (priv->search_widget, "notify::text",
2554           G_CALLBACK (individual_view_search_text_notify_cb), view);
2555       g_signal_connect (priv->search_widget, "activate",
2556           G_CALLBACK (individual_view_search_activate_cb), view);
2557       g_signal_connect (priv->search_widget, "key-navigation",
2558           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2559       g_signal_connect (priv->search_widget, "hide",
2560           G_CALLBACK (individual_view_search_hide_cb), view);
2561       g_signal_connect (priv->search_widget, "show",
2562           G_CALLBACK (individual_view_search_show_cb), view);
2563     }
2564 }
2565
2566 gboolean
2567 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2568 {
2569   EmpathyIndividualViewPriv *priv;
2570
2571   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2572
2573   priv = GET_PRIV (self);
2574
2575   return (priv->search_widget != NULL &&
2576           gtk_widget_get_visible (priv->search_widget));
2577 }
2578
2579 gboolean
2580 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2581 {
2582   EmpathyIndividualViewPriv *priv;
2583
2584   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2585
2586   priv = GET_PRIV (self);
2587
2588   return priv->show_offline;
2589 }
2590
2591 void
2592 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2593     gboolean show_offline)
2594 {
2595   EmpathyIndividualViewPriv *priv;
2596
2597   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2598
2599   priv = GET_PRIV (self);
2600
2601   priv->show_offline = show_offline;
2602
2603   g_object_notify (G_OBJECT (self), "show-offline");
2604   gtk_tree_model_filter_refilter (priv->filter);
2605 }
2606
2607 gboolean
2608 empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
2609 {
2610   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2611
2612   return GET_PRIV (self)->show_untrusted;
2613 }
2614
2615 void
2616 empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
2617     gboolean show_untrusted)
2618 {
2619   EmpathyIndividualViewPriv *priv;
2620
2621   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2622
2623   priv = GET_PRIV (self);
2624
2625   priv->show_untrusted = show_untrusted;
2626
2627   g_object_notify (G_OBJECT (self), "show-untrusted");
2628   gtk_tree_model_filter_refilter (priv->filter);
2629 }
2630
2631 EmpathyIndividualStore *
2632 empathy_individual_view_get_store (EmpathyIndividualView *self)
2633 {
2634   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2635
2636   return GET_PRIV (self)->store;
2637 }
2638
2639 void
2640 empathy_individual_view_set_store (EmpathyIndividualView *self,
2641     EmpathyIndividualStore *store)
2642 {
2643   EmpathyIndividualViewPriv *priv;
2644
2645   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2646   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2647
2648   priv = GET_PRIV (self);
2649
2650   /* Destroy the old filter and remove the old store */
2651   if (priv->store != NULL)
2652     {
2653       g_signal_handlers_disconnect_by_func (priv->store,
2654           individual_view_store_row_changed_cb, self);
2655       g_signal_handlers_disconnect_by_func (priv->store,
2656           individual_view_store_row_deleted_cb, self);
2657
2658       g_signal_handlers_disconnect_by_func (priv->filter,
2659           individual_view_row_has_child_toggled_cb, self);
2660
2661       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2662     }
2663
2664   tp_clear_object (&priv->filter);
2665   tp_clear_object (&priv->store);
2666
2667   /* Set the new store */
2668   priv->store = store;
2669
2670   if (store != NULL)
2671     {
2672       g_object_ref (store);
2673
2674       /* Create a new filter */
2675       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2676           GTK_TREE_MODEL (priv->store), NULL));
2677       gtk_tree_model_filter_set_visible_func (priv->filter,
2678           individual_view_filter_visible_func, self, NULL);
2679
2680       g_signal_connect (priv->filter, "row-has-child-toggled",
2681           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2682       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2683           GTK_TREE_MODEL (priv->filter));
2684
2685       tp_g_signal_connect_object (priv->store, "row-changed",
2686           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2687       tp_g_signal_connect_object (priv->store, "row-inserted",
2688           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2689       tp_g_signal_connect_object (priv->store, "row-deleted",
2690           G_CALLBACK (individual_view_store_row_deleted_cb), self, 0);
2691     }
2692 }
2693
2694 void
2695 empathy_individual_view_start_search (EmpathyIndividualView *self)
2696 {
2697   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2698
2699   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2700   g_return_if_fail (priv->search_widget != NULL);
2701
2702   if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
2703     gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
2704   else
2705     gtk_widget_show (GTK_WIDGET (priv->search_widget));
2706 }