]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
dad975b3c4a4d6d6ec3f8e72b1cd61ce11042f43
[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 static gboolean
1641 individual_view_is_visible_individual (EmpathyIndividualView *self,
1642     FolksIndividual *individual,
1643     gboolean is_online,
1644     gboolean is_searching,
1645     const gchar *group,
1646     gboolean is_fake_group,
1647     guint event_count)
1648 {
1649   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1650   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
1651   GeeSet *personas;
1652   GeeIterator *iter;
1653   gboolean is_favorite;
1654
1655   /* Always display individuals having pending events */
1656   if (event_count > 0)
1657     return TRUE;
1658
1659   /* We're only giving the visibility wrt filtering here, not things like
1660    * presence. */
1661   if (!priv->show_untrusted &&
1662       folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
1663     {
1664       return FALSE;
1665     }
1666
1667   if (!priv->show_uninteresting)
1668     {
1669       gboolean contains_interesting_persona = FALSE;
1670
1671       /* Hide all individuals which consist entirely of uninteresting
1672        * personas */
1673       personas = folks_individual_get_personas (individual);
1674       iter = gee_iterable_iterator (GEE_ITERABLE (personas));
1675       while (!contains_interesting_persona && gee_iterator_next (iter))
1676         {
1677           FolksPersona *persona = gee_iterator_get (iter);
1678
1679           if (empathy_folks_persona_is_interesting (persona))
1680             contains_interesting_persona = TRUE;
1681
1682           g_clear_object (&persona);
1683         }
1684       g_clear_object (&iter);
1685
1686       if (!contains_interesting_persona)
1687         return FALSE;
1688     }
1689
1690   is_favorite = folks_favourite_details_get_is_favourite (
1691       FOLKS_FAVOURITE_DETAILS (individual));
1692   if (!is_searching) {
1693     if (is_favorite && is_fake_group &&
1694         !tp_strdiff (group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
1695         /* Always display favorite contacts in the favorite group */
1696         return TRUE;
1697
1698     return (priv->show_offline || is_online);
1699   }
1700
1701   return empathy_individual_match_string (individual,
1702       empathy_live_search_get_text (live),
1703       empathy_live_search_get_words (live));
1704 }
1705
1706 static gchar *
1707 get_group (GtkTreeModel *model,
1708     GtkTreeIter *iter,
1709     gboolean *is_fake)
1710 {
1711   GtkTreeIter parent_iter;
1712   gchar *name = NULL;
1713
1714   *is_fake = FALSE;
1715
1716   if (!gtk_tree_model_iter_parent (model, &parent_iter, iter))
1717     return NULL;
1718
1719   gtk_tree_model_get (model, &parent_iter,
1720       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1721       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake,
1722       -1);
1723
1724   return name;
1725 }
1726
1727
1728 static gboolean
1729 individual_view_filter_visible_func (GtkTreeModel *model,
1730     GtkTreeIter *iter,
1731     gpointer user_data)
1732 {
1733   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1734   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1735   FolksIndividual *individual = NULL;
1736   gboolean is_group, is_separator, valid;
1737   GtkTreeIter child_iter;
1738   gboolean visible, is_online;
1739   gboolean is_searching = TRUE;
1740   guint event_count;
1741
1742   if (priv->custom_filter != NULL)
1743     return priv->custom_filter (model, iter, priv->custom_filter_data);
1744
1745   if (priv->search_widget == NULL ||
1746       !gtk_widget_get_visible (priv->search_widget))
1747      is_searching = FALSE;
1748
1749   gtk_tree_model_get (model, iter,
1750       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1751       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1752       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1753       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1754       EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
1755       -1);
1756
1757   if (individual != NULL)
1758     {
1759       gchar *group;
1760       gboolean is_fake_group;
1761
1762       group = get_group (model, iter, &is_fake_group);
1763
1764       visible = individual_view_is_visible_individual (self, individual,
1765           is_online, is_searching, group, is_fake_group, event_count);
1766
1767       g_object_unref (individual);
1768       g_free (group);
1769
1770       return visible;
1771     }
1772
1773   if (is_separator)
1774     return TRUE;
1775
1776   /* Not a contact, not a separator, must be a group */
1777   g_return_val_if_fail (is_group, FALSE);
1778
1779   /* only show groups which are not empty */
1780   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1781        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1782     {
1783       gchar *group;
1784       gboolean is_fake_group;
1785
1786       gtk_tree_model_get (model, &child_iter,
1787         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1788         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1789         EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &event_count,
1790         -1);
1791
1792       if (individual == NULL)
1793         continue;
1794
1795       group = get_group (model, &child_iter, &is_fake_group);
1796
1797       visible = individual_view_is_visible_individual (self, individual,
1798           is_online, is_searching, group, is_fake_group, event_count);
1799
1800       g_object_unref (individual);
1801       g_free (group);
1802
1803       /* show group if it has at least one visible contact in it */
1804       if (visible)
1805         return TRUE;
1806     }
1807
1808   return FALSE;
1809 }
1810
1811 static void
1812 individual_view_constructed (GObject *object)
1813 {
1814   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1815   GtkCellRenderer *cell;
1816   GtkTreeViewColumn *col;
1817   guint i;
1818
1819   /* Setup view */
1820   g_object_set (view,
1821       "headers-visible", FALSE,
1822       "show-expanders", FALSE,
1823       NULL);
1824
1825   col = gtk_tree_view_column_new ();
1826
1827   /* State */
1828   cell = gtk_cell_renderer_pixbuf_new ();
1829   gtk_tree_view_column_pack_start (col, cell, FALSE);
1830   gtk_tree_view_column_set_cell_data_func (col, cell,
1831       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1832       view, NULL);
1833
1834   g_object_set (cell,
1835       "xpad", 5,
1836       "ypad", 1,
1837       "visible", FALSE,
1838       NULL);
1839
1840   /* Group icon */
1841   cell = gtk_cell_renderer_pixbuf_new ();
1842   gtk_tree_view_column_pack_start (col, cell, FALSE);
1843   gtk_tree_view_column_set_cell_data_func (col, cell,
1844       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1845       view, NULL);
1846
1847   g_object_set (cell,
1848       "xpad", 0,
1849       "ypad", 0,
1850       "visible", FALSE,
1851       "width", 16,
1852       "height", 16,
1853       NULL);
1854
1855   /* Name */
1856   cell = empathy_cell_renderer_text_new ();
1857   gtk_tree_view_column_pack_start (col, cell, TRUE);
1858   gtk_tree_view_column_set_cell_data_func (col, cell,
1859       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1860
1861   gtk_tree_view_column_add_attribute (col, cell,
1862       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1863   gtk_tree_view_column_add_attribute (col, cell,
1864       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1865   gtk_tree_view_column_add_attribute (col, cell,
1866       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1867   gtk_tree_view_column_add_attribute (col, cell,
1868       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1869   gtk_tree_view_column_add_attribute (col, cell,
1870       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1871   gtk_tree_view_column_add_attribute (col, cell,
1872       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1873   gtk_tree_view_column_add_attribute (col, cell,
1874       "client-types", EMPATHY_INDIVIDUAL_STORE_COL_CLIENT_TYPES);
1875
1876   /* Audio Call Icon */
1877   cell = empathy_cell_renderer_activatable_new ();
1878   gtk_tree_view_column_pack_start (col, cell, FALSE);
1879   gtk_tree_view_column_set_cell_data_func (col, cell,
1880       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1881       view, NULL);
1882
1883   g_object_set (cell, "visible", FALSE, NULL);
1884
1885   g_signal_connect (cell, "path-activated",
1886       G_CALLBACK (individual_view_call_activated_cb), view);
1887
1888   /* Avatar */
1889   cell = gtk_cell_renderer_pixbuf_new ();
1890   gtk_tree_view_column_pack_start (col, cell, FALSE);
1891   gtk_tree_view_column_set_cell_data_func (col, cell,
1892       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1893       view, NULL);
1894
1895   g_object_set (cell,
1896       "xpad", 0,
1897       "ypad", 0,
1898       "visible", FALSE,
1899       "width", 32,
1900       "height", 32,
1901       NULL);
1902
1903   /* Expander */
1904   cell = empathy_cell_renderer_expander_new ();
1905   gtk_tree_view_column_pack_end (col, cell, FALSE);
1906   gtk_tree_view_column_set_cell_data_func (col, cell,
1907       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1908       view, NULL);
1909
1910   /* Actually add the column now we have added all cell renderers */
1911   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1912
1913   /* Drag & Drop. */
1914   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1915     {
1916       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1917     }
1918 }
1919
1920 static void
1921 individual_view_set_view_features (EmpathyIndividualView *view,
1922     EmpathyIndividualFeatureFlags features)
1923 {
1924   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1925   gboolean has_tooltip;
1926
1927   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1928
1929   priv->view_features = features;
1930
1931   /* Setting reorderable is a hack that gets us row previews as drag icons
1932      for free.  We override all the drag handlers.  It's tricky to get the
1933      position of the drag icon right in drag_begin.  GtkTreeView has special
1934      voodoo for it, so we let it do the voodoo that he do (but only if dragging
1935      is enabled).
1936    */
1937   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
1938       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG));
1939
1940   /* Update DnD source/dest */
1941   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG)
1942     {
1943       gtk_drag_source_set (GTK_WIDGET (view),
1944           GDK_BUTTON1_MASK,
1945           drag_types_source,
1946           G_N_ELEMENTS (drag_types_source),
1947           GDK_ACTION_MOVE | GDK_ACTION_COPY);
1948     }
1949   else
1950     {
1951       gtk_drag_source_unset (GTK_WIDGET (view));
1952
1953     }
1954
1955   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP)
1956     {
1957       gtk_drag_dest_set (GTK_WIDGET (view),
1958           GTK_DEST_DEFAULT_ALL,
1959           drag_types_dest,
1960           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
1961     }
1962   else
1963     {
1964       /* FIXME: URI could still be droped depending on FT feature */
1965       gtk_drag_dest_unset (GTK_WIDGET (view));
1966     }
1967
1968   /* Update has-tooltip */
1969   has_tooltip =
1970       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP) != 0;
1971   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1972 }
1973
1974 static void
1975 individual_view_dispose (GObject *object)
1976 {
1977   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1978   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1979
1980   tp_clear_object (&priv->store);
1981   tp_clear_object (&priv->filter);
1982   tp_clear_object (&priv->tooltip_widget);
1983
1984   empathy_individual_view_set_live_search (view, NULL);
1985
1986   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
1987 }
1988
1989 static void
1990 individual_view_finalize (GObject *object)
1991 {
1992   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1993
1994   if (priv->expand_groups_idle_handler != 0)
1995     g_source_remove (priv->expand_groups_idle_handler);
1996   g_hash_table_unref (priv->expand_groups);
1997
1998   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
1999 }
2000
2001 static void
2002 individual_view_get_property (GObject *object,
2003     guint param_id,
2004     GValue *value,
2005     GParamSpec *pspec)
2006 {
2007   EmpathyIndividualViewPriv *priv;
2008
2009   priv = GET_PRIV (object);
2010
2011   switch (param_id)
2012     {
2013     case PROP_STORE:
2014       g_value_set_object (value, priv->store);
2015       break;
2016     case PROP_VIEW_FEATURES:
2017       g_value_set_flags (value, priv->view_features);
2018       break;
2019     case PROP_INDIVIDUAL_FEATURES:
2020       g_value_set_flags (value, priv->individual_features);
2021       break;
2022     case PROP_SHOW_OFFLINE:
2023       g_value_set_boolean (value, priv->show_offline);
2024       break;
2025     case PROP_SHOW_UNTRUSTED:
2026       g_value_set_boolean (value, priv->show_untrusted);
2027       break;
2028     case PROP_SHOW_UNINTERESTING:
2029       g_value_set_boolean (value, priv->show_uninteresting);
2030       break;
2031     default:
2032       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2033       break;
2034     };
2035 }
2036
2037 static void
2038 individual_view_set_property (GObject *object,
2039     guint param_id,
2040     const GValue *value,
2041     GParamSpec *pspec)
2042 {
2043   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
2044   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
2045
2046   switch (param_id)
2047     {
2048     case PROP_STORE:
2049       empathy_individual_view_set_store (view, g_value_get_object (value));
2050       break;
2051     case PROP_VIEW_FEATURES:
2052       individual_view_set_view_features (view, g_value_get_flags (value));
2053       break;
2054     case PROP_INDIVIDUAL_FEATURES:
2055       priv->individual_features = g_value_get_flags (value);
2056       break;
2057     case PROP_SHOW_OFFLINE:
2058       empathy_individual_view_set_show_offline (view,
2059           g_value_get_boolean (value));
2060       break;
2061     case PROP_SHOW_UNTRUSTED:
2062       empathy_individual_view_set_show_untrusted (view,
2063           g_value_get_boolean (value));
2064       break;
2065     case PROP_SHOW_UNINTERESTING:
2066       empathy_individual_view_set_show_uninteresting (view,
2067           g_value_get_boolean (value));
2068     default:
2069       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
2070       break;
2071     };
2072 }
2073
2074 static void
2075 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
2076 {
2077   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2078   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2079   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
2080
2081   object_class->constructed = individual_view_constructed;
2082   object_class->dispose = individual_view_dispose;
2083   object_class->finalize = individual_view_finalize;
2084   object_class->get_property = individual_view_get_property;
2085   object_class->set_property = individual_view_set_property;
2086
2087   widget_class->drag_data_received = individual_view_drag_data_received;
2088   widget_class->drag_drop = individual_view_drag_drop;
2089   widget_class->drag_begin = individual_view_drag_begin;
2090   widget_class->drag_data_get = individual_view_drag_data_get;
2091   widget_class->drag_end = individual_view_drag_end;
2092   widget_class->drag_motion = individual_view_drag_motion;
2093
2094   /* We use the class method to let user of this widget to connect to
2095    * the signal and stop emission of the signal so the default handler
2096    * won't be called. */
2097   tree_view_class->row_activated = individual_view_row_activated;
2098
2099   klass->drag_individual_received = real_drag_individual_received_cb;
2100
2101   signals[DRAG_INDIVIDUAL_RECEIVED] =
2102       g_signal_new ("drag-individual-received",
2103       G_OBJECT_CLASS_TYPE (klass),
2104       G_SIGNAL_RUN_LAST,
2105       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_individual_received),
2106       NULL, NULL,
2107       g_cclosure_marshal_generic,
2108       G_TYPE_NONE, 4, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL,
2109       G_TYPE_STRING, G_TYPE_STRING);
2110
2111   signals[DRAG_PERSONA_RECEIVED] =
2112       g_signal_new ("drag-persona-received",
2113       G_OBJECT_CLASS_TYPE (klass),
2114       G_SIGNAL_RUN_LAST,
2115       G_STRUCT_OFFSET (EmpathyIndividualViewClass, drag_persona_received),
2116       NULL, NULL,
2117       g_cclosure_marshal_generic,
2118       G_TYPE_BOOLEAN, 3, G_TYPE_UINT, FOLKS_TYPE_PERSONA, FOLKS_TYPE_INDIVIDUAL);
2119
2120   g_object_class_install_property (object_class,
2121       PROP_STORE,
2122       g_param_spec_object ("store",
2123           "The store of the view",
2124           "The store of the view",
2125           EMPATHY_TYPE_INDIVIDUAL_STORE,
2126           G_PARAM_READWRITE));
2127   g_object_class_install_property (object_class,
2128       PROP_VIEW_FEATURES,
2129       g_param_spec_flags ("view-features",
2130           "Features of the view",
2131           "Flags for all enabled features",
2132           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
2133           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
2134   g_object_class_install_property (object_class,
2135       PROP_INDIVIDUAL_FEATURES,
2136       g_param_spec_flags ("individual-features",
2137           "Features of the individual menu",
2138           "Flags for all enabled features for the menu",
2139           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
2140           EMPATHY_INDIVIDUAL_FEATURE_NONE, G_PARAM_READWRITE));
2141   g_object_class_install_property (object_class,
2142       PROP_SHOW_OFFLINE,
2143       g_param_spec_boolean ("show-offline",
2144           "Show Offline",
2145           "Whether contact list should display "
2146           "offline contacts", FALSE, G_PARAM_READWRITE));
2147   g_object_class_install_property (object_class,
2148       PROP_SHOW_UNTRUSTED,
2149       g_param_spec_boolean ("show-untrusted",
2150           "Show Untrusted Individuals",
2151           "Whether the view should display untrusted individuals; "
2152           "those who could not be who they say they are.",
2153           TRUE, G_PARAM_READWRITE));
2154   g_object_class_install_property (object_class,
2155       PROP_SHOW_UNINTERESTING,
2156       g_param_spec_boolean ("show-uninteresting",
2157           "Show Uninteresting Individuals",
2158           "Whether the view should not filter out individuals using "
2159           "empathy_folks_persona_is_interesting.",
2160           FALSE, G_PARAM_READWRITE));
2161
2162   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
2163 }
2164
2165 static void
2166 empathy_individual_view_init (EmpathyIndividualView *view)
2167 {
2168   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
2169       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
2170
2171   view->priv = priv;
2172
2173   priv->show_untrusted = TRUE;
2174   priv->show_uninteresting = FALSE;
2175
2176   /* Get saved group states. */
2177   empathy_contact_groups_get_all ();
2178
2179   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
2180       (GDestroyNotify) g_free, NULL);
2181
2182   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
2183       empathy_individual_store_row_separator_func, NULL, NULL);
2184
2185   /* Connect to tree view signals rather than override. */
2186   g_signal_connect (view, "button-press-event",
2187       G_CALLBACK (individual_view_button_press_event_cb), NULL);
2188   g_signal_connect (view, "key-press-event",
2189       G_CALLBACK (individual_view_key_press_event_cb), NULL);
2190   g_signal_connect (view, "row-expanded",
2191       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2192       GINT_TO_POINTER (TRUE));
2193   g_signal_connect (view, "row-collapsed",
2194       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
2195       GINT_TO_POINTER (FALSE));
2196   g_signal_connect (view, "query-tooltip",
2197       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
2198 }
2199
2200 EmpathyIndividualView *
2201 empathy_individual_view_new (EmpathyIndividualStore *store,
2202     EmpathyIndividualViewFeatureFlags view_features,
2203     EmpathyIndividualFeatureFlags individual_features)
2204 {
2205   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
2206
2207   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
2208       "store", store,
2209       "individual-features", individual_features,
2210       "view-features", view_features, NULL);
2211 }
2212
2213 FolksIndividual *
2214 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
2215 {
2216   GtkTreeSelection *selection;
2217   GtkTreeIter iter;
2218   GtkTreeModel *model;
2219   FolksIndividual *individual;
2220
2221   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2222
2223   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2224   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2225     return NULL;
2226
2227   gtk_tree_model_get (model, &iter,
2228       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
2229
2230   return individual;
2231 }
2232
2233 static gchar *
2234 empathy_individual_view_dup_selected_group (EmpathyIndividualView *view,
2235     gboolean *is_fake_group)
2236 {
2237   GtkTreeSelection *selection;
2238   GtkTreeIter iter;
2239   GtkTreeModel *model;
2240   gboolean is_group;
2241   gchar *name;
2242   gboolean fake;
2243
2244   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2245
2246   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2247   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2248     return NULL;
2249
2250   gtk_tree_model_get (model, &iter,
2251       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2252       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2253       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2254
2255   if (!is_group)
2256     {
2257       g_free (name);
2258       return NULL;
2259     }
2260
2261   if (is_fake_group != NULL)
2262     *is_fake_group = fake;
2263
2264   return name;
2265 }
2266
2267 enum
2268 {
2269   REMOVE_DIALOG_RESPONSE_CANCEL = 0,
2270   REMOVE_DIALOG_RESPONSE_DELETE,
2271   REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK,
2272 };
2273
2274 static int
2275 individual_view_remove_dialog_show (GtkWindow *parent,
2276     const gchar *message,
2277     const gchar *secondary_text,
2278     gboolean block_button,
2279     GdkPixbuf *avatar)
2280 {
2281   GtkWidget *dialog;
2282   gboolean res;
2283
2284   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2285       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2286
2287   if (avatar != NULL)
2288     {
2289       GtkWidget *image = gtk_image_new_from_pixbuf (avatar);
2290       gtk_message_dialog_set_image (GTK_MESSAGE_DIALOG (dialog), image);
2291       gtk_widget_show (image);
2292     }
2293
2294   if (block_button)
2295     {
2296       GtkWidget *button;
2297
2298       /* gtk_dialog_add_button() doesn't allow us to pass a string with a
2299        * mnemonic so we have to create the button manually. */
2300       button = gtk_button_new_with_mnemonic (
2301           _("Delete and _Block"));
2302
2303       gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button,
2304           REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK);
2305
2306       gtk_widget_show (button);
2307     }
2308
2309   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2310       GTK_STOCK_CANCEL, REMOVE_DIALOG_RESPONSE_CANCEL,
2311       GTK_STOCK_DELETE, REMOVE_DIALOG_RESPONSE_DELETE, NULL);
2312   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2313       "%s", secondary_text);
2314
2315   gtk_widget_show (dialog);
2316
2317   res = gtk_dialog_run (GTK_DIALOG (dialog));
2318   gtk_widget_destroy (dialog);
2319
2320   return res;
2321 }
2322
2323 static void
2324 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2325     EmpathyIndividualView *view)
2326 {
2327   gchar *group;
2328
2329   group = empathy_individual_view_dup_selected_group (view, NULL);
2330   if (group != NULL)
2331     {
2332       gchar *text;
2333       GtkWindow *parent;
2334
2335       text =
2336           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2337           group);
2338       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2339       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2340               text, FALSE, NULL) == REMOVE_DIALOG_RESPONSE_DELETE)
2341         {
2342           EmpathyIndividualManager *manager =
2343               empathy_individual_manager_dup_singleton ();
2344           empathy_individual_manager_remove_group (manager, group);
2345           g_object_unref (G_OBJECT (manager));
2346         }
2347
2348       g_free (text);
2349     }
2350
2351   g_free (group);
2352 }
2353
2354 GtkWidget *
2355 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2356 {
2357   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2358   gchar *group;
2359   GtkWidget *menu;
2360   GtkWidget *item;
2361   GtkWidget *image;
2362   gboolean is_fake_group;
2363
2364   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2365
2366   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2367               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2368     return NULL;
2369
2370   group = empathy_individual_view_dup_selected_group (view, &is_fake_group);
2371   if (!group || is_fake_group)
2372     {
2373       /* We can't alter fake groups */
2374       g_free (group);
2375       return NULL;
2376     }
2377
2378   menu = gtk_menu_new ();
2379
2380   /* TODO: implement
2381      if (priv->view_features &
2382      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2383      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2384      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2385      gtk_widget_show (item);
2386      g_signal_connect (item, "activate",
2387      G_CALLBACK (individual_view_group_rename_activate_cb),
2388      view);
2389      }
2390    */
2391
2392   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2393     {
2394       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2395       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2396           GTK_ICON_SIZE_MENU);
2397       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2398       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2399       gtk_widget_show (item);
2400       g_signal_connect (item, "activate",
2401           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2402     }
2403
2404   g_free (group);
2405
2406   return menu;
2407 }
2408
2409 static void
2410 got_avatar (GObject *source_object,
2411     GAsyncResult *result,
2412     gpointer user_data)
2413 {
2414   FolksIndividual *individual = FOLKS_INDIVIDUAL (source_object);
2415   EmpathyIndividualView *view = user_data;
2416   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2417   GdkPixbuf *avatar;
2418   EmpathyIndividualManager *manager;
2419   gchar *text;
2420   GtkWindow *parent;
2421   GeeSet *personas;
2422   guint persona_count = 0;
2423   gboolean can_block;
2424   GError *error = NULL;
2425   gint res;
2426
2427   avatar = empathy_pixbuf_avatar_from_individual_scaled_finish (individual,
2428       result, &error);
2429
2430   if (error != NULL)
2431     {
2432       DEBUG ("Could not get avatar: %s", error->message);
2433       g_error_free (error);
2434     }
2435
2436   /* We couldn't retrieve the avatar, but that isn't a fatal error,
2437    * so we still display the remove dialog. */
2438
2439   personas = folks_individual_get_personas (individual);
2440
2441   if (priv->show_uninteresting)
2442     {
2443       persona_count = gee_collection_get_size (GEE_COLLECTION (personas));
2444     }
2445   else
2446     {
2447       GeeIterator *iter;
2448
2449       iter = gee_iterable_iterator (GEE_ITERABLE (personas));
2450       while (persona_count < 2 && gee_iterator_next (iter))
2451         {
2452           FolksPersona *persona = gee_iterator_get (iter);
2453
2454           if (empathy_folks_persona_is_interesting (persona))
2455             persona_count++;
2456
2457           g_clear_object (&persona);
2458         }
2459       g_clear_object (&iter);
2460     }
2461
2462   /* If we have more than one TpfPersona, display a different message
2463    * ensuring the user knows that *all* of the meta-contacts' personas will
2464    * be removed. */
2465
2466   if (persona_count < 2)
2467     {
2468       /* Not a meta-contact */
2469       text =
2470           g_strdup_printf (
2471               _("Do you really want to remove the contact '%s'?"),
2472               folks_alias_details_get_alias (
2473                   FOLKS_ALIAS_DETAILS (individual)));
2474     }
2475   else
2476     {
2477       /* Meta-contact */
2478       text =
2479           g_strdup_printf (
2480               _("Do you really want to remove the linked contact '%s'? "
2481                 "Note that this will remove all the contacts which make up "
2482                 "this linked contact."),
2483               folks_alias_details_get_alias (
2484                   FOLKS_ALIAS_DETAILS (individual)));
2485     }
2486
2487
2488   manager = empathy_individual_manager_dup_singleton ();
2489   can_block = empathy_individual_manager_supports_blocking (manager,
2490       individual);
2491   parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2492   res = individual_view_remove_dialog_show (parent, _("Removing contact"),
2493           text, can_block, avatar);
2494
2495   if (res == REMOVE_DIALOG_RESPONSE_DELETE ||
2496       res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2497     {
2498       gboolean abusive;
2499
2500       if (res == REMOVE_DIALOG_RESPONSE_DELETE_AND_BLOCK)
2501         {
2502           if (!empathy_block_individual_dialog_show (parent, individual,
2503                 avatar, &abusive))
2504             goto finally;
2505
2506           empathy_individual_manager_set_blocked (manager, individual,
2507               TRUE, abusive);
2508         }
2509
2510       empathy_individual_manager_remove (manager, individual, "");
2511     }
2512
2513  finally:
2514   g_free (text);
2515   g_object_unref (manager);
2516 }
2517
2518 static void
2519 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2520     EmpathyIndividualView *view)
2521 {
2522   FolksIndividual *individual;
2523
2524   individual = empathy_individual_view_dup_selected (view);
2525
2526   if (individual != NULL)
2527     {
2528       empathy_pixbuf_avatar_from_individual_scaled_async (individual,
2529           48, 48, NULL, got_avatar, view);
2530       g_object_unref (individual);
2531     }
2532 }
2533
2534 static void
2535 individual_menu_link_contacts_activated_cb (EmpathyIndividualMenu *menu,
2536     EmpathyLinkingDialog *linking_dialog,
2537     EmpathyIndividualView *self)
2538 {
2539   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2540   EmpathyIndividualLinker *linker;
2541
2542   linker = empathy_linking_dialog_get_individual_linker (linking_dialog);
2543   empathy_individual_linker_set_search_text (linker,
2544       empathy_live_search_get_text (EMPATHY_LIVE_SEARCH (priv->search_widget)));
2545 }
2546
2547 GtkWidget *
2548 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2549 {
2550   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2551   FolksIndividual *individual;
2552   GtkWidget *menu = NULL;
2553   GtkWidget *item;
2554   GtkWidget *image;
2555   gboolean can_remove = FALSE;
2556   GeeSet *personas;
2557   GeeIterator *iter;
2558
2559   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2560
2561   if (priv->individual_features == EMPATHY_INDIVIDUAL_FEATURE_NONE)
2562     /* No need to create a context menu */
2563     return NULL;
2564
2565   individual = empathy_individual_view_dup_selected (view);
2566   if (individual == NULL)
2567     return NULL;
2568
2569   if (!empathy_folks_individual_contains_contact (individual))
2570     goto out;
2571
2572   /* If any of the Individual's personas can be removed, add an option to
2573    * remove. This will act as a best-effort option. If any Personas cannot be
2574    * removed from the server, then this option will just be inactive upon
2575    * subsequent menu openings */
2576   personas = folks_individual_get_personas (individual);
2577   iter = gee_iterable_iterator (GEE_ITERABLE (personas));
2578   while (!can_remove && gee_iterator_next (iter))
2579     {
2580       FolksPersona *persona = gee_iterator_get (iter);
2581       FolksPersonaStore *store = folks_persona_get_store (persona);
2582       FolksMaybeBool maybe_can_remove =
2583           folks_persona_store_get_can_remove_personas (store);
2584
2585       if (maybe_can_remove == FOLKS_MAYBE_BOOL_TRUE)
2586         can_remove = TRUE;
2587
2588       g_clear_object (&persona);
2589     }
2590   g_clear_object (&iter);
2591
2592   menu = empathy_individual_menu_new (individual, priv->individual_features,
2593       priv->store);
2594
2595   /* Remove contact */
2596   if ((priv->view_features &
2597       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE) &&
2598       can_remove)
2599     {
2600       /* create the menu if required, or just add a separator */
2601       if (menu == NULL)
2602         menu = gtk_menu_new ();
2603       else
2604         {
2605           item = gtk_separator_menu_item_new ();
2606           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2607           gtk_widget_show (item);
2608         }
2609
2610       /* Remove */
2611       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2612       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2613           GTK_ICON_SIZE_MENU);
2614       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2615       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2616       gtk_widget_show (item);
2617       g_signal_connect (item, "activate",
2618           G_CALLBACK (individual_view_remove_activate_cb), view);
2619     }
2620
2621   /* Connect to EmpathyIndividualMenu::link-contacts-activated so that we can
2622    * set the live search text on the new linking dialogue to be the same as
2623    * our own. */
2624   g_signal_connect (menu, "link-contacts-activated",
2625       (GCallback) individual_menu_link_contacts_activated_cb, view);
2626
2627 out:
2628   g_object_unref (individual);
2629
2630   return menu;
2631 }
2632
2633 void
2634 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2635     EmpathyLiveSearch *search)
2636 {
2637   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2638
2639   /* remove old handlers if old search was not null */
2640   if (priv->search_widget != NULL)
2641     {
2642       g_signal_handlers_disconnect_by_func (view,
2643           individual_view_start_search_cb, NULL);
2644
2645       g_signal_handlers_disconnect_by_func (priv->search_widget,
2646           individual_view_search_text_notify_cb, view);
2647       g_signal_handlers_disconnect_by_func (priv->search_widget,
2648           individual_view_search_activate_cb, view);
2649       g_signal_handlers_disconnect_by_func (priv->search_widget,
2650           individual_view_search_key_navigation_cb, view);
2651       g_signal_handlers_disconnect_by_func (priv->search_widget,
2652           individual_view_search_hide_cb, view);
2653       g_signal_handlers_disconnect_by_func (priv->search_widget,
2654           individual_view_search_show_cb, view);
2655       g_object_unref (priv->search_widget);
2656       priv->search_widget = NULL;
2657     }
2658
2659   /* connect handlers if new search is not null */
2660   if (search != NULL)
2661     {
2662       priv->search_widget = g_object_ref (search);
2663
2664       g_signal_connect (view, "start-interactive-search",
2665           G_CALLBACK (individual_view_start_search_cb), NULL);
2666
2667       g_signal_connect (priv->search_widget, "notify::text",
2668           G_CALLBACK (individual_view_search_text_notify_cb), view);
2669       g_signal_connect (priv->search_widget, "activate",
2670           G_CALLBACK (individual_view_search_activate_cb), view);
2671       g_signal_connect (priv->search_widget, "key-navigation",
2672           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2673       g_signal_connect (priv->search_widget, "hide",
2674           G_CALLBACK (individual_view_search_hide_cb), view);
2675       g_signal_connect (priv->search_widget, "show",
2676           G_CALLBACK (individual_view_search_show_cb), view);
2677     }
2678 }
2679
2680 gboolean
2681 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2682 {
2683   EmpathyIndividualViewPriv *priv;
2684
2685   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2686
2687   priv = GET_PRIV (self);
2688
2689   return (priv->search_widget != NULL &&
2690           gtk_widget_get_visible (priv->search_widget));
2691 }
2692
2693 gboolean
2694 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2695 {
2696   EmpathyIndividualViewPriv *priv;
2697
2698   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2699
2700   priv = GET_PRIV (self);
2701
2702   return priv->show_offline;
2703 }
2704
2705 void
2706 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2707     gboolean show_offline)
2708 {
2709   EmpathyIndividualViewPriv *priv;
2710
2711   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2712
2713   priv = GET_PRIV (self);
2714
2715   priv->show_offline = show_offline;
2716
2717   g_object_notify (G_OBJECT (self), "show-offline");
2718   gtk_tree_model_filter_refilter (priv->filter);
2719 }
2720
2721 gboolean
2722 empathy_individual_view_get_show_untrusted (EmpathyIndividualView *self)
2723 {
2724   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2725
2726   return GET_PRIV (self)->show_untrusted;
2727 }
2728
2729 void
2730 empathy_individual_view_set_show_untrusted (EmpathyIndividualView *self,
2731     gboolean show_untrusted)
2732 {
2733   EmpathyIndividualViewPriv *priv;
2734
2735   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2736
2737   priv = GET_PRIV (self);
2738
2739   priv->show_untrusted = show_untrusted;
2740
2741   g_object_notify (G_OBJECT (self), "show-untrusted");
2742   gtk_tree_model_filter_refilter (priv->filter);
2743 }
2744
2745 EmpathyIndividualStore *
2746 empathy_individual_view_get_store (EmpathyIndividualView *self)
2747 {
2748   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2749
2750   return GET_PRIV (self)->store;
2751 }
2752
2753 void
2754 empathy_individual_view_set_store (EmpathyIndividualView *self,
2755     EmpathyIndividualStore *store)
2756 {
2757   EmpathyIndividualViewPriv *priv;
2758
2759   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2760   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2761
2762   priv = GET_PRIV (self);
2763
2764   /* Destroy the old filter and remove the old store */
2765   if (priv->store != NULL)
2766     {
2767       g_signal_handlers_disconnect_by_func (priv->filter,
2768           individual_view_row_has_child_toggled_cb, self);
2769
2770       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2771     }
2772
2773   tp_clear_object (&priv->filter);
2774   tp_clear_object (&priv->store);
2775
2776   /* Set the new store */
2777   priv->store = store;
2778
2779   if (store != NULL)
2780     {
2781       g_object_ref (store);
2782
2783       /* Create a new filter */
2784       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2785           GTK_TREE_MODEL (priv->store), NULL));
2786       gtk_tree_model_filter_set_visible_func (priv->filter,
2787           individual_view_filter_visible_func, self, NULL);
2788
2789       g_signal_connect (priv->filter, "row-has-child-toggled",
2790           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2791       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2792           GTK_TREE_MODEL (priv->filter));
2793     }
2794 }
2795
2796 void
2797 empathy_individual_view_start_search (EmpathyIndividualView *self)
2798 {
2799   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2800
2801   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2802   g_return_if_fail (priv->search_widget != NULL);
2803
2804   if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
2805     gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
2806   else
2807     gtk_widget_show (GTK_WIDGET (priv->search_widget));
2808 }
2809
2810 void
2811 empathy_individual_view_set_custom_filter (EmpathyIndividualView *self,
2812     GtkTreeModelFilterVisibleFunc filter,
2813     gpointer data)
2814 {
2815   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2816
2817   priv->custom_filter = filter;
2818   priv->custom_filter_data = data;
2819 }
2820
2821 void
2822 empathy_individual_view_refilter (EmpathyIndividualView *self)
2823 {
2824   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2825
2826   gtk_tree_model_filter_refilter (priv->filter);
2827 }
2828
2829 void
2830 empathy_individual_view_select_first (EmpathyIndividualView *self)
2831 {
2832   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
2833   GtkTreeIter iter;
2834
2835   gtk_tree_model_filter_refilter (priv->filter);
2836
2837   if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->filter), &iter))
2838     {
2839       GtkTreeSelection *selection = gtk_tree_view_get_selection (
2840           GTK_TREE_VIEW (self));
2841
2842       gtk_tree_selection_select_iter (selection, &iter);
2843     }
2844 }
2845
2846 void
2847 empathy_individual_view_set_show_uninteresting (EmpathyIndividualView *self,
2848     gboolean show_uninteresting)
2849 {
2850   EmpathyIndividualViewPriv *priv;
2851
2852   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2853
2854   priv = GET_PRIV (self);
2855
2856   priv->show_uninteresting = show_uninteresting;
2857
2858   g_object_notify (G_OBJECT (self), "show-uninteresting");
2859   gtk_tree_model_filter_refilter (priv->filter);
2860 }