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