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