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