]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
Only unref the EmpathyIndividualView after emptying the group hash table
[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   /* Empty the table of groups to expand/contract, since it may contain groups
1507    * which no longer exist in the tree view. This can happen after going
1508    * offline, for example. */
1509   g_hash_table_remove_all (priv->expand_groups);
1510   priv->expand_groups_idle_handler = 0;
1511   g_object_unref (self);
1512
1513   return FALSE;
1514 }
1515
1516 static void
1517 individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
1518     GtkTreePath *path,
1519     GtkTreeIter *iter,
1520     EmpathyIndividualView *view)
1521 {
1522   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1523   gboolean should_expand, is_group = FALSE;
1524   gchar *name = NULL;
1525   gpointer will_expand;
1526
1527   gtk_tree_model_get (model, iter,
1528       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1529       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1530       -1);
1531
1532   if (!is_group || EMP_STR_EMPTY (name))
1533     {
1534       g_free (name);
1535       return;
1536     }
1537
1538   should_expand = (priv->view_features &
1539           EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1540       (priv->search_widget != NULL &&
1541           gtk_widget_get_visible (priv->search_widget)) ||
1542       empathy_contact_group_get_expanded (name);
1543
1544   /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1545    * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
1546    * a hash table, and expand or contract them as appropriate all at once in
1547    * an idle handler which iterates over all the group rows. */
1548   if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1549       &will_expand) == FALSE ||
1550       GPOINTER_TO_INT (will_expand) != should_expand)
1551     {
1552       g_hash_table_insert (priv->expand_groups, g_strdup (name),
1553           GINT_TO_POINTER (should_expand));
1554
1555       if (priv->expand_groups_idle_handler == 0)
1556         {
1557           priv->expand_groups_idle_handler =
1558               g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
1559                   g_object_ref (view));
1560         }
1561     }
1562
1563   g_free (name);
1564 }
1565
1566 /* FIXME: This is a workaround for bgo#621076 */
1567 static void
1568 individual_view_verify_group_visibility (EmpathyIndividualView *view,
1569     GtkTreePath *path)
1570 {
1571   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1572   GtkTreeModel *model;
1573   GtkTreePath *parent_path;
1574   GtkTreeIter parent_iter;
1575
1576   if (gtk_tree_path_get_depth (path) < 2)
1577     return;
1578
1579   /* A group row is visible if and only if at least one if its child is visible.
1580    * So when a row is inserted/deleted/changed in the base model, that could
1581    * modify the visibility of its parent in the filter model.
1582   */
1583
1584   model = GTK_TREE_MODEL (priv->store);
1585   parent_path = gtk_tree_path_copy (path);
1586   gtk_tree_path_up (parent_path);
1587   if (gtk_tree_model_get_iter (model, &parent_iter, parent_path))
1588     {
1589       /* This tells the filter to verify the visibility of that row, and
1590        * show/hide it if necessary */
1591       gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
1592               parent_path, &parent_iter);
1593     }
1594   gtk_tree_path_free (parent_path);
1595 }
1596
1597 static void
1598 individual_view_store_row_changed_cb (GtkTreeModel *model,
1599   GtkTreePath *path,
1600   GtkTreeIter *iter,
1601   EmpathyIndividualView *view)
1602 {
1603   individual_view_verify_group_visibility (view, path);
1604 }
1605
1606 static void
1607 individual_view_store_row_deleted_cb (GtkTreeModel *model,
1608   GtkTreePath *path,
1609   EmpathyIndividualView *view)
1610 {
1611   individual_view_verify_group_visibility (view, path);
1612 }
1613
1614 static gboolean
1615 individual_view_is_visible_individual (EmpathyIndividualView *self,
1616     FolksIndividual *individual,
1617     gboolean is_online,
1618     gboolean is_searching)
1619 {
1620   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1621   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
1622   const gchar *str;
1623   GList *personas, *l;
1624
1625   /* We're only giving the visibility wrt filtering here, not things like
1626    * presence. */
1627   if (priv->show_untrusted == FALSE &&
1628       folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
1629     {
1630       return FALSE;
1631     }
1632
1633   if (is_searching == FALSE)
1634     return (priv->show_offline || is_online);
1635
1636   /* check alias name */
1637   str = folks_individual_get_alias (individual);
1638
1639   if (empathy_live_search_match (live, str))
1640     return TRUE;
1641
1642   /* check contact id, remove the @server.com part */
1643   personas = folks_individual_get_personas (individual);
1644   for (l = personas; l; l = l->next)
1645     {
1646       const gchar *p;
1647       gchar *dup_str = NULL;
1648       gboolean visible;
1649
1650       if (!TPF_IS_PERSONA (l->data))
1651         continue;
1652
1653       str = folks_persona_get_display_id (l->data);
1654       p = strstr (str, "@");
1655       if (p != NULL)
1656         str = dup_str = g_strndup (str, p - str);
1657
1658       visible = empathy_live_search_match (live, str);
1659       g_free (dup_str);
1660       if (visible)
1661         return TRUE;
1662     }
1663
1664   /* FIXME: Add more rules here, we could check phone numbers in
1665    * contact's vCard for example. */
1666
1667   return FALSE;
1668 }
1669
1670 static gboolean
1671 individual_view_filter_visible_func (GtkTreeModel *model,
1672     GtkTreeIter *iter,
1673     gpointer user_data)
1674 {
1675   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1676   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1677   FolksIndividual *individual = NULL;
1678   gboolean is_group, is_separator, valid;
1679   GtkTreeIter child_iter;
1680   gboolean visible, is_online;
1681   gboolean is_searching = TRUE;
1682
1683   if (priv->search_widget == NULL ||
1684       !gtk_widget_get_visible (priv->search_widget))
1685      is_searching = FALSE;
1686
1687   gtk_tree_model_get (model, iter,
1688       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1689       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1690       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1691       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1692       -1);
1693
1694   if (individual != NULL)
1695     {
1696       visible = individual_view_is_visible_individual (self, individual,
1697           is_online, is_searching);
1698
1699       g_object_unref (individual);
1700
1701       /* FIXME: Work around bgo#626552/bgo#621076 */
1702       if (visible == TRUE)
1703         {
1704           GtkTreePath *path = gtk_tree_model_get_path (model, iter);
1705           individual_view_verify_group_visibility (self, path);
1706           gtk_tree_path_free (path);
1707         }
1708
1709       return visible;
1710     }
1711
1712   if (is_separator)
1713     return TRUE;
1714
1715   /* Not a contact, not a separator, must be a group */
1716   g_return_val_if_fail (is_group, FALSE);
1717
1718   /* only show groups which are not empty */
1719   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1720        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1721     {
1722       gtk_tree_model_get (model, &child_iter,
1723         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1724         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1725         -1);
1726
1727       if (individual == NULL)
1728         continue;
1729
1730       visible = individual_view_is_visible_individual (self, individual,
1731           is_online, is_searching);
1732       g_object_unref (individual);
1733
1734       /* show group if it has at least one visible contact in it */
1735       if (visible == TRUE)
1736         return TRUE;
1737     }
1738
1739   return FALSE;
1740 }
1741
1742 static void
1743 individual_view_constructed (GObject *object)
1744 {
1745   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1746   GtkCellRenderer *cell;
1747   GtkTreeViewColumn *col;
1748   guint i;
1749
1750   /* Setup view */
1751   g_object_set (view,
1752       "headers-visible", FALSE,
1753       "show-expanders", FALSE,
1754       NULL);
1755
1756   col = gtk_tree_view_column_new ();
1757
1758   /* State */
1759   cell = gtk_cell_renderer_pixbuf_new ();
1760   gtk_tree_view_column_pack_start (col, cell, FALSE);
1761   gtk_tree_view_column_set_cell_data_func (col, cell,
1762       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1763       view, NULL);
1764
1765   g_object_set (cell,
1766       "xpad", 5,
1767       "ypad", 1,
1768       "visible", FALSE,
1769       NULL);
1770
1771   /* Group icon */
1772   cell = gtk_cell_renderer_pixbuf_new ();
1773   gtk_tree_view_column_pack_start (col, cell, FALSE);
1774   gtk_tree_view_column_set_cell_data_func (col, cell,
1775       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1776       view, NULL);
1777
1778   g_object_set (cell,
1779       "xpad", 0,
1780       "ypad", 0,
1781       "visible", FALSE,
1782       "width", 16,
1783       "height", 16,
1784       NULL);
1785
1786   /* Name */
1787   cell = empathy_cell_renderer_text_new ();
1788   gtk_tree_view_column_pack_start (col, cell, TRUE);
1789   gtk_tree_view_column_set_cell_data_func (col, cell,
1790       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1791
1792   gtk_tree_view_column_add_attribute (col, cell,
1793       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1794   gtk_tree_view_column_add_attribute (col, cell,
1795       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1796   gtk_tree_view_column_add_attribute (col, cell,
1797       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1798   gtk_tree_view_column_add_attribute (col, cell,
1799       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1800   gtk_tree_view_column_add_attribute (col, cell,
1801       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1802   gtk_tree_view_column_add_attribute (col, cell,
1803       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1804
1805   /* Audio Call Icon */
1806   cell = empathy_cell_renderer_activatable_new ();
1807   gtk_tree_view_column_pack_start (col, cell, FALSE);
1808   gtk_tree_view_column_set_cell_data_func (col, cell,
1809       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1810       view, NULL);
1811
1812   g_object_set (cell, "visible", FALSE, NULL);
1813
1814   g_signal_connect (cell, "path-activated",
1815       G_CALLBACK (individual_view_call_activated_cb), view);
1816
1817   /* Avatar */
1818   cell = gtk_cell_renderer_pixbuf_new ();
1819   gtk_tree_view_column_pack_start (col, cell, FALSE);
1820   gtk_tree_view_column_set_cell_data_func (col, cell,
1821       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1822       view, NULL);
1823
1824   g_object_set (cell,
1825       "xpad", 0,
1826       "ypad", 0,
1827       "visible", FALSE,
1828       "width", 32,
1829       "height", 32,
1830       NULL);
1831
1832   /* Expander */
1833   cell = empathy_cell_renderer_expander_new ();
1834   gtk_tree_view_column_pack_end (col, cell, FALSE);
1835   gtk_tree_view_column_set_cell_data_func (col, cell,
1836       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1837       view, NULL);
1838
1839   /* Actually add the column now we have added all cell renderers */
1840   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1841
1842   /* Drag & Drop. */
1843   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1844     {
1845       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1846     }
1847
1848   for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i)
1849     {
1850       drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1851           FALSE);
1852     }
1853 }
1854
1855 static void
1856 individual_view_set_view_features (EmpathyIndividualView *view,
1857     EmpathyIndividualFeatureFlags features)
1858 {
1859   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1860   gboolean has_tooltip;
1861
1862   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1863
1864   priv->view_features = features;
1865
1866   /* Setting reorderable is a hack that gets us row previews as drag icons
1867      for free.  We override all the drag handlers.  It's tricky to get the
1868      position of the drag icon right in drag_begin.  GtkTreeView has special
1869      voodoo for it, so we let it do the voodoo that he do (but only if dragging
1870      is enabled).
1871    */
1872   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
1873       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
1874
1875   /* Update DnD source/dest */
1876   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
1877     {
1878       gtk_drag_source_set (GTK_WIDGET (view),
1879           GDK_BUTTON1_MASK,
1880           drag_types_source,
1881           G_N_ELEMENTS (drag_types_source),
1882           GDK_ACTION_MOVE | GDK_ACTION_COPY);
1883     }
1884   else
1885     {
1886       gtk_drag_source_unset (GTK_WIDGET (view));
1887
1888     }
1889
1890   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
1891     {
1892       gtk_drag_dest_set (GTK_WIDGET (view),
1893           GTK_DEST_DEFAULT_ALL,
1894           drag_types_dest,
1895           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
1896     }
1897   else
1898     {
1899       /* FIXME: URI could still be droped depending on FT feature */
1900       gtk_drag_dest_unset (GTK_WIDGET (view));
1901     }
1902
1903   /* Update has-tooltip */
1904   has_tooltip =
1905       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
1906   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1907 }
1908
1909 static void
1910 individual_view_dispose (GObject *object)
1911 {
1912   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1913   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1914
1915   tp_clear_object (&priv->store);
1916   tp_clear_object (&priv->filter);
1917   tp_clear_pointer (&priv->tooltip_widget, gtk_widget_destroy);
1918
1919   empathy_individual_view_set_live_search (view, NULL);
1920
1921   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
1922 }
1923
1924 static void
1925 individual_view_finalize (GObject *object)
1926 {
1927   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1928
1929   if (priv->expand_groups_idle_handler != 0)
1930     g_source_remove (priv->expand_groups_idle_handler);
1931   g_hash_table_destroy (priv->expand_groups);
1932
1933   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
1934 }
1935
1936 static void
1937 individual_view_get_property (GObject *object,
1938     guint param_id,
1939     GValue *value,
1940     GParamSpec *pspec)
1941 {
1942   EmpathyIndividualViewPriv *priv;
1943
1944   priv = GET_PRIV (object);
1945
1946   switch (param_id)
1947     {
1948     case PROP_STORE:
1949       g_value_set_object (value, priv->store);
1950       break;
1951     case PROP_VIEW_FEATURES:
1952       g_value_set_flags (value, priv->view_features);
1953       break;
1954     case PROP_INDIVIDUAL_FEATURES:
1955       g_value_set_flags (value, priv->individual_features);
1956       break;
1957     case PROP_SHOW_OFFLINE:
1958       g_value_set_boolean (value, priv->show_offline);
1959       break;
1960     case PROP_SHOW_UNTRUSTED:
1961       g_value_set_boolean (value, priv->show_untrusted);
1962       break;
1963     default:
1964       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1965       break;
1966     };
1967 }
1968
1969 static void
1970 individual_view_set_property (GObject *object,
1971     guint param_id,
1972     const GValue *value,
1973     GParamSpec *pspec)
1974 {
1975   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1976   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1977
1978   switch (param_id)
1979     {
1980     case PROP_STORE:
1981       empathy_individual_view_set_store (view, g_value_get_object (value));
1982       break;
1983     case PROP_VIEW_FEATURES:
1984       individual_view_set_view_features (view, g_value_get_flags (value));
1985       break;
1986     case PROP_INDIVIDUAL_FEATURES:
1987       priv->individual_features = g_value_get_flags (value);
1988       break;
1989     case PROP_SHOW_OFFLINE:
1990       empathy_individual_view_set_show_offline (view,
1991           g_value_get_boolean (value));
1992       break;
1993     case PROP_SHOW_UNTRUSTED:
1994       empathy_individual_view_set_show_untrusted (view,
1995           g_value_get_boolean (value));
1996       break;
1997     default:
1998       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1999       break;
2000     };
2001 }
2002
2003 static void
2004 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
2005 {
2006   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2007   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2008   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
2009
2010   object_class->constructed = individual_view_constructed;
2011   object_class->dispose = individual_view_dispose;
2012   object_class->finalize = individual_view_finalize;
2013   object_class->get_property = individual_view_get_property;
2014   object_class->set_property = individual_view_set_property;
2015
2016   widget_class->drag_data_received = individual_view_drag_data_received;
2017   widget_class->drag_drop = individual_view_drag_drop;
2018   widget_class->drag_begin = individual_view_drag_begin;
2019   widget_class->drag_data_get = individual_view_drag_data_get;
2020   widget_class->drag_end = individual_view_drag_end;
2021   widget_class->drag_motion = individual_view_drag_motion;
2022
2023   /* We use the class method to let user of this widget to connect to
2024    * the signal and stop emission of the signal so the default handler
2025    * won't be called. */
2026   tree_view_class->row_activated = individual_view_row_activated;
2027
2028   klass->drag_individual_received = real_drag_individual_received_cb;
2029
2030   signals[DRAG_INDIVIDUAL_RECEIVED] =
2031       g_signal_new ("drag-individual-received",
2032       G_OBJECT_CLASS_TYPE (klass),
2033       G_SIGNAL_RUN_LAST,
2034       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
2035       NULL, NULL,
2036       _empathy_gtk_marshal_VOID__UINT_OBJECT_STRING_STRING,
2037       G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
2038       G_TYPE_STRING, G_TYPE_STRING);
2039
2040   signals[DRAG_PERSONA_RECEIVED] =
2041       g_signal_new ("drag-persona-received",
2042       G_OBJECT_CLASS_TYPE (klass),
2043       G_SIGNAL_RUN_LAST,
2044       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
2045       NULL, NULL,
2046       _empathy_gtk_marshal_BOOLEAN__UINT_OBJECT_OBJECT,
2047       G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
2048
2049   g_object_class_install_property (object_class,
2050       PROP_STORE,
2051       g_param_spec_object ("store",
2052           "The store of the view",
2053           "The store of the view",
2054           EMPATHY_TYPE_INDIVIDUAL_STORE,
2055           G_PARAM_READWRITE));
2056   g_object_class_install_property (object_class,
2057       PROP_VIEW_FEATURES,
2058       g_param_spec_flags ("view-features",
2059           "Features of the view",
2060           "Flags for all enabled features",
2061           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
2062           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
2063   g_object_class_install_property (object_class,
2064       PROP_INDIVIDUAL_FEATURES,
2065       g_param_spec_flags ("individual-features",
2066           "Features of the individual menu",
2067           "Flags for all enabled features for the menu",
2068           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
2069           EMPATHY_INDIVIDUAL_FEATURE_NONE, G_PARAM_READWRITE));
2070   g_object_class_install_property (object_class,
2071       PROP_SHOW_OFFLINE,
2072       g_param_spec_boolean ("show-offline",
2073           "Show Offline",
2074           "Whether contact list should display "
2075           "offline contacts", FALSE, G_PARAM_READWRITE));
2076   g_object_class_install_property (object_class,
2077       PROP_SHOW_UNTRUSTED,
2078       g_param_spec_boolean ("show-untrusted",
2079           "Show Untrusted Individuals",
2080           "Whether the view should display untrusted individuals; "
2081           "those who could not be who they say they are.",
2082           TRUE, G_PARAM_READWRITE));
2083
2084   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
2085 }
2086
2087 static void
2088 empathy_individual_view_init (EmpathyIndividualView *view)
2089 {
2090   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
2091       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
2092
2093   view->priv = priv;
2094
2095   priv->show_untrusted = TRUE;
2096
2097   /* Get saved group states. */
2098   empathy_contact_groups_get_all ();
2099
2100   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
2101       (GDestroyNotify) g_free, NULL);
2102
2103   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
2104       empathy_individual_store_row_separator_func, NULL, NULL);
2105
2106   /* Connect to tree view signals rather than override. */
2107   g_signal_connect (view, "button-press-event",
2108       G_CALLBACK (individual_view_button_press_event_cb), NULL);
2109   g_signal_connect (view, "key-press-event",
2110       G_CALLBACK (individual_view_key_press_event_cb), NULL);
2111   g_signal_connect (view, "row-expanded",
2112       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2113       GINT_TO_POINTER (TRUE));
2114   g_signal_connect (view, "row-collapsed",
2115       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2116       GINT_TO_POINTER (FALSE));
2117   g_signal_connect (view, "query-tooltip",
2118       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
2119 }
2120
2121 EmpathyIndividualView *
2122 empathy_individual_view_new (EmpathyIndividualStore *store,
2123     EmpathyIndividualViewFeatureFlags view_features,
2124     EmpathyIndividualFeatureFlags individual_features)
2125 {
2126   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
2127
2128   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
2129       "store", store,
2130       "individual-features", individual_features,
2131       "view-features", view_features, NULL);
2132 }
2133
2134 FolksIndividual *
2135 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
2136 {
2137   EmpathyIndividualViewPriv *priv;
2138   GtkTreeSelection *selection;
2139   GtkTreeIter iter;
2140   GtkTreeModel *model;
2141   FolksIndividual *individual;
2142
2143   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2144
2145   priv = GET_PRIV (view);
2146
2147   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2148   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2149     return NULL;
2150
2151   gtk_tree_model_get (model, &iter,
2152       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
2153
2154   return individual;
2155 }
2156
2157 EmpathyIndividualManagerFlags
2158 empathy_individual_view_get_flags (EmpathyIndividualView *view)
2159 {
2160   EmpathyIndividualViewPriv *priv;
2161   GtkTreeSelection *selection;
2162   GtkTreeIter iter;
2163   GtkTreeModel *model;
2164   EmpathyIndividualFeatureFlags flags;
2165
2166   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), 0);
2167
2168   priv = GET_PRIV (view);
2169
2170   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2171   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2172     return 0;
2173
2174   gtk_tree_model_get (model, &iter,
2175       EMPATHY_INDIVIDUAL_STORE_COL_FLAGS, &flags, -1);
2176
2177   return flags;
2178 }
2179
2180 gchar *
2181 empathy_individual_view_get_selected_group (EmpathyIndividualView *view,
2182     gboolean *is_fake_group)
2183 {
2184   EmpathyIndividualViewPriv *priv;
2185   GtkTreeSelection *selection;
2186   GtkTreeIter iter;
2187   GtkTreeModel *model;
2188   gboolean is_group;
2189   gchar *name;
2190   gboolean fake;
2191
2192   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2193
2194   priv = GET_PRIV (view);
2195
2196   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2197   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2198     return NULL;
2199
2200   gtk_tree_model_get (model, &iter,
2201       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2202       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2203       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2204
2205   if (!is_group)
2206     {
2207       g_free (name);
2208       return NULL;
2209     }
2210
2211   if (is_fake_group != NULL)
2212     *is_fake_group = fake;
2213
2214   return name;
2215 }
2216
2217 static gboolean
2218 individual_view_remove_dialog_show (GtkWindow *parent,
2219     const gchar *message,
2220     const gchar *secondary_text)
2221 {
2222   GtkWidget *dialog;
2223   gboolean res;
2224
2225   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2226       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2227   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2228       GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
2229       GTK_STOCK_DELETE, GTK_RESPONSE_YES, NULL);
2230   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2231       "%s", secondary_text);
2232
2233   gtk_widget_show (dialog);
2234
2235   res = gtk_dialog_run (GTK_DIALOG (dialog));
2236   gtk_widget_destroy (dialog);
2237
2238   return (res == GTK_RESPONSE_YES);
2239 }
2240
2241 static void
2242 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2243     EmpathyIndividualView *view)
2244 {
2245   gchar *group;
2246
2247   group = empathy_individual_view_get_selected_group (view, NULL);
2248   if (group != NULL)
2249     {
2250       gchar *text;
2251       GtkWindow *parent;
2252
2253       text =
2254           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2255           group);
2256       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2257       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2258               text))
2259         {
2260           EmpathyIndividualManager *manager =
2261               empathy_individual_manager_dup_singleton ();
2262           empathy_individual_manager_remove_group (manager, group);
2263           g_object_unref (G_OBJECT (manager));
2264         }
2265
2266       g_free (text);
2267     }
2268
2269   g_free (group);
2270 }
2271
2272 GtkWidget *
2273 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2274 {
2275   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2276   gchar *group;
2277   GtkWidget *menu;
2278   GtkWidget *item;
2279   GtkWidget *image;
2280   gboolean is_fake_group;
2281
2282   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2283
2284   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2285               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2286     return NULL;
2287
2288   group = empathy_individual_view_get_selected_group (view, &is_fake_group);
2289   if (!group || is_fake_group)
2290     {
2291       /* We can't alter fake groups */
2292       return NULL;
2293     }
2294
2295   menu = gtk_menu_new ();
2296
2297   /* TODO: implement
2298      if (priv->view_features &
2299      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2300      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2301      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2302      gtk_widget_show (item);
2303      g_signal_connect (item, "activate",
2304      G_CALLBACK (individual_view_group_rename_activate_cb),
2305      view);
2306      }
2307    */
2308
2309   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2310     {
2311       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2312       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2313           GTK_ICON_SIZE_MENU);
2314       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2315       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2316       gtk_widget_show (item);
2317       g_signal_connect (item, "activate",
2318           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2319     }
2320
2321   g_free (group);
2322
2323   return menu;
2324 }
2325
2326 static void
2327 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2328     EmpathyIndividualView *view)
2329 {
2330   FolksIndividual *individual;
2331
2332   individual = empathy_individual_view_dup_selected (view);
2333
2334   if (individual != NULL)
2335     {
2336       gchar *text;
2337       GtkWindow *parent;
2338
2339       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2340       text =
2341           g_strdup_printf (_
2342           ("Do you really want to remove the contact '%s'?"),
2343           folks_individual_get_alias (individual));
2344       if (individual_view_remove_dialog_show (parent, _("Removing contact"),
2345               text))
2346         {
2347           EmpathyIndividualManager *manager;
2348
2349           manager = empathy_individual_manager_dup_singleton ();
2350           empathy_individual_manager_remove (manager, individual, "");
2351           g_object_unref (G_OBJECT (manager));
2352         }
2353
2354       g_free (text);
2355       g_object_unref (individual);
2356     }
2357 }
2358
2359 GtkWidget *
2360 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2361 {
2362   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2363   FolksIndividual *individual;
2364   GtkWidget *menu = NULL;
2365   GtkWidget *item;
2366   GtkWidget *image;
2367   EmpathyIndividualManagerFlags flags;
2368
2369   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2370
2371   individual = empathy_individual_view_dup_selected (view);
2372   if (individual == NULL)
2373     return NULL;
2374
2375   flags = empathy_individual_view_get_flags (view);
2376
2377   menu = empathy_individual_menu_new (individual, priv->individual_features);
2378
2379   /* Remove contact */
2380   if (priv->view_features &
2381       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE &&
2382       flags & EMPATHY_INDIVIDUAL_MANAGER_CAN_REMOVE)
2383     {
2384
2385       /* create the menu if required, or just add a separator */
2386       if (menu == NULL)
2387         menu = gtk_menu_new ();
2388       else
2389         {
2390           item = gtk_separator_menu_item_new ();
2391           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2392           gtk_widget_show (item);
2393         }
2394
2395       /* Remove */
2396       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2397       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2398           GTK_ICON_SIZE_MENU);
2399       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2400       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2401       gtk_widget_show (item);
2402       g_signal_connect (item, "activate",
2403           G_CALLBACK (individual_view_remove_activate_cb), view);
2404     }
2405
2406   g_object_unref (individual);
2407
2408   return menu;
2409 }
2410
2411 void
2412 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2413     EmpathyLiveSearch *search)
2414 {
2415   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2416
2417   /* remove old handlers if old search was not null */
2418   if (priv->search_widget != NULL)
2419     {
2420       g_signal_handlers_disconnect_by_func (view,
2421           individual_view_start_search_cb, NULL);
2422
2423       g_signal_handlers_disconnect_by_func (priv->search_widget,
2424           individual_view_search_text_notify_cb, view);
2425       g_signal_handlers_disconnect_by_func (priv->search_widget,
2426           individual_view_search_activate_cb, view);
2427       g_signal_handlers_disconnect_by_func (priv->search_widget,
2428           individual_view_search_key_navigation_cb, view);
2429       g_signal_handlers_disconnect_by_func (priv->search_widget,
2430           individual_view_search_hide_cb, view);
2431       g_signal_handlers_disconnect_by_func (priv->search_widget,
2432           individual_view_search_show_cb, view);
2433       g_object_unref (priv->search_widget);
2434       priv->search_widget = NULL;
2435     }
2436
2437   /* connect handlers if new search is not null */
2438   if (search != NULL)
2439     {
2440       priv->search_widget = g_object_ref (search);
2441
2442       g_signal_connect (view, "start-interactive-search",
2443           G_CALLBACK (individual_view_start_search_cb), NULL);
2444
2445       g_signal_connect (priv->search_widget, "notify::text",
2446           G_CALLBACK (individual_view_search_text_notify_cb), view);
2447       g_signal_connect (priv->search_widget, "activate",
2448           G_CALLBACK (individual_view_search_activate_cb), view);
2449       g_signal_connect (priv->search_widget, "key-navigation",
2450           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2451       g_signal_connect (priv->search_widget, "hide",
2452           G_CALLBACK (individual_view_search_hide_cb), view);
2453       g_signal_connect (priv->search_widget, "show",
2454           G_CALLBACK (individual_view_search_show_cb), view);
2455     }
2456 }
2457
2458 gboolean
2459 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2460 {
2461   EmpathyIndividualViewPriv *priv;
2462
2463   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2464
2465   priv = GET_PRIV (self);
2466
2467   return (priv->search_widget != NULL &&
2468           gtk_widget_get_visible (priv->search_widget));
2469 }
2470
2471 gboolean
2472 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2473 {
2474   EmpathyIndividualViewPriv *priv;
2475
2476   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2477
2478   priv = GET_PRIV (self);
2479
2480   return priv->show_offline;
2481 }
2482
2483 void
2484 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2485     gboolean show_offline)
2486 {
2487   EmpathyIndividualViewPriv *priv;
2488
2489   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2490
2491   priv = GET_PRIV (self);
2492
2493   priv->show_offline = show_offline;
2494
2495   g_object_notify (G_OBJECT (self), "show-offline");
2496   gtk_tree_model_filter_refilter (priv->filter);
2497 }
2498
2499 gboolean
2500 empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
2501 {
2502   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2503
2504   return GET_PRIV (self)->show_untrusted;
2505 }
2506
2507 void
2508 empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
2509     gboolean show_untrusted)
2510 {
2511   EmpathyIndividualViewPriv *priv;
2512
2513   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2514
2515   priv = GET_PRIV (self);
2516
2517   priv->show_untrusted = show_untrusted;
2518
2519   g_object_notify (G_OBJECT (self), "show-untrusted");
2520   gtk_tree_model_filter_refilter (priv->filter);
2521 }
2522
2523 EmpathyIndividualStore *
2524 empathy_individual_view_get_store (EmpathyIndividualView *self)
2525 {
2526   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2527
2528   return GET_PRIV (self)->store;
2529 }
2530
2531 void
2532 empathy_individual_view_set_store (EmpathyIndividualView *self,
2533     EmpathyIndividualStore *store)
2534 {
2535   EmpathyIndividualViewPriv *priv;
2536
2537   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2538   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2539
2540   priv = GET_PRIV (self);
2541
2542   /* Destroy the old filter and remove the old store */
2543   if (priv->store != NULL)
2544     {
2545       g_signal_handlers_disconnect_by_func (priv->store,
2546           individual_view_store_row_changed_cb, self);
2547       g_signal_handlers_disconnect_by_func (priv->store,
2548           individual_view_store_row_deleted_cb, self);
2549
2550       g_signal_handlers_disconnect_by_func (priv->filter,
2551           individual_view_row_has_child_toggled_cb, self);
2552
2553       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2554     }
2555
2556   tp_clear_object (&priv->filter);
2557   tp_clear_object (&priv->store);
2558
2559   /* Set the new store */
2560   priv->store = store;
2561
2562   if (store != NULL)
2563     {
2564       g_object_ref (store);
2565
2566       /* Create a new filter */
2567       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2568           GTK_TREE_MODEL (priv->store), NULL));
2569       gtk_tree_model_filter_set_visible_func (priv->filter,
2570           individual_view_filter_visible_func, self, NULL);
2571
2572       g_signal_connect (priv->filter, "row-has-child-toggled",
2573           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2574       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2575           GTK_TREE_MODEL (priv->filter));
2576
2577       tp_g_signal_connect_object (priv->store, "row-changed",
2578           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2579       tp_g_signal_connect_object (priv->store, "row-inserted",
2580           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2581       tp_g_signal_connect_object (priv->store, "row-deleted",
2582           G_CALLBACK (individual_view_store_row_deleted_cb), self, 0);
2583     }
2584 }