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