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