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