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