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