]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
individual-view: use tp_g_signal_connect_object() to connect the destroy sig
[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
897 static gboolean
898 individual_view_drag_drop (GtkWidget *widget,
899     GdkDragContext *drag_context,
900     gint x,
901     gint y,
902     guint time_)
903 {
904   return FALSE;
905 }
906
907 typedef struct
908 {
909   EmpathyIndividualView *view;
910   guint button;
911   guint32 time;
912 } MenuPopupData;
913
914 static void
915 menu_deactivate_cb (GtkMenuShell *menushell,
916     gpointer user_data)
917 {
918   /* FIXME: we shouldn't have to disconnec the signal (bgo #641327) */
919   g_signal_handlers_disconnect_by_func (menushell,
920       menu_deactivate_cb, user_data);
921
922   gtk_menu_detach (GTK_MENU (menushell));
923 }
924
925 static gboolean
926 individual_view_popup_menu_idle_cb (gpointer user_data)
927 {
928   MenuPopupData *data = user_data;
929   GtkWidget *menu;
930
931   menu = empathy_individual_view_get_individual_menu (data->view);
932   if (menu == NULL)
933     menu = empathy_individual_view_get_group_menu (data->view);
934
935   if (menu != NULL)
936     {
937       gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (data->view),
938           NULL);
939       gtk_widget_show (menu);
940       gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, data->button,
941           data->time);
942
943       /* menu is initially unowned but gtk_menu_attach_to_widget() taked its
944        * floating ref. We can either wait that the treeview releases its ref
945        * when it will be destroyed (when leaving Empathy) or explicitely
946        * detach the menu when it's not displayed any more.
947        * We go for the latter as we don't want to keep useless menus in memory
948        * during the whole lifetime of Empathy. */
949       g_signal_connect (menu, "deactivate", G_CALLBACK (menu_deactivate_cb),
950           NULL);
951     }
952
953   g_slice_free (MenuPopupData, data);
954
955   return FALSE;
956 }
957
958 static gboolean
959 individual_view_button_press_event_cb (EmpathyIndividualView *view,
960     GdkEventButton *event,
961     gpointer user_data)
962 {
963   if (event->button == 3)
964     {
965       MenuPopupData *data;
966
967       data = g_slice_new (MenuPopupData);
968       data->view = view;
969       data->button = event->button;
970       data->time = event->time;
971       g_idle_add (individual_view_popup_menu_idle_cb, data);
972     }
973
974   return FALSE;
975 }
976
977 static gboolean
978 individual_view_key_press_event_cb (EmpathyIndividualView *view,
979     GdkEventKey *event,
980     gpointer user_data)
981 {
982   if (event->keyval == GDK_KEY_Menu)
983     {
984       MenuPopupData *data;
985
986       data = g_slice_new (MenuPopupData);
987       data->view = view;
988       data->button = 0;
989       data->time = event->time;
990       g_idle_add (individual_view_popup_menu_idle_cb, data);
991     } else if (event->keyval == GDK_KEY_F2) {
992         FolksIndividual *individual;
993
994         g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), FALSE);
995
996         individual = empathy_individual_view_dup_selected (view);
997         if (individual == NULL)
998             return FALSE;
999
1000         empathy_individual_edit_dialog_show (individual, NULL);
1001
1002         g_object_unref (individual);
1003     }
1004
1005   return FALSE;
1006 }
1007
1008 static void
1009 individual_view_row_activated (GtkTreeView *view,
1010     GtkTreePath *path,
1011     GtkTreeViewColumn *column)
1012 {
1013   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1014   FolksIndividual *individual;
1015   EmpathyContact *contact;
1016   GtkTreeModel *model;
1017   GtkTreeIter iter;
1018
1019   if (!(priv->individual_features & EMPATHY_INDIVIDUAL_FEATURE_CHAT))
1020     return;
1021
1022   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1023   gtk_tree_model_get_iter (model, &iter, path);
1024   gtk_tree_model_get (model, &iter,
1025       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
1026
1027   if (individual == NULL)
1028     return;
1029
1030   /* Determine which Persona to chat to, by choosing the most available one. */
1031   contact = empathy_contact_dup_best_for_action (individual,
1032       EMPATHY_ACTION_CHAT);
1033
1034   if (contact != NULL)
1035     {
1036       DEBUG ("Starting a chat");
1037
1038       empathy_chat_with_contact (contact,
1039           gtk_get_current_event_time ());
1040     }
1041
1042   g_object_unref (individual);
1043   tp_clear_object (&contact);
1044 }
1045
1046 static void
1047 individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
1048     const gchar *path_string,
1049     EmpathyIndividualView *view)
1050 {
1051   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1052   GtkWidget *menu;
1053   GtkTreeModel *model;
1054   GtkTreeIter iter;
1055   FolksIndividual *individual;
1056   GdkEventButton *event;
1057   GtkMenuShell *shell;
1058   GtkWidget *item;
1059
1060   if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_CALL))
1061     return;
1062
1063   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1064   if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
1065     return;
1066
1067   gtk_tree_model_get (model, &iter,
1068       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
1069   if (individual == NULL)
1070     return;
1071
1072   event = (GdkEventButton *) gtk_get_current_event ();
1073
1074   menu = empathy_context_menu_new (GTK_WIDGET (view));
1075   shell = GTK_MENU_SHELL (menu);
1076
1077   /* audio */
1078   item = empathy_individual_audio_call_menu_item_new (individual);
1079   gtk_menu_shell_append (shell, item);
1080   gtk_widget_show (item);
1081
1082   /* video */
1083   item = empathy_individual_video_call_menu_item_new (individual);
1084   gtk_menu_shell_append (shell, item);
1085   gtk_widget_show (item);
1086
1087   gtk_widget_show (menu);
1088   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1089       event->button, event->time);
1090
1091   g_object_unref (individual);
1092 }
1093
1094 static void
1095 individual_view_cell_set_background (EmpathyIndividualView *view,
1096     GtkCellRenderer *cell,
1097     gboolean is_group,
1098     gboolean is_active)
1099 {
1100   if (!is_group && is_active)
1101     {
1102       GtkStyleContext *style;
1103       GdkRGBA color;
1104
1105       style = gtk_widget_get_style_context (GTK_WIDGET (view));
1106
1107       gtk_style_context_get_background_color (style, GTK_STATE_FLAG_SELECTED,
1108           &color);
1109
1110       /* Here we take the current theme colour and add it to
1111        * the colour for white and average the two. This
1112        * gives a colour which is inline with the theme but
1113        * slightly whiter.
1114        */
1115       empathy_make_color_whiter (&color);
1116
1117       g_object_set (cell, "cell-background-rgba", &color, NULL);
1118     }
1119   else
1120     g_object_set (cell, "cell-background-rgba", NULL, NULL);
1121 }
1122
1123 static void
1124 individual_view_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
1125     GtkCellRenderer *cell,
1126     GtkTreeModel *model,
1127     GtkTreeIter *iter,
1128     EmpathyIndividualView *view)
1129 {
1130   GdkPixbuf *pixbuf;
1131   gboolean is_group;
1132   gboolean is_active;
1133
1134   gtk_tree_model_get (model, iter,
1135       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1136       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
1137       EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, &pixbuf, -1);
1138
1139   g_object_set (cell,
1140       "visible", !is_group,
1141       "pixbuf", pixbuf,
1142       NULL);
1143
1144   tp_clear_object (&pixbuf);
1145
1146   individual_view_cell_set_background (view, cell, is_group, is_active);
1147 }
1148
1149 static void
1150 individual_view_group_icon_cell_data_func (GtkTreeViewColumn *tree_column,
1151     GtkCellRenderer *cell,
1152     GtkTreeModel *model,
1153     GtkTreeIter *iter,
1154     EmpathyIndividualView *view)
1155 {
1156   GdkPixbuf *pixbuf = NULL;
1157   gboolean is_group;
1158   gchar *name;
1159
1160   gtk_tree_model_get (model, iter,
1161       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1162       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
1163
1164   if (!is_group)
1165     goto out;
1166
1167   if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
1168     {
1169       pixbuf = empathy_pixbuf_from_icon_name ("emblem-favorite",
1170           GTK_ICON_SIZE_MENU);
1171     }
1172   else if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_PEOPLE_NEARBY))
1173     {
1174       pixbuf = empathy_pixbuf_from_icon_name ("im-local-xmpp",
1175           GTK_ICON_SIZE_MENU);
1176     }
1177
1178 out:
1179   g_object_set (cell,
1180       "visible", pixbuf != NULL,
1181       "pixbuf", pixbuf,
1182       NULL);
1183
1184   tp_clear_object (&pixbuf);
1185
1186   g_free (name);
1187 }
1188
1189 static void
1190 individual_view_audio_call_cell_data_func (GtkTreeViewColumn *tree_column,
1191     GtkCellRenderer *cell,
1192     GtkTreeModel *model,
1193     GtkTreeIter *iter,
1194     EmpathyIndividualView *view)
1195 {
1196   gboolean is_group;
1197   gboolean is_active;
1198   gboolean can_audio, can_video;
1199
1200   gtk_tree_model_get (model, iter,
1201       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1202       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
1203       EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL, &can_audio,
1204       EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL, &can_video, -1);
1205
1206   g_object_set (cell,
1207       "visible", !is_group && (can_audio || can_video),
1208       "icon-name", can_video ? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
1209       NULL);
1210
1211   individual_view_cell_set_background (view, cell, is_group, is_active);
1212 }
1213
1214 static void
1215 individual_view_avatar_cell_data_func (GtkTreeViewColumn *tree_column,
1216     GtkCellRenderer *cell,
1217     GtkTreeModel *model,
1218     GtkTreeIter *iter,
1219     EmpathyIndividualView *view)
1220 {
1221   GdkPixbuf *pixbuf;
1222   gboolean show_avatar;
1223   gboolean is_group;
1224   gboolean is_active;
1225
1226   gtk_tree_model_get (model, iter,
1227       EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR, &pixbuf,
1228       EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
1229       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1230       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1231
1232   g_object_set (cell,
1233       "visible", !is_group && show_avatar,
1234       "pixbuf", pixbuf,
1235       NULL);
1236
1237   tp_clear_object (&pixbuf);
1238
1239   individual_view_cell_set_background (view, cell, is_group, is_active);
1240 }
1241
1242 static void
1243 individual_view_text_cell_data_func (GtkTreeViewColumn *tree_column,
1244     GtkCellRenderer *cell,
1245     GtkTreeModel *model,
1246     GtkTreeIter *iter,
1247     EmpathyIndividualView *view)
1248 {
1249   gboolean is_group;
1250   gboolean is_active;
1251
1252   gtk_tree_model_get (model, iter,
1253       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1254       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1255
1256   individual_view_cell_set_background (view, cell, is_group, is_active);
1257 }
1258
1259 static void
1260 individual_view_expander_cell_data_func (GtkTreeViewColumn *column,
1261     GtkCellRenderer *cell,
1262     GtkTreeModel *model,
1263     GtkTreeIter *iter,
1264     EmpathyIndividualView *view)
1265 {
1266   gboolean is_group;
1267   gboolean is_active;
1268
1269   gtk_tree_model_get (model, iter,
1270       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1271       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1272
1273   if (gtk_tree_model_iter_has_child (model, iter))
1274     {
1275       GtkTreePath *path;
1276       gboolean row_expanded;
1277
1278       path = gtk_tree_model_get_path (model, iter);
1279       row_expanded =
1280           gtk_tree_view_row_expanded (GTK_TREE_VIEW
1281           (gtk_tree_view_column_get_tree_view (column)), path);
1282       gtk_tree_path_free (path);
1283
1284       g_object_set (cell,
1285           "visible", TRUE,
1286           "expander-style",
1287           row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
1288           NULL);
1289     }
1290   else
1291     g_object_set (cell, "visible", FALSE, NULL);
1292
1293   individual_view_cell_set_background (view, cell, is_group, is_active);
1294 }
1295
1296 static void
1297 individual_view_row_expand_or_collapse_cb (EmpathyIndividualView *view,
1298     GtkTreeIter *iter,
1299     GtkTreePath *path,
1300     gpointer user_data)
1301 {
1302   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1303   GtkTreeModel *model;
1304   gchar *name;
1305   gboolean expanded;
1306
1307   if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE))
1308     return;
1309
1310   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1311
1312   gtk_tree_model_get (model, iter,
1313       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
1314
1315   expanded = GPOINTER_TO_INT (user_data);
1316   empathy_contact_group_set_expanded (name, expanded);
1317
1318   g_free (name);
1319 }
1320
1321 static gboolean
1322 individual_view_start_search_cb (EmpathyIndividualView *view,
1323     gpointer data)
1324 {
1325   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1326
1327   if (priv->search_widget == NULL)
1328     return FALSE;
1329
1330   empathy_individual_view_start_search (view);
1331
1332   return TRUE;
1333 }
1334
1335 static void
1336 individual_view_search_text_notify_cb (EmpathyLiveSearch *search,
1337     GParamSpec *pspec,
1338     EmpathyIndividualView *view)
1339 {
1340   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1341   GtkTreePath *path;
1342   GtkTreeViewColumn *focus_column;
1343   GtkTreeModel *model;
1344   GtkTreeIter iter;
1345   gboolean set_cursor = FALSE;
1346
1347   gtk_tree_model_filter_refilter (priv->filter);
1348
1349   /* Set cursor on the first contact. If it is already set on a group,
1350    * set it on its first child contact. Note that first child of a group
1351    * is its separator, that's why we actually set to the 2nd
1352    */
1353
1354   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1355   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1356
1357   if (path == NULL)
1358     {
1359       path = gtk_tree_path_new_from_string ("0:1");
1360       set_cursor = TRUE;
1361     }
1362   else if (gtk_tree_path_get_depth (path) < 2)
1363     {
1364       gboolean is_group;
1365
1366       gtk_tree_model_get_iter (model, &iter, path);
1367       gtk_tree_model_get (model, &iter,
1368           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1369           -1);
1370
1371       if (is_group)
1372         {
1373           gtk_tree_path_down (path);
1374           gtk_tree_path_next (path);
1375           set_cursor = TRUE;
1376         }
1377     }
1378
1379   if (set_cursor)
1380     {
1381       /* FIXME: Workaround for GTK bug #621651, we have to make sure the path is
1382        * valid. */
1383       if (gtk_tree_model_get_iter (model, &iter, path))
1384         {
1385           gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path, focus_column,
1386               FALSE);
1387         }
1388     }
1389
1390   gtk_tree_path_free (path);
1391 }
1392
1393 static void
1394 individual_view_search_activate_cb (GtkWidget *search,
1395   EmpathyIndividualView *view)
1396 {
1397   GtkTreePath *path;
1398   GtkTreeViewColumn *focus_column;
1399
1400   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1401   if (path != NULL)
1402     {
1403       gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path, focus_column);
1404       gtk_tree_path_free (path);
1405
1406       gtk_widget_hide (search);
1407     }
1408 }
1409
1410 static gboolean
1411 individual_view_search_key_navigation_cb (GtkWidget *search,
1412   GdkEvent *event,
1413   EmpathyIndividualView *view)
1414 {
1415   GdkEvent *new_event;
1416   gboolean ret = FALSE;
1417
1418   new_event = gdk_event_copy (event);
1419   gtk_widget_grab_focus (GTK_WIDGET (view));
1420   ret = gtk_widget_event (GTK_WIDGET (view), new_event);
1421   gtk_widget_grab_focus (search);
1422
1423   gdk_event_free (new_event);
1424
1425   return ret;
1426 }
1427
1428 static void
1429 individual_view_search_hide_cb (EmpathyLiveSearch *search,
1430     EmpathyIndividualView *view)
1431 {
1432   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1433   GtkTreeModel *model;
1434   GtkTreePath *cursor_path;
1435   GtkTreeIter iter;
1436   gboolean valid = FALSE;
1437
1438   /* block expand or collapse handlers, they would write the
1439    * expand or collapsed setting to file otherwise */
1440   g_signal_handlers_block_by_func (view,
1441       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1442   g_signal_handlers_block_by_func (view,
1443     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1444
1445   /* restore which groups are expanded and which are not */
1446   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1447   for (valid = gtk_tree_model_get_iter_first (model, &iter);
1448        valid; valid = gtk_tree_model_iter_next (model, &iter))
1449     {
1450       gboolean is_group;
1451       gchar *name = NULL;
1452       GtkTreePath *path;
1453
1454       gtk_tree_model_get (model, &iter,
1455           EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1456           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1457           -1);
1458
1459       if (!is_group)
1460         {
1461           g_free (name);
1462           continue;
1463         }
1464
1465       path = gtk_tree_model_get_path (model, &iter);
1466       if ((priv->view_features &
1467             EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1468           empathy_contact_group_get_expanded (name))
1469         {
1470           gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
1471         }
1472       else
1473         {
1474           gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1475         }
1476
1477       gtk_tree_path_free (path);
1478       g_free (name);
1479     }
1480
1481   /* unblock expand or collapse handlers */
1482   g_signal_handlers_unblock_by_func (view,
1483       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1484   g_signal_handlers_unblock_by_func (view,
1485       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1486
1487   /* keep the selected contact visible */
1488   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &cursor_path, NULL);
1489
1490   if (cursor_path != NULL)
1491     gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view), cursor_path, NULL,
1492         FALSE, 0, 0);
1493
1494   gtk_tree_path_free (cursor_path);
1495 }
1496
1497 static void
1498 individual_view_search_show_cb (EmpathyLiveSearch *search,
1499     EmpathyIndividualView *view)
1500 {
1501   /* block expand or collapse handlers during expand all, they would
1502    * write the expand or collapsed setting to file otherwise */
1503   g_signal_handlers_block_by_func (view,
1504       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1505
1506   gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
1507
1508   g_signal_handlers_unblock_by_func (view,
1509       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1510 }
1511
1512 static gboolean
1513 expand_idle_foreach_cb (GtkTreeModel *model,
1514     GtkTreePath *path,
1515     GtkTreeIter *iter,
1516     EmpathyIndividualView *self)
1517 {
1518   EmpathyIndividualViewPriv *priv;
1519   gboolean is_group;
1520   gpointer should_expand;
1521   gchar *name;
1522
1523   /* We only want groups */
1524   if (gtk_tree_path_get_depth (path) > 1)
1525     return FALSE;
1526
1527   gtk_tree_model_get (model, iter,
1528       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1529       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1530       -1);
1531
1532   if (!is_group)
1533     {
1534       g_free (name);
1535       return FALSE;
1536     }
1537
1538   priv = GET_PRIV (self);
1539
1540   if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1541       &should_expand))
1542     {
1543       if (GPOINTER_TO_INT (should_expand))
1544         gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
1545       else
1546         gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
1547
1548       g_hash_table_remove (priv->expand_groups, name);
1549     }
1550
1551   g_free (name);
1552
1553   return FALSE;
1554 }
1555
1556 static gboolean
1557 individual_view_expand_idle_cb (EmpathyIndividualView *self)
1558 {
1559   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1560
1561   DEBUG ("individual_view_expand_idle_cb");
1562
1563   g_signal_handlers_block_by_func (self,
1564     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1565   g_signal_handlers_block_by_func (self,
1566     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1567
1568   /* The store/filter could've been removed while we were in the idle queue */
1569   if (priv->filter != NULL)
1570     {
1571       gtk_tree_model_foreach (GTK_TREE_MODEL (priv->filter),
1572           (GtkTreeModelForeachFunc) expand_idle_foreach_cb, self);
1573     }
1574
1575   g_signal_handlers_unblock_by_func (self,
1576       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1577   g_signal_handlers_unblock_by_func (self,
1578       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1579
1580   /* Empty the table of groups to expand/contract, since it may contain groups
1581    * which no longer exist in the tree view. This can happen after going
1582    * offline, for example. */
1583   g_hash_table_remove_all (priv->expand_groups);
1584   priv->expand_groups_idle_handler = 0;
1585   g_object_unref (self);
1586
1587   return FALSE;
1588 }
1589
1590 static void
1591 individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
1592     GtkTreePath *path,
1593     GtkTreeIter *iter,
1594     EmpathyIndividualView *view)
1595 {
1596   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1597   gboolean should_expand, is_group = FALSE;
1598   gchar *name = NULL;
1599   gpointer will_expand;
1600
1601   gtk_tree_model_get (model, iter,
1602       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1603       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1604       -1);
1605
1606   if (!is_group || EMP_STR_EMPTY (name))
1607     {
1608       g_free (name);
1609       return;
1610     }
1611
1612   should_expand = (priv->view_features &
1613           EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1614       (priv->search_widget != NULL &&
1615           gtk_widget_get_visible (priv->search_widget)) ||
1616       empathy_contact_group_get_expanded (name);
1617
1618   /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1619    * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
1620    * a hash table, and expand or contract them as appropriate all at once in
1621    * an idle handler which iterates over all the group rows. */
1622   if (!g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1623       &will_expand) ||
1624       GPOINTER_TO_INT (will_expand) != should_expand)
1625     {
1626       g_hash_table_insert (priv->expand_groups, g_strdup (name),
1627           GINT_TO_POINTER (should_expand));
1628
1629       if (priv->expand_groups_idle_handler == 0)
1630         {
1631           priv->expand_groups_idle_handler =
1632               g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
1633                   g_object_ref (view));
1634         }
1635     }
1636
1637   g_free (name);
1638 }
1639
1640 /* FIXME: This is a workaround for bgo#621076 */
1641 static void
1642 individual_view_verify_group_visibility (EmpathyIndividualView *view,
1643     GtkTreePath *path)
1644 {
1645   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1646   GtkTreeModel *model;
1647   GtkTreePath *parent_path;
1648   GtkTreeIter parent_iter;
1649
1650   if (gtk_tree_path_get_depth (path) < 2)
1651     return;
1652
1653   /* A group row is visible if and only if at least one if its child is visible.
1654    * So when a row is inserted/deleted/changed in the base model, that could
1655    * modify the visibility of its parent in the filter model.
1656   */
1657
1658   model = GTK_TREE_MODEL (priv->store);
1659   parent_path = gtk_tree_path_copy (path);
1660   gtk_tree_path_up (parent_path);
1661   if (gtk_tree_model_get_iter (model, &parent_iter, parent_path))
1662     {
1663       /* This tells the filter to verify the visibility of that row, and
1664        * show/hide it if necessary */
1665       gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
1666               parent_path, &parent_iter);
1667     }
1668   gtk_tree_path_free (parent_path);
1669 }
1670
1671 static void
1672 individual_view_store_row_changed_cb (GtkTreeModel *model,
1673   GtkTreePath *path,
1674   GtkTreeIter *iter,
1675   EmpathyIndividualView *view)
1676 {
1677   individual_view_verify_group_visibility (view, path);
1678 }
1679
1680 static void
1681 individual_view_store_row_deleted_cb (GtkTreeModel *model,
1682   GtkTreePath *path,
1683   EmpathyIndividualView *view)
1684 {
1685   individual_view_verify_group_visibility (view, path);
1686 }
1687
1688 static gboolean
1689 individual_view_is_visible_individual (EmpathyIndividualView *self,
1690     FolksIndividual *individual,
1691     gboolean is_online,
1692     gboolean is_searching,
1693     const gchar *group,
1694     gboolean is_fake_group,
1695     guint event_count)
1696 {
1697   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1698   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
1699   GeeSet *personas;
1700   GeeIterator *iter;
1701   gboolean is_favorite;
1702
1703   /* Always display individuals having pending events */
1704   if (event_count > 0)
1705     return TRUE;
1706
1707   /* We're only giving the visibility wrt filtering here, not things like
1708    * presence. */
1709   if (!priv->show_untrusted &&
1710       folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
1711     {
1712       return FALSE;
1713     }
1714
1715   if (!priv->show_uninteresting)
1716     {
1717       gboolean contains_interesting_persona = FALSE;
1718
1719       /* Hide all individuals which consist entirely of uninteresting
1720        * personas */
1721       personas = folks_individual_get_personas (individual);
1722       iter = gee_iterable_iterator (GEE_ITERABLE (personas));
1723       while (!contains_interesting_persona && gee_iterator_next (iter))
1724         {
1725           FolksPersona *persona = gee_iterator_get (iter);
1726
1727           if (empathy_folks_persona_is_interesting (persona))
1728             contains_interesting_persona = TRUE;
1729
1730           g_clear_object (&persona);
1731         }
1732       g_clear_object (&iter);
1733
1734       if (!contains_interesting_persona)
1735         return FALSE;
1736     }
1737
1738   is_favorite = folks_favourite_details_get_is_favourite (
1739       FOLKS_FAVOURITE_DETAILS (individual));
1740   if (!is_searching) {
1741     if (is_favorite && is_fake_group &&
1742         !tp_strdiff (group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
1743         /* Always display favorite contacts in the favorite group */
1744         return TRUE;
1745
1746     return (priv->show_offline || is_online);
1747   }
1748
1749   return empathy_individual_match_string (individual,
1750       empathy_live_search_get_text (live),
1751       empathy_live_search_get_words (live));
1752 }
1753
1754 static gchar *
1755 get_group (GtkTreeModel *model,
1756     GtkTreeIter *iter,
1757     gboolean *is_fake)
1758 {
1759   GtkTreeIter parent_iter;
1760   gchar *name = NULL;
1761
1762   *is_fake = FALSE;
1763
1764   if (!gtk_tree_model_iter_parent (model, &parent_iter, iter))
1765     return NULL;
1766
1767   gtk_tree_model_get (model, &parent_iter,
1768       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1769       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake,
1770       -1);
1771
1772   return name;
1773 }
1774
1775
1776 static gboolean
1777 individual_view_filter_visible_func (GtkTreeModel *model,
1778     GtkTreeIter *iter,
1779     gpointer user_data)
1780 {
1781   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1782   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1783   FolksIndividual *individual = NULL;
1784   gboolean is_group, is_separator, valid;
1785   GtkTreeIter child_iter;
1786   gboolean visible, is_online;
1787   gboolean is_searching = TRUE;
1788   guint event_count;
1789
1790   if (priv->custom_filter != NULL)
1791     return priv->custom_filter (model, iter, priv->custom_filter_data);
1792
1793   if (priv->search_widget == NULL ||
1794       !gtk_widget_get_visible (priv->search_widget))
1795      is_searching = FALSE;
1796
1797   gtk_tree_model_get (model, iter,
1798       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1799       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1800       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1801       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1802       EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
1803       -1);
1804
1805   if (individual != NULL)
1806     {
1807       gchar *group;
1808       gboolean is_fake_group;
1809
1810       group = get_group (model, iter, &is_fake_group);
1811
1812       visible = individual_view_is_visible_individual (self, individual,
1813           is_online, is_searching, group, is_fake_group, event_count);
1814
1815       g_object_unref (individual);
1816       g_free (group);
1817
1818       /* FIXME: Work around bgo#626552/bgo#621076 */
1819       if (visible)
1820         {
1821           GtkTreePath *path = gtk_tree_model_get_path (model, iter);
1822           individual_view_verify_group_visibility (self, path);
1823           gtk_tree_path_free (path);
1824         }
1825
1826       return visible;
1827     }
1828
1829   if (is_separator)
1830     return TRUE;
1831
1832   /* Not a contact, not a separator, must be a group */
1833   g_return_val_if_fail (is_group, FALSE);
1834
1835   /* only show groups which are not empty */
1836   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1837        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1838     {
1839       gchar *group;
1840       gboolean is_fake_group;
1841
1842       gtk_tree_model_get (model, &child_iter,
1843         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1844         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1845         EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
1846         -1);
1847
1848       if (individual == NULL)
1849         continue;
1850
1851       group = get_group (model, &child_iter, &is_fake_group);
1852
1853       visible = individual_view_is_visible_individual (self, individual,
1854           is_online, is_searching, group, is_fake_group, event_count);
1855
1856       g_object_unref (individual);
1857       g_free (group);
1858
1859       /* show group if it has at least one visible contact in it */
1860       if (visible)
1861         return TRUE;
1862     }
1863
1864   return FALSE;
1865 }
1866
1867 static void
1868 individual_view_constructed (GObject *object)
1869 {
1870   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1871   GtkCellRenderer *cell;
1872   GtkTreeViewColumn *col;
1873   guint i;
1874
1875   /* Setup view */
1876   g_object_set (view,
1877       "headers-visible", FALSE,
1878       "show-expanders", FALSE,
1879       NULL);
1880
1881   col = gtk_tree_view_column_new ();
1882
1883   /* State */
1884   cell = gtk_cell_renderer_pixbuf_new ();
1885   gtk_tree_view_column_pack_start (col, cell, FALSE);
1886   gtk_tree_view_column_set_cell_data_func (col, cell,
1887       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1888       view, NULL);
1889
1890   g_object_set (cell,
1891       "xpad", 5,
1892       "ypad", 1,
1893       "visible", FALSE,
1894       NULL);
1895
1896   /* Group icon */
1897   cell = gtk_cell_renderer_pixbuf_new ();
1898   gtk_tree_view_column_pack_start (col, cell, FALSE);
1899   gtk_tree_view_column_set_cell_data_func (col, cell,
1900       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1901       view, NULL);
1902
1903   g_object_set (cell,
1904       "xpad", 0,
1905       "ypad", 0,
1906       "visible", FALSE,
1907       "width", 16,
1908       "height", 16,
1909       NULL);
1910
1911   /* Name */
1912   cell = empathy_cell_renderer_text_new ();
1913   gtk_tree_view_column_pack_start (col, cell, TRUE);
1914   gtk_tree_view_column_set_cell_data_func (col, cell,
1915       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1916
1917   gtk_tree_view_column_add_attribute (col, cell,
1918       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1919   gtk_tree_view_column_add_attribute (col, cell,
1920       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1921   gtk_tree_view_column_add_attribute (col, cell,
1922       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1923   gtk_tree_view_column_add_attribute (col, cell,
1924       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1925   gtk_tree_view_column_add_attribute (col, cell,
1926       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1927   gtk_tree_view_column_add_attribute (col, cell,
1928       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1929   gtk_tree_view_column_add_attribute (col, cell,
1930       "client-types", EMPATHY_INDIVIDUAL_STORE_COL_CLIENT_TYPES);
1931
1932   /* Audio Call Icon */
1933   cell = empathy_cell_renderer_activatable_new ();
1934   gtk_tree_view_column_pack_start (col, cell, FALSE);
1935   gtk_tree_view_column_set_cell_data_func (col, cell,
1936       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1937       view, NULL);
1938
1939   g_object_set (cell, "visible", FALSE, NULL);
1940
1941   g_signal_connect (cell, "path-activated",
1942       G_CALLBACK (individual_view_call_activated_cb), view);
1943
1944   /* Avatar */
1945   cell = gtk_cell_renderer_pixbuf_new ();
1946   gtk_tree_view_column_pack_start (col, cell, FALSE);
1947   gtk_tree_view_column_set_cell_data_func (col, cell,
1948       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1949       view, NULL);
1950
1951   g_object_set (cell,
1952       "xpad", 0,
1953       "ypad", 0,
1954       "visible", FALSE,
1955       "width", 32,
1956       "height", 32,
1957       NULL);
1958
1959   /* Expander */
1960   cell = empathy_cell_renderer_expander_new ();
1961   gtk_tree_view_column_pack_end (col, cell, FALSE);
1962   gtk_tree_view_column_set_cell_data_func (col, cell,
1963       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1964       view, NULL);
1965
1966   /* Actually add the column now we have added all cell renderers */
1967   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1968
1969   /* Drag & Drop. */
1970   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1971     {
1972       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1973     }
1974 }
1975
1976 static void
1977 individual_view_set_view_features (EmpathyIndividualView *view,
1978     EmpathyIndividualFeatureFlags features)
1979 {
1980   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1981   gboolean has_tooltip;
1982
1983   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1984
1985   priv->view_features = features;
1986
1987   /* Setting reorderable is a hack that gets us row previews as drag icons
1988      for free.  We override all the drag handlers.  It's tricky to get the
1989      position of the drag icon right in drag_begin.  GtkTreeView has special
1990      voodoo for it, so we let it do the voodoo that he do (but only if dragging
1991      is enabled).
1992    */
1993   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
1994       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
1995
1996   /* Update DnD source/dest */
1997   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
1998     {
1999       gtk_drag_source_set (GTK_WIDGET (view),
2000           GDK_BUTTON1_MASK,
2001           drag_types_source,
2002           G_N_ELEMENTS (drag_types_source),
2003           GDK_ACTION_MOVE | GDK_ACTION_COPY);
2004     }
2005   else
2006     {
2007       gtk_drag_source_unset (GTK_WIDGET (view));
2008
2009     }
2010
2011   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
2012     {
2013       gtk_drag_dest_set (GTK_WIDGET (view),
2014           GTK_DEST_DEFAULT_ALL,
2015           drag_types_dest,
2016           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
2017     }
2018   else
2019     {
2020       /* FIXME: URI could still be droped depending on FT feature */
2021       gtk_drag_dest_unset (GTK_WIDGET (view));
2022     }
2023
2024   /* Update has-tooltip */
2025   has_tooltip =
2026       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
2027   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
2028 }
2029
2030 static void
2031 individual_view_dispose (GObject *object)
2032 {
2033   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2034   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2035
2036   tp_clear_object (&priv->store);
2037   tp_clear_object (&priv->filter);
2038   tp_clear_object (&priv->tooltip_widget);
2039
2040   empathy_individual_view_set_live_search (view, NULL);
2041
2042   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
2043 }
2044
2045 static void
2046 individual_view_finalize (GObject *object)
2047 {
2048   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2049
2050   if (priv->expand_groups_idle_handler != 0)
2051     g_source_remove (priv->expand_groups_idle_handler);
2052   g_hash_table_unref (priv->expand_groups);
2053
2054   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
2055 }
2056
2057 static void
2058 individual_view_get_property (GObject *object,
2059     guint param_id,
2060     GValue *value,
2061     GParamSpec *pspec)
2062 {
2063   EmpathyIndividualViewPriv *priv;
2064
2065   priv = GET_PRIV (object);
2066
2067   switch (param_id)
2068     {
2069     case PROP_STORE:
2070       g_value_set_object (value, priv->store);
2071       break;
2072     case PROP_VIEW_FEATURES:
2073       g_value_set_flags (value, priv->view_features);
2074       break;
2075     case PROP_INDIVIDUAL_FEATURES:
2076       g_value_set_flags (value, priv->individual_features);
2077       break;
2078     case PROP_SHOW_OFFLINE:
2079       g_value_set_boolean (value, priv->show_offline);
2080       break;
2081     case PROP_SHOW_UNTRUSTED:
2082       g_value_set_boolean (value, priv->show_untrusted);
2083       break;
2084     case PROP_SHOW_UNINTERESTING:
2085       g_value_set_boolean (value, priv->show_uninteresting);
2086       break;
2087     default:
2088       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2089       break;
2090     };
2091 }
2092
2093 static void
2094 individual_view_set_property (GObject *object,
2095     guint param_id,
2096     const GValue *value,
2097     GParamSpec *pspec)
2098 {
2099   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2100   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2101
2102   switch (param_id)
2103     {
2104     case PROP_STORE:
2105       empathy_individual_view_set_store (view, g_value_get_object (value));
2106       break;
2107     case PROP_VIEW_FEATURES:
2108       individual_view_set_view_features (view, g_value_get_flags (value));
2109       break;
2110     case PROP_INDIVIDUAL_FEATURES:
2111       priv->individual_features = g_value_get_flags (value);
2112       break;
2113     case PROP_SHOW_OFFLINE:
2114       empathy_individual_view_set_show_offline (view,
2115           g_value_get_boolean (value));
2116       break;
2117     case PROP_SHOW_UNTRUSTED:
2118       empathy_individual_view_set_show_untrusted (view,
2119           g_value_get_boolean (value));
2120       break;
2121     case PROP_SHOW_UNINTERESTING:
2122       empathy_individual_view_set_show_uninteresting (view,
2123           g_value_get_boolean (value));
2124     default:
2125       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2126       break;
2127     };
2128 }
2129
2130 static void
2131 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
2132 {
2133   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2134   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2135   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
2136
2137   object_class->constructed = individual_view_constructed;
2138   object_class->dispose = individual_view_dispose;
2139   object_class->finalize = individual_view_finalize;
2140   object_class->get_property = individual_view_get_property;
2141   object_class->set_property = individual_view_set_property;
2142
2143   widget_class->drag_data_received = individual_view_drag_data_received;
2144   widget_class->drag_drop = individual_view_drag_drop;
2145   widget_class->drag_begin = individual_view_drag_begin;
2146   widget_class->drag_data_get = individual_view_drag_data_get;
2147   widget_class->drag_end = individual_view_drag_end;
2148   widget_class->drag_motion = individual_view_drag_motion;
2149
2150   /* We use the class method to let user of this widget to connect to
2151    * the signal and stop emission of the signal so the default handler
2152    * won't be called. */
2153   tree_view_class->row_activated = individual_view_row_activated;
2154
2155   klass->drag_individual_received = real_drag_individual_received_cb;
2156
2157   signals[DRAG_INDIVIDUAL_RECEIVED] =
2158       g_signal_new ("drag-individual-received",
2159       G_OBJECT_CLASS_TYPE (klass),
2160       G_SIGNAL_RUN_LAST,
2161       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
2162       NULL, NULL,
2163       g_cclosure_marshal_generic,
2164       G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
2165       G_TYPE_STRING, G_TYPE_STRING);
2166
2167   signals[DRAG_PERSONA_RECEIVED] =
2168       g_signal_new ("drag-persona-received",
2169       G_OBJECT_CLASS_TYPE (klass),
2170       G_SIGNAL_RUN_LAST,
2171       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
2172       NULL, NULL,
2173       g_cclosure_marshal_generic,
2174       G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
2175
2176   g_object_class_install_property (object_class,
2177       PROP_STORE,
2178       g_param_spec_object ("store",
2179           "The store of the view",
2180           "The store of the view",
2181           EMPATHY_TYPE_INDIVIDUAL_STORE,
2182           G_PARAM_READWRITE));
2183   g_object_class_install_property (object_class,
2184       PROP_VIEW_FEATURES,
2185       g_param_spec_flags ("view-features",
2186           "Features of the view",
2187           "Flags for all enabled features",
2188           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
2189           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
2190   g_object_class_install_property (object_class,
2191       PROP_INDIVIDUAL_FEATURES,
2192       g_param_spec_flags ("individual-features",
2193           "Features of the individual menu",
2194           "Flags for all enabled features for the menu",
2195           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
2196           EMPATHY_INDIVIDUAL_FEATURE_NONE, G_PARAM_READWRITE));
2197   g_object_class_install_property (object_class,
2198       PROP_SHOW_OFFLINE,
2199       g_param_spec_boolean ("show-offline",
2200           "Show Offline",
2201           "Whether contact list should display "
2202           "offline contacts", FALSE, G_PARAM_READWRITE));
2203   g_object_class_install_property (object_class,
2204       PROP_SHOW_UNTRUSTED,
2205       g_param_spec_boolean ("show-untrusted",
2206           "Show Untrusted Individuals",
2207           "Whether the view should display untrusted individuals; "
2208           "those who could not be who they say they are.",
2209           TRUE, G_PARAM_READWRITE));
2210   g_object_class_install_property (object_class,
2211       PROP_SHOW_UNINTERESTING,
2212       g_param_spec_boolean ("show-uninteresting",
2213           "Show Uninteresting Individuals",
2214           "Whether the view should not filter out individuals using "
2215           "empathy_folks_persona_is_interesting.",
2216           FALSE, G_PARAM_READWRITE));
2217
2218   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
2219 }
2220
2221 static void
2222 empathy_individual_view_init (EmpathyIndividualView *view)
2223 {
2224   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
2225       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
2226
2227   view->priv = priv;
2228
2229   priv->show_untrusted = TRUE;
2230   priv->show_uninteresting = FALSE;
2231
2232   /* Get saved group states. */
2233   empathy_contact_groups_get_all ();
2234
2235   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
2236       (GDestroyNotify) g_free, NULL);
2237
2238   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
2239       empathy_individual_store_row_separator_func, NULL, NULL);
2240
2241   /* Connect to tree view signals rather than override. */
2242   g_signal_connect (view, "button-press-event",
2243       G_CALLBACK (individual_view_button_press_event_cb), NULL);
2244   g_signal_connect (view, "key-press-event",
2245       G_CALLBACK (individual_view_key_press_event_cb), NULL);
2246   g_signal_connect (view, "row-expanded",
2247       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2248       GINT_TO_POINTER (TRUE));
2249   g_signal_connect (view, "row-collapsed",
2250       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2251       GINT_TO_POINTER (FALSE));
2252   g_signal_connect (view, "query-tooltip",
2253       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
2254 }
2255
2256 EmpathyIndividualView *
2257 empathy_individual_view_new (EmpathyIndividualStore *store,
2258     EmpathyIndividualViewFeatureFlags view_features,
2259     EmpathyIndividualFeatureFlags individual_features)
2260 {
2261   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
2262
2263   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
2264       "store", store,
2265       "individual-features", individual_features,
2266       "view-features", view_features, NULL);
2267 }
2268
2269 FolksIndividual *
2270 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
2271 {
2272   GtkTreeSelection *selection;
2273   GtkTreeIter iter;
2274   GtkTreeModel *model;
2275   FolksIndividual *individual;
2276
2277   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2278
2279   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2280   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2281     return NULL;
2282
2283   gtk_tree_model_get (model, &iter,
2284       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
2285
2286   return individual;
2287 }
2288
2289 static gchar *
2290 empathy_individual_view_dup_selected_group (EmpathyIndividualView *view,
2291     gboolean *is_fake_group)
2292 {
2293   GtkTreeSelection *selection;
2294   GtkTreeIter iter;
2295   GtkTreeModel *model;
2296   gboolean is_group;
2297   gchar *name;
2298   gboolean fake;
2299
2300   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2301
2302   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2303   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2304     return NULL;
2305
2306   gtk_tree_model_get (model, &iter,
2307       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2308       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2309       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2310
2311   if (!is_group)
2312     {
2313       g_free (name);
2314       return NULL;
2315     }
2316
2317   if (is_fake_group != NULL)
2318     *is_fake_group = fake;
2319
2320   return name;
2321 }
2322
2323 enum
2324 {
2325   REMOVE_DIALOG_RESPONSE_CANCEL = 0,
2326   REMOVE_DIALOG_RESPONSE_DELETE,
2327   REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK,
2328 };
2329
2330 static int
2331 individual_view_remove_dialog_show (GtkWindow *parent,
2332     const gchar *message,
2333     const gchar *secondary_text,
2334     gboolean block_button,
2335     GdkPixbuf *avatar)
2336 {
2337   GtkWidget *dialog;
2338   gboolean res;
2339
2340   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2341       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2342
2343   if (avatar != NULL)
2344     {
2345       GtkWidget *image = gtk_image_new_from_pixbuf (avatar);
2346       gtk_message_dialog_set_image (GTK_MESSAGE_DIALOG (dialog), image);
2347       gtk_widget_show (image);
2348     }
2349
2350   if (block_button)
2351     {
2352       GtkWidget *button;
2353
2354       /* gtk_dialog_add_button() doesn't allow us to pass a string with a
2355        * mnemonic so we have to create the button manually. */
2356       button = gtk_button_new_with_mnemonic (
2357           _("Delete and _Block"));
2358
2359       gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
2360           REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK);
2361
2362       gtk_widget_show (button);
2363     }
2364
2365   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2366       GTK_STOCK_CANCEL, REMOVE_DIALOG_RESPONSE_CANCEL,
2367       GTK_STOCK_DELETE, REMOVE_DIALOG_RESPONSE_DELETE, NULL);
2368   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2369       "%s", secondary_text);
2370
2371   gtk_widget_show (dialog);
2372
2373   res = gtk_dialog_run (GTK_DIALOG (dialog));
2374   gtk_widget_destroy (dialog);
2375
2376   return res;
2377 }
2378
2379 static void
2380 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2381     EmpathyIndividualView *view)
2382 {
2383   gchar *group;
2384
2385   group = empathy_individual_view_dup_selected_group (view, NULL);
2386   if (group != NULL)
2387     {
2388       gchar *text;
2389       GtkWindow *parent;
2390
2391       text =
2392           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2393           group);
2394       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2395       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2396               text, FALSE, NULL) == REMOVE_DIALOG_RESPONSE_DELETE)
2397         {
2398           EmpathyIndividualManager *manager =
2399               empathy_individual_manager_dup_singleton ();
2400           empathy_individual_manager_remove_group (manager, group);
2401           g_object_unref (G_OBJECT (manager));
2402         }
2403
2404       g_free (text);
2405     }
2406
2407   g_free (group);
2408 }
2409
2410 GtkWidget *
2411 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2412 {
2413   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2414   gchar *group;
2415   GtkWidget *menu;
2416   GtkWidget *item;
2417   GtkWidget *image;
2418   gboolean is_fake_group;
2419
2420   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2421
2422   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2423               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2424     return NULL;
2425
2426   group = empathy_individual_view_dup_selected_group (view, &is_fake_group);
2427   if (!group || is_fake_group)
2428     {
2429       /* We can't alter fake groups */
2430       g_free (group);
2431       return NULL;
2432     }
2433
2434   menu = gtk_menu_new ();
2435
2436   /* TODO: implement
2437      if (priv->view_features &
2438      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2439      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2440      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2441      gtk_widget_show (item);
2442      g_signal_connect (item, "activate",
2443      G_CALLBACK (individual_view_group_rename_activate_cb),
2444      view);
2445      }
2446    */
2447
2448   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2449     {
2450       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2451       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2452           GTK_ICON_SIZE_MENU);
2453       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2454       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2455       gtk_widget_show (item);
2456       g_signal_connect (item, "activate",
2457           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2458     }
2459
2460   g_free (group);
2461
2462   return menu;
2463 }
2464
2465 static void
2466 got_avatar (GObject *source_object,
2467     GAsyncResult *result,
2468     gpointer user_data)
2469 {
2470   FolksIndividual *individual = FOLKS_INDIVIDUAL (source_object);
2471   EmpathyIndividualView *view = user_data;
2472   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2473   GdkPixbuf *avatar;
2474   EmpathyIndividualManager *manager;
2475   gchar *text;
2476   GtkWindow *parent;
2477   GeeSet *personas;
2478   guint persona_count = 0;
2479   gboolean can_block;
2480   GError *error = NULL;
2481   gint res;
2482
2483   avatar = empathy_pixbuf_avatar_from_individual_scaled_finish (individual,
2484       result, &error);
2485
2486   if (error != NULL)
2487     {
2488       DEBUG ("Could not get avatar: %s", error->message);
2489       g_error_free (error);
2490     }
2491
2492   /* We couldn't retrieve the avatar, but that isn't a fatal error,
2493    * so we still display the remove dialog. */
2494
2495   personas = folks_individual_get_personas (individual);
2496
2497   if (priv->show_uninteresting)
2498     {
2499       persona_count = gee_collection_get_size (GEE_COLLECTION (personas));
2500     }
2501   else
2502     {
2503       GeeIterator *iter;
2504
2505       iter = gee_iterable_iterator (GEE_ITERABLE (personas));
2506       while (persona_count < 2 && gee_iterator_next (iter))
2507         {
2508           FolksPersona *persona = gee_iterator_get (iter);
2509
2510           if (empathy_folks_persona_is_interesting (persona))
2511             persona_count++;
2512
2513           g_clear_object (&persona);
2514         }
2515       g_clear_object (&iter);
2516     }
2517
2518   /* If we have more than one TpfPersona, display a different message
2519    * ensuring the user knows that *all* of the meta-contacts' personas will
2520    * be removed. */
2521
2522   if (persona_count < 2)
2523     {
2524       /* Not a meta-contact */
2525       text =
2526           g_strdup_printf (
2527               _("Do you really want to remove the contact '%s'?"),
2528               folks_alias_details_get_alias (
2529                   FOLKS_ALIAS_DETAILS (individual)));
2530     }
2531   else
2532     {
2533       /* Meta-contact */
2534       text =
2535           g_strdup_printf (
2536               _("Do you really want to remove the linked contact '%s'? "
2537                 "Note that this will remove all the contacts which make up "
2538                 "this linked contact."),
2539               folks_alias_details_get_alias (
2540                   FOLKS_ALIAS_DETAILS (individual)));
2541     }
2542
2543
2544   manager = empathy_individual_manager_dup_singleton ();
2545   can_block = empathy_individual_manager_supports_blocking (manager,
2546       individual);
2547   parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2548   res = individual_view_remove_dialog_show (parent, _("Removing contact"),
2549           text, can_block, avatar);
2550
2551   if (res == REMOVE_DIALOG_RESPONSE_DELETE ||
2552       res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2553     {
2554       gboolean abusive;
2555
2556       if (res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2557         {
2558           if (!empathy_block_individual_dialog_show (parent, individual,
2559                 avatar, &abusive))
2560             goto finally;
2561
2562           empathy_individual_manager_set_blocked (manager, individual,
2563               TRUE, abusive);
2564         }
2565
2566       empathy_individual_manager_remove (manager, individual, "");
2567     }
2568
2569  finally:
2570   g_free (text);
2571   g_object_unref (manager);
2572 }
2573
2574 static void
2575 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2576     EmpathyIndividualView *view)
2577 {
2578   FolksIndividual *individual;
2579
2580   individual = empathy_individual_view_dup_selected (view);
2581
2582   if (individual != NULL)
2583     {
2584       empathy_pixbuf_avatar_from_individual_scaled_async (individual,
2585           48, 48, NULL, got_avatar, view);
2586       g_object_unref (individual);
2587     }
2588 }
2589
2590 static void
2591 individual_menu_link_contacts_activated_cb (EmpathyIndividualMenu *menu,
2592     EmpathyLinkingDialog *linking_dialog,
2593     EmpathyIndividualView *self)
2594 {
2595   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2596   EmpathyIndividualLinker *linker;
2597
2598   linker = empathy_linking_dialog_get_individual_linker (linking_dialog);
2599   empathy_individual_linker_set_search_text (linker,
2600       empathy_live_search_get_text (EMPATHY_LIVE_SEARCH (priv->search_widget)));
2601 }
2602
2603 GtkWidget *
2604 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2605 {
2606   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2607   FolksIndividual *individual;
2608   GtkWidget *menu = NULL;
2609   GtkWidget *item;
2610   GtkWidget *image;
2611   gboolean can_remove = FALSE;
2612   GeeSet *personas;
2613   GeeIterator *iter;
2614
2615   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2616
2617   if (priv->individual_features == EMPATHY_INDIVIDUAL_FEATURE_NONE)
2618     /* No need to create a context menu */
2619     return NULL;
2620
2621   individual = empathy_individual_view_dup_selected (view);
2622   if (individual == NULL)
2623     return NULL;
2624
2625   if (!empathy_folks_individual_contains_contact (individual))
2626     goto out;
2627
2628   /* If any of the Individual's personas can be removed, add an option to
2629    * remove. This will act as a best-effort option. If any Personas cannot be
2630    * removed from the server, then this option will just be inactive upon
2631    * subsequent menu openings */
2632   personas = folks_individual_get_personas (individual);
2633   iter = gee_iterable_iterator (GEE_ITERABLE (personas));
2634   while (!can_remove && gee_iterator_next (iter))
2635     {
2636       FolksPersona *persona = gee_iterator_get (iter);
2637       FolksPersonaStore *store = folks_persona_get_store (persona);
2638       FolksMaybeBool maybe_can_remove =
2639           folks_persona_store_get_can_remove_personas (store);
2640
2641       if (maybe_can_remove == FOLKS_MAYBE_BOOL_TRUE)
2642         can_remove = TRUE;
2643
2644       g_clear_object (&persona);
2645     }
2646   g_clear_object (&iter);
2647
2648   menu = empathy_individual_menu_new (individual, priv->individual_features,
2649       priv->store);
2650
2651   /* Remove contact */
2652   if ((priv->view_features &
2653       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE) &&
2654       can_remove)
2655     {
2656       /* create the menu if required, or just add a separator */
2657       if (menu == NULL)
2658         menu = gtk_menu_new ();
2659       else
2660         {
2661           item = gtk_separator_menu_item_new ();
2662           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2663           gtk_widget_show (item);
2664         }
2665
2666       /* Remove */
2667       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2668       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2669           GTK_ICON_SIZE_MENU);
2670       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2671       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2672       gtk_widget_show (item);
2673       g_signal_connect (item, "activate",
2674           G_CALLBACK (individual_view_remove_activate_cb), view);
2675     }
2676
2677   /* Connect to EmpathyIndividualMenu::link-contacts-activated so that we can
2678    * set the live search text on the new linking dialogue to be the same as
2679    * our own. */
2680   g_signal_connect (menu, "link-contacts-activated",
2681       (GCallback) individual_menu_link_contacts_activated_cb, view);
2682
2683 out:
2684   g_object_unref (individual);
2685
2686   return menu;
2687 }
2688
2689 void
2690 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2691     EmpathyLiveSearch *search)
2692 {
2693   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2694
2695   /* remove old handlers if old search was not null */
2696   if (priv->search_widget != NULL)
2697     {
2698       g_signal_handlers_disconnect_by_func (view,
2699           individual_view_start_search_cb, NULL);
2700
2701       g_signal_handlers_disconnect_by_func (priv->search_widget,
2702           individual_view_search_text_notify_cb, view);
2703       g_signal_handlers_disconnect_by_func (priv->search_widget,
2704           individual_view_search_activate_cb, view);
2705       g_signal_handlers_disconnect_by_func (priv->search_widget,
2706           individual_view_search_key_navigation_cb, view);
2707       g_signal_handlers_disconnect_by_func (priv->search_widget,
2708           individual_view_search_hide_cb, view);
2709       g_signal_handlers_disconnect_by_func (priv->search_widget,
2710           individual_view_search_show_cb, view);
2711       g_object_unref (priv->search_widget);
2712       priv->search_widget = NULL;
2713     }
2714
2715   /* connect handlers if new search is not null */
2716   if (search != NULL)
2717     {
2718       priv->search_widget = g_object_ref (search);
2719
2720       g_signal_connect (view, "start-interactive-search",
2721           G_CALLBACK (individual_view_start_search_cb), NULL);
2722
2723       g_signal_connect (priv->search_widget, "notify::text",
2724           G_CALLBACK (individual_view_search_text_notify_cb), view);
2725       g_signal_connect (priv->search_widget, "activate",
2726           G_CALLBACK (individual_view_search_activate_cb), view);
2727       g_signal_connect (priv->search_widget, "key-navigation",
2728           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2729       g_signal_connect (priv->search_widget, "hide",
2730           G_CALLBACK (individual_view_search_hide_cb), view);
2731       g_signal_connect (priv->search_widget, "show",
2732           G_CALLBACK (individual_view_search_show_cb), view);
2733     }
2734 }
2735
2736 gboolean
2737 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2738 {
2739   EmpathyIndividualViewPriv *priv;
2740
2741   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2742
2743   priv = GET_PRIV (self);
2744
2745   return (priv->search_widget != NULL &&
2746           gtk_widget_get_visible (priv->search_widget));
2747 }
2748
2749 gboolean
2750 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2751 {
2752   EmpathyIndividualViewPriv *priv;
2753
2754   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2755
2756   priv = GET_PRIV (self);
2757
2758   return priv->show_offline;
2759 }
2760
2761 void
2762 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2763     gboolean show_offline)
2764 {
2765   EmpathyIndividualViewPriv *priv;
2766
2767   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2768
2769   priv = GET_PRIV (self);
2770
2771   priv->show_offline = show_offline;
2772
2773   g_object_notify (G_OBJECT (self), "show-offline");
2774   gtk_tree_model_filter_refilter (priv->filter);
2775 }
2776
2777 gboolean
2778 empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
2779 {
2780   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2781
2782   return GET_PRIV (self)->show_untrusted;
2783 }
2784
2785 void
2786 empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
2787     gboolean show_untrusted)
2788 {
2789   EmpathyIndividualViewPriv *priv;
2790
2791   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2792
2793   priv = GET_PRIV (self);
2794
2795   priv->show_untrusted = show_untrusted;
2796
2797   g_object_notify (G_OBJECT (self), "show-untrusted");
2798   gtk_tree_model_filter_refilter (priv->filter);
2799 }
2800
2801 EmpathyIndividualStore *
2802 empathy_individual_view_get_store (EmpathyIndividualView *self)
2803 {
2804   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2805
2806   return GET_PRIV (self)->store;
2807 }
2808
2809 void
2810 empathy_individual_view_set_store (EmpathyIndividualView *self,
2811     EmpathyIndividualStore *store)
2812 {
2813   EmpathyIndividualViewPriv *priv;
2814
2815   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2816   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2817
2818   priv = GET_PRIV (self);
2819
2820   /* Destroy the old filter and remove the old store */
2821   if (priv->store != NULL)
2822     {
2823       g_signal_handlers_disconnect_by_func (priv->store,
2824           individual_view_store_row_changed_cb, self);
2825       g_signal_handlers_disconnect_by_func (priv->store,
2826           individual_view_store_row_deleted_cb, self);
2827
2828       g_signal_handlers_disconnect_by_func (priv->filter,
2829           individual_view_row_has_child_toggled_cb, self);
2830
2831       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2832     }
2833
2834   tp_clear_object (&priv->filter);
2835   tp_clear_object (&priv->store);
2836
2837   /* Set the new store */
2838   priv->store = store;
2839
2840   if (store != NULL)
2841     {
2842       g_object_ref (store);
2843
2844       /* Create a new filter */
2845       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2846           GTK_TREE_MODEL (priv->store), NULL));
2847       gtk_tree_model_filter_set_visible_func (priv->filter,
2848           individual_view_filter_visible_func, self, NULL);
2849
2850       g_signal_connect (priv->filter, "row-has-child-toggled",
2851           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2852       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2853           GTK_TREE_MODEL (priv->filter));
2854
2855       tp_g_signal_connect_object (priv->store, "row-changed",
2856           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2857       tp_g_signal_connect_object (priv->store, "row-inserted",
2858           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2859       tp_g_signal_connect_object (priv->store, "row-deleted",
2860           G_CALLBACK (individual_view_store_row_deleted_cb), self, 0);
2861     }
2862 }
2863
2864 void
2865 empathy_individual_view_start_search (EmpathyIndividualView *self)
2866 {
2867   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2868
2869   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2870   g_return_if_fail (priv->search_widget != NULL);
2871
2872   if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
2873     gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
2874   else
2875     gtk_widget_show (GTK_WIDGET (priv->search_widget));
2876 }
2877
2878 void
2879 empathy_individual_view_set_custom_filter (EmpathyIndividualView *self,
2880     GtkTreeModelFilterVisibleFunc filter,
2881     gpointer data)
2882 {
2883   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2884
2885   priv->custom_filter = filter;
2886   priv->custom_filter_data = data;
2887 }
2888
2889 void
2890 empathy_individual_view_refilter (EmpathyIndividualView *self)
2891 {
2892   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2893
2894   gtk_tree_model_filter_refilter (priv->filter);
2895 }
2896
2897 void
2898 empathy_individual_view_select_first (EmpathyIndividualView *self)
2899 {
2900   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2901   GtkTreeIter iter;
2902
2903   gtk_tree_model_filter_refilter (priv->filter);
2904
2905   if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->filter), &iter))
2906     {
2907       GtkTreeSelection *selection = gtk_tree_view_get_selection (
2908           GTK_TREE_VIEW (self));
2909
2910       gtk_tree_selection_select_iter (selection, &iter);
2911     }
2912 }
2913
2914 void
2915 empathy_individual_view_set_show_uninteresting (EmpathyIndividualView *self,
2916     gboolean show_uninteresting)
2917 {
2918   EmpathyIndividualViewPriv *priv;
2919
2920   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2921
2922   priv = GET_PRIV (self);
2923
2924   priv->show_uninteresting = show_uninteresting;
2925
2926   g_object_notify (G_OBJECT (self), "show-uninteresting");
2927   gtk_tree_model_filter_refilter (priv->filter);
2928 }