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