]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
empathy-roster-model-manager: added xmpp-local contacts support empathy-roster-view...
[empathy.git] / libempathy-gtk / empathy-roster-view.c
1 #include "config.h"
2
3 #include "empathy-roster-view.h"
4
5 #include <glib/gi18n-lib.h>
6
7 #include <libempathy-gtk/empathy-roster-contact.h>
8 #include <libempathy-gtk/empathy-roster-group.h>
9 #include <libempathy-gtk/empathy-ui-utils.h>
10
11 #include <libempathy/empathy-utils.h>
12
13 G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, EGG_TYPE_LIST_BOX)
14
15 /* Flashing delay for icons (milliseconds). */
16 #define FLASH_TIMEOUT 500
17
18 enum
19 {
20   PROP_MANAGER = 1,
21   PROP_MODEL,
22   PROP_SHOW_OFFLINE,
23   PROP_SHOW_GROUPS,
24   PROP_EMPTY,
25   N_PROPS
26 };
27
28 enum
29 {
30   SIG_INDIVIDUAL_ACTIVATED,
31   SIG_POPUP_INDIVIDUAL_MENU,
32   SIG_EVENT_ACTIVATED,
33   SIG_INDIVIDUAL_TOOLTIP,
34   LAST_SIGNAL
35 };
36
37 static guint signals[LAST_SIGNAL];
38
39 #define NO_GROUP "X-no-group"
40
41 struct _EmpathyRosterViewPriv
42 {
43   EmpathyIndividualManager *manager;
44
45   /* FolksIndividual (borrowed) -> GHashTable (
46    * (gchar * group_name) -> EmpathyRosterContact (borrowed))
47    *
48    * When not using groups, this hash just have one element mapped
49    * from the special NO_GROUP key. We could use it as a set but
50    * I prefer to stay coherent in the way this hash is managed.
51    */
52   GHashTable *roster_contacts;
53   /* (gchar *group_name) -> EmpathyRosterGroup (borrowed) */
54   GHashTable *roster_groups;
55   /* Hash of the EmpathyRosterContact currently displayed */
56   GHashTable *displayed_contacts;
57
58   guint last_event_id;
59   /* queue of (Event *). The most recent events are in the head of the queue
60    * so we always display the icon of the oldest one. */
61   GQueue *events;
62   guint flash_id;
63   gboolean display_flash_event;
64
65   gboolean show_offline;
66   gboolean show_groups;
67   gboolean empty;
68
69   EmpathyLiveSearch *search;
70
71   EmpathyRosterModel *model;
72 };
73
74 typedef struct
75 {
76   guint id;
77   FolksIndividual *individual;
78   gchar *icon;
79   gpointer user_data;
80 } Event;
81
82 static Event *
83 event_new (guint id,
84     FolksIndividual *individual,
85     const gchar *icon,
86     gpointer user_data)
87 {
88   Event *event = g_slice_new (Event);
89
90   event->id = id;
91   event->individual = g_object_ref (individual);
92   event->icon = g_strdup (icon);
93   event->user_data = user_data;
94   return event;
95 }
96
97 static void
98 event_free (gpointer data)
99 {
100   Event *event = data;
101   g_object_unref (event->individual);
102   g_free (event->icon);
103
104   g_slice_free (Event, event);
105 }
106
107 static void
108 empathy_roster_view_get_property (GObject *object,
109     guint property_id,
110     GValue *value,
111     GParamSpec *pspec)
112 {
113   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
114
115   switch (property_id)
116     {
117       case PROP_MANAGER:
118         g_value_set_object (value, self->priv->manager);
119         break;
120       case PROP_MODEL:
121         g_value_set_object (value, self->priv->model);
122         break;
123       case PROP_SHOW_OFFLINE:
124         g_value_set_boolean (value, self->priv->show_offline);
125         break;
126       case PROP_SHOW_GROUPS:
127         g_value_set_boolean (value, self->priv->show_groups);
128         break;
129       case PROP_EMPTY:
130         g_value_set_boolean (value, self->priv->empty);
131         break;
132       default:
133         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
134         break;
135     }
136 }
137
138 static void
139 empathy_roster_view_set_property (GObject *object,
140     guint property_id,
141     const GValue *value,
142     GParamSpec *pspec)
143 {
144   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
145
146   switch (property_id)
147     {
148       case PROP_MANAGER:
149         g_assert (self->priv->manager == NULL); /* construct only */
150         self->priv->manager = g_value_dup_object (value);
151         break;
152       case PROP_MODEL:
153         g_assert (self->priv->model == NULL);
154         self->priv->model = g_value_dup_object (value);
155         break;
156       case PROP_SHOW_OFFLINE:
157         empathy_roster_view_show_offline (self, g_value_get_boolean (value));
158         break;
159       case PROP_SHOW_GROUPS:
160         empathy_roster_view_show_groups (self, g_value_get_boolean (value));
161         break;
162       default:
163         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
164         break;
165     }
166 }
167
168 static void
169 roster_contact_changed_cb (GtkWidget *child,
170     GParamSpec *spec,
171     EmpathyRosterView *self)
172 {
173   egg_list_box_child_changed (EGG_LIST_BOX (self), child);
174 }
175
176 static GtkWidget *
177 add_roster_contact (EmpathyRosterView *self,
178     FolksIndividual *individual,
179     const gchar *group)
180 {
181   GtkWidget *contact;
182
183   contact = empathy_roster_contact_new (individual, group);
184
185   /* Need to refilter if online is changed */
186   g_signal_connect (contact, "notify::online",
187       G_CALLBACK (roster_contact_changed_cb), self);
188
189   /* Need to resort if alias is changed */
190   g_signal_connect (contact, "notify::alias",
191       G_CALLBACK (roster_contact_changed_cb), self);
192
193   gtk_widget_show (contact);
194   gtk_container_add (GTK_CONTAINER (self), contact);
195
196   return contact;
197 }
198
199 static void
200 group_expanded_cb (EmpathyRosterGroup *group,
201     GParamSpec *spec,
202     EmpathyRosterView *self)
203 {
204   GList *widgets, *l;
205
206   widgets = empathy_roster_group_get_widgets (group);
207   for (l = widgets; l != NULL; l = g_list_next (l))
208     {
209       egg_list_box_child_changed (EGG_LIST_BOX (self), l->data);
210     }
211
212   g_list_free (widgets);
213 }
214
215 static EmpathyRosterGroup *
216 lookup_roster_group (EmpathyRosterView *self,
217     const gchar *group)
218 {
219   return g_hash_table_lookup (self->priv->roster_groups, group);
220 }
221
222 static EmpathyRosterGroup *
223 ensure_roster_group (EmpathyRosterView *self,
224     const gchar *group)
225 {
226   GtkWidget *roster_group;
227
228   roster_group = (GtkWidget *) lookup_roster_group (self, group);
229   if (roster_group != NULL)
230     return EMPATHY_ROSTER_GROUP (roster_group);
231
232   if (!tp_strdiff (group, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
233     roster_group = empathy_roster_group_new (group, "emblem-favorite-symbolic");
234   else if (!tp_strdiff (group, EMPATHY_ROSTER_VIEW_GROUP_PEOPLE_NEARBY))
235     roster_group = empathy_roster_group_new (group, "im-local-xmpp");
236   else
237     roster_group = empathy_roster_group_new (group, NULL);
238
239   g_signal_connect (roster_group, "notify::expanded",
240       G_CALLBACK (group_expanded_cb), self);
241
242   gtk_widget_show (roster_group);
243   gtk_container_add (GTK_CONTAINER (self), roster_group);
244
245   g_hash_table_insert (self->priv->roster_groups, g_strdup (group),
246       roster_group);
247
248   return EMPATHY_ROSTER_GROUP (roster_group);
249 }
250
251 static void
252 update_group_widgets (EmpathyRosterView *self,
253     EmpathyRosterGroup *group,
254     EmpathyRosterContact *contact,
255     gboolean add)
256 {
257   guint old_count, count;
258
259   old_count = empathy_roster_group_get_widgets_count (group);
260
261   if (add)
262     count = empathy_roster_group_add_widget (group, GTK_WIDGET (contact));
263   else
264     count = empathy_roster_group_remove_widget (group, GTK_WIDGET (contact));
265
266   if (count != old_count)
267     egg_list_box_child_changed (EGG_LIST_BOX (self), GTK_WIDGET (group));
268 }
269
270 static void
271 add_to_group (EmpathyRosterView *self,
272     FolksIndividual *individual,
273     const gchar *group)
274 {
275   GtkWidget *contact;
276   GHashTable *contacts;
277   EmpathyRosterGroup *roster_group = NULL;
278
279   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
280   if (contacts == NULL)
281     return;
282
283   if (tp_strdiff (group, NO_GROUP))
284     roster_group = ensure_roster_group (self, group);
285
286   contact = add_roster_contact (self, individual, group);
287   g_hash_table_insert (contacts, g_strdup (group), contact);
288
289   if (roster_group != NULL)
290     {
291       update_group_widgets (self, roster_group,
292           EMPATHY_ROSTER_CONTACT (contact), TRUE);
293     }
294 }
295
296 static void
297 individual_added (EmpathyRosterView *self,
298     FolksIndividual *individual)
299 {
300   GHashTable *contacts;
301
302   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
303   if (contacts != NULL)
304     return;
305
306   contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
307
308   g_hash_table_insert (self->priv->roster_contacts, individual, contacts);
309
310   if (!self->priv->show_groups)
311     {
312       add_to_group (self, individual, NO_GROUP);
313     }
314   else
315     {
316       GList *groups, *l;
317       GList *tops;
318
319       tops = empathy_individual_manager_get_top_individuals (
320           self->priv->manager);
321
322       if (folks_favourite_details_get_is_favourite (
323             FOLKS_FAVOURITE_DETAILS (individual)) ||
324           g_list_index (tops, individual) != -1)
325         {
326           add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
327         }
328
329       groups = empathy_roster_model_get_groups_for_individual (self->priv->model,
330           individual);
331
332       if (g_list_length (groups) > 0)
333         {
334           for (l = groups; l != NULL; l = g_list_next (l))
335             {
336               add_to_group (self, individual, l->data);
337             }
338         }
339       else
340         {
341           /* No group, adds to Ungrouped */
342           add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED);
343         }
344
345       g_list_free (groups);
346     }
347 }
348
349 static void
350 set_event_icon_on_individual (EmpathyRosterView *self,
351     FolksIndividual *individual,
352     const gchar *icon)
353 {
354   GHashTable *contacts;
355   GHashTableIter iter;
356   gpointer v;
357
358   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
359   if (contacts == NULL)
360     return;
361
362   g_hash_table_iter_init (&iter, contacts);
363   while (g_hash_table_iter_next (&iter, NULL, &v))
364     {
365       EmpathyRosterContact *contact =v;
366
367       empathy_roster_contact_set_event_icon (contact, icon);
368     }
369 }
370
371 static void
372 flash_event (Event *event,
373     EmpathyRosterView *self)
374 {
375   set_event_icon_on_individual (self, event->individual, event->icon);
376 }
377
378 static void
379 unflash_event (Event *event,
380     EmpathyRosterView *self)
381 {
382   set_event_icon_on_individual (self, event->individual, NULL);
383 }
384
385 static gboolean
386 flash_cb (gpointer data)
387 {
388   EmpathyRosterView *self = data;
389
390   if (self->priv->display_flash_event)
391     {
392       g_queue_foreach (self->priv->events, (GFunc) flash_event, self);
393       self->priv->display_flash_event = FALSE;
394     }
395   else
396     {
397       g_queue_foreach (self->priv->events, (GFunc) unflash_event, self);
398       self->priv->display_flash_event = TRUE;
399     }
400
401   return TRUE;
402 }
403
404 static void
405 start_flashing (EmpathyRosterView *self)
406 {
407   if (self->priv->flash_id != 0)
408     return;
409
410   self->priv->display_flash_event = TRUE;
411
412   self->priv->flash_id = g_timeout_add (FLASH_TIMEOUT,
413       flash_cb, self);
414 }
415
416 static void
417 stop_flashing (EmpathyRosterView *self)
418 {
419   if (self->priv->flash_id == 0)
420     return;
421
422   g_source_remove (self->priv->flash_id);
423   self->priv->flash_id = 0;
424 }
425
426 static void
427 remove_event (EmpathyRosterView *self,
428     Event *event)
429 {
430   unflash_event (event, self);
431   g_queue_remove (self->priv->events, event);
432
433   if (g_queue_get_length (self->priv->events) == 0)
434     {
435       stop_flashing (self);
436     }
437 }
438
439 static void
440 remove_all_individual_event (EmpathyRosterView *self,
441     FolksIndividual *individual)
442 {
443   GList *l;
444
445   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
446       l = g_list_next (l))
447     {
448       Event *event = l->data;
449
450       if (event->individual == individual)
451         {
452           remove_event (self, event);
453           return;
454         }
455     }
456 }
457
458 static void
459 individual_removed (EmpathyRosterView *self,
460     FolksIndividual *individual)
461 {
462   GHashTable *contacts;
463   GHashTableIter iter;
464   gpointer key, value;
465
466   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
467   if (contacts == NULL)
468     return;
469
470   remove_all_individual_event (self, individual);
471
472   g_hash_table_iter_init (&iter, contacts);
473   while (g_hash_table_iter_next (&iter, &key, &value))
474     {
475       const gchar *group_name = key;
476       GtkWidget *contact = value;
477       EmpathyRosterGroup *group;
478
479       group = lookup_roster_group (self, group_name);
480       if (group != NULL)
481         {
482           update_group_widgets (self, group,
483               EMPATHY_ROSTER_CONTACT (contact), FALSE);
484         }
485
486       gtk_container_remove (GTK_CONTAINER (self), contact);
487     }
488
489   g_hash_table_remove (self->priv->roster_contacts, individual);
490 }
491
492 static void
493 individual_added_cb (EmpathyRosterModel *model,
494     FolksIndividual *individual,
495     EmpathyRosterView *self)
496 {
497   individual_added (self, individual);
498 }
499
500 static void
501 individual_removed_cb (EmpathyRosterModel *model,
502     FolksIndividual *individual,
503     EmpathyRosterView *self)
504 {
505   individual_removed (self, individual);
506 }
507
508 static gint
509 compare_roster_contacts_by_alias (EmpathyRosterContact *a,
510     EmpathyRosterContact *b)
511 {
512   FolksIndividual *ind_a, *ind_b;
513   const gchar *alias_a, *alias_b;
514
515   ind_a = empathy_roster_contact_get_individual (a);
516   ind_b = empathy_roster_contact_get_individual (b);
517
518   alias_a = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_a));
519   alias_b = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_b));
520
521   return g_ascii_strcasecmp (alias_a, alias_b);
522 }
523
524 static gboolean
525 contact_is_favourite (EmpathyRosterContact *contact)
526 {
527   FolksIndividual *individual;
528
529   individual = empathy_roster_contact_get_individual (contact);
530
531   return folks_favourite_details_get_is_favourite (
532       FOLKS_FAVOURITE_DETAILS (individual));
533 }
534
535 static gboolean
536 contact_in_top (EmpathyRosterView *self,
537     EmpathyRosterContact *contact)
538 {
539   FolksIndividual *individual;
540   GList *tops;
541
542   if (contact_is_favourite (contact))
543     return TRUE;
544
545   individual = empathy_roster_contact_get_individual (contact);
546
547   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
548
549   if (g_list_index (tops, individual) != -1)
550     return TRUE;
551
552   return FALSE;
553 }
554
555 static gint
556 compare_roster_contacts_no_group (EmpathyRosterView *self,
557     EmpathyRosterContact *a,
558     EmpathyRosterContact *b)
559 {
560   gboolean top_a, top_b;
561
562   top_a = contact_in_top (self, a);
563   top_b = contact_in_top (self, b);
564
565   if (top_a == top_b)
566     /* Both contacts are in the top of the roster (or not). Sort them
567      * alphabetically */
568     return compare_roster_contacts_by_alias (a, b);
569   else if (top_a)
570     return -1;
571   else
572     return 1;
573 }
574
575 static gint
576 compare_group_names (const gchar *group_a,
577     const gchar *group_b)
578 {
579   if (!tp_strdiff (group_a, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
580     return -1;
581
582   if (!tp_strdiff (group_b, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
583     return 1;
584
585   if (!tp_strdiff (group_a, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED))
586     return 1;
587   else if (!tp_strdiff (group_b, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED))
588     return -1;
589
590   return g_ascii_strcasecmp (group_a, group_b);
591 }
592
593 static gint
594 compare_roster_contacts_with_groups (EmpathyRosterView *self,
595     EmpathyRosterContact *a,
596     EmpathyRosterContact *b)
597 {
598   const gchar *group_a, *group_b;
599
600   group_a = empathy_roster_contact_get_group (a);
601   group_b = empathy_roster_contact_get_group (b);
602
603   if (!tp_strdiff (group_a, group_b))
604     /* Same group, compare the contacts */
605     return compare_roster_contacts_by_alias (a, b);
606
607   /* Sort by group */
608   return compare_group_names (group_a, group_b);
609 }
610
611 static gint
612 compare_roster_contacts (EmpathyRosterView *self,
613     EmpathyRosterContact *a,
614     EmpathyRosterContact *b)
615 {
616   if (!self->priv->show_groups)
617     return compare_roster_contacts_no_group (self, a, b);
618   else
619     return compare_roster_contacts_with_groups (self, a, b);
620 }
621
622 static gint
623 compare_roster_groups (EmpathyRosterGroup *a,
624     EmpathyRosterGroup *b)
625 {
626   const gchar *name_a, *name_b;
627
628   name_a = empathy_roster_group_get_name (a);
629   name_b = empathy_roster_group_get_name (b);
630
631   return compare_group_names (name_a, name_b);
632 }
633
634 static gint
635 compare_contact_group (EmpathyRosterContact *contact,
636     EmpathyRosterGroup *group)
637 {
638   const char *contact_group, *group_name;
639
640   contact_group = empathy_roster_contact_get_group (contact);
641   group_name = empathy_roster_group_get_name (group);
642
643   if (!tp_strdiff (contact_group, group_name))
644     /* @contact is in @group, @group has to be displayed first */
645     return 1;
646
647   /* @contact is in a different group, sort by group name */
648   return compare_group_names (contact_group, group_name);
649 }
650
651 static gint
652 roster_view_sort (gconstpointer a,
653     gconstpointer b,
654     gpointer user_data)
655 {
656   EmpathyRosterView *self = user_data;
657
658   if (EMPATHY_IS_ROSTER_CONTACT (a) && EMPATHY_IS_ROSTER_CONTACT (b))
659     return compare_roster_contacts (self, EMPATHY_ROSTER_CONTACT (a),
660         EMPATHY_ROSTER_CONTACT (b));
661   else if (EMPATHY_IS_ROSTER_GROUP (a) && EMPATHY_IS_ROSTER_GROUP (b))
662     return compare_roster_groups (EMPATHY_ROSTER_GROUP (a),
663         EMPATHY_ROSTER_GROUP (b));
664   else if (EMPATHY_IS_ROSTER_CONTACT (a) && EMPATHY_IS_ROSTER_GROUP (b))
665     return compare_contact_group (EMPATHY_ROSTER_CONTACT (a),
666         EMPATHY_ROSTER_GROUP (b));
667   else if (EMPATHY_IS_ROSTER_GROUP (a) && EMPATHY_IS_ROSTER_CONTACT (b))
668     return -1 * compare_contact_group (EMPATHY_ROSTER_CONTACT (b),
669         EMPATHY_ROSTER_GROUP (a));
670
671   g_return_val_if_reached (0);
672 }
673
674 static void
675 update_separator (GtkWidget **separator,
676     GtkWidget *child,
677     GtkWidget *before,
678     gpointer user_data)
679 {
680   if (before == NULL)
681     {
682       /* No separator before the first row */
683       g_clear_object (separator);
684       return;
685     }
686
687   if (*separator != NULL)
688     return;
689
690   *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
691   g_object_ref_sink (*separator);
692 }
693
694 static gboolean
695 is_searching (EmpathyRosterView *self)
696 {
697   if (self->priv->search == NULL)
698     return FALSE;
699
700   return gtk_widget_get_visible (GTK_WIDGET (self->priv->search));
701 }
702
703 static void
704 update_empty (EmpathyRosterView *self,
705     gboolean empty)
706 {
707   if (self->priv->empty == empty)
708     return;
709
710   self->priv->empty = empty;
711   g_object_notify (G_OBJECT (self), "empty");
712 }
713
714 static void
715 add_to_displayed (EmpathyRosterView *self,
716     EmpathyRosterContact *contact)
717 {
718   FolksIndividual *individual;
719   GHashTable *contacts;
720   GHashTableIter iter;
721   gpointer k;
722
723   if (g_hash_table_lookup (self->priv->displayed_contacts, contact) != NULL)
724     return;
725
726   g_hash_table_add (self->priv->displayed_contacts, contact);
727   update_empty (self, FALSE);
728
729   /* Groups of this contact may now be displayed if we just displays the first
730    * child in this group. */
731
732   if (!self->priv->show_groups)
733     return;
734
735   individual = empathy_roster_contact_get_individual (contact);
736   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
737   if (contacts == NULL)
738     return;
739
740   g_hash_table_iter_init (&iter, contacts);
741   while (g_hash_table_iter_next (&iter, &k, NULL))
742     {
743       const gchar *group_name = k;
744       GtkWidget *group;
745
746       group = g_hash_table_lookup (self->priv->roster_groups, group_name);
747       if (group == NULL)
748         continue;
749
750       egg_list_box_child_changed (EGG_LIST_BOX (self), group);
751     }
752 }
753
754 static void
755 remove_from_displayed (EmpathyRosterView *self,
756     EmpathyRosterContact *contact)
757 {
758   g_hash_table_remove (self->priv->displayed_contacts, contact);
759
760   if (g_hash_table_size (self->priv->displayed_contacts) == 0)
761     update_empty (self, TRUE);
762 }
763
764 /**
765  * check if @contact should be displayed according to @self's current status
766  * and without consideration for the state of @contact's groups.
767  */
768 static gboolean
769 contact_should_be_displayed (EmpathyRosterView *self,
770     EmpathyRosterContact *contact)
771 {
772   if (is_searching (self))
773     {
774       FolksIndividual *individual;
775
776       individual = empathy_roster_contact_get_individual (contact);
777
778       return empathy_individual_match_string (individual,
779           empathy_live_search_get_text (self->priv->search),
780           empathy_live_search_get_words (self->priv->search));
781     }
782
783   if (self->priv->show_offline)
784       return TRUE;
785
786   if (contact_is_favourite (contact))
787     {
788       const gchar *group_name;
789
790       if (!self->priv->show_groups)
791         /* Always display favourite contacts in non-group mode. */
792         return TRUE;
793
794       group_name = empathy_roster_contact_get_group (contact);
795
796       if (!tp_strdiff (group_name, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
797         /* Always display favourite contact in group mode only in the
798          * 'top group'*/
799         return TRUE;
800     }
801
802   return empathy_roster_contact_is_online (contact);
803 }
804
805
806 static gboolean
807 filter_contact (EmpathyRosterView *self,
808     EmpathyRosterContact *contact)
809 {
810   gboolean displayed;
811
812   displayed = contact_should_be_displayed (self, contact);
813
814   if (self->priv->show_groups)
815     {
816       const gchar *group_name;
817       EmpathyRosterGroup *group;
818
819       group_name = empathy_roster_contact_get_group (contact);
820       group = lookup_roster_group (self, group_name);
821
822       if (group != NULL)
823         {
824           /* When searching, always display even if the group is closed */
825           if (!is_searching (self) &&
826               !gtk_expander_get_expanded (GTK_EXPANDER (group)))
827             displayed = FALSE;
828         }
829     }
830
831   if (displayed)
832     {
833       add_to_displayed (self, contact);
834     }
835   else
836     {
837       remove_from_displayed (self, contact);
838     }
839
840   return displayed;
841 }
842
843 static gboolean
844 filter_group (EmpathyRosterView *self,
845     EmpathyRosterGroup *group)
846 {
847   GList *widgets, *l;
848
849   /* Display the group if it contains at least one displayed contact */
850   widgets = empathy_roster_group_get_widgets (group);
851   for (l = widgets; l != NULL; l = g_list_next (l))
852     {
853       EmpathyRosterContact *contact = l->data;
854
855       if (contact_should_be_displayed (self, contact))
856         return TRUE;
857     }
858
859   return FALSE;
860 }
861
862 static gboolean
863 filter_list (GtkWidget *child,
864     gpointer user_data)
865 {
866   EmpathyRosterView *self = user_data;
867
868   if (EMPATHY_IS_ROSTER_CONTACT (child))
869     return filter_contact (self, EMPATHY_ROSTER_CONTACT (child));
870
871   else if (EMPATHY_IS_ROSTER_GROUP (child))
872     return filter_group (self, EMPATHY_ROSTER_GROUP (child));
873
874   g_return_val_if_reached (FALSE);
875 }
876
877 /* @list: GList of EmpathyRosterContact
878  *
879  * Returns: %TRUE if @list contains an EmpathyRosterContact associated with
880  * @individual */
881 static gboolean
882 individual_in_list (FolksIndividual *individual,
883     GList *list)
884 {
885   GList *l;
886
887   for (l = list; l != NULL; l = g_list_next (l))
888     {
889       EmpathyRosterContact *contact = l->data;
890
891       if (empathy_roster_contact_get_individual (contact) == individual)
892         return TRUE;
893     }
894
895   return FALSE;
896 }
897
898 static void
899 populate_view (EmpathyRosterView *self)
900 {
901   GList *individuals, *l;
902
903   individuals = empathy_roster_model_get_individuals (self->priv->model);
904   for (l = individuals; l != NULL; l = g_list_next (l))
905     {
906       FolksIndividual *individual = l->data;
907
908       individual_added (self, individual);
909     }
910
911   g_list_free (individuals);
912 }
913
914 static void
915 remove_from_group (EmpathyRosterView *self,
916     FolksIndividual *individual,
917     const gchar *group)
918 {
919   GHashTable *contacts;
920   GtkWidget *contact;
921   EmpathyRosterGroup *roster_group;
922
923   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
924   if (contacts == NULL)
925     return;
926
927   contact = g_hash_table_lookup (contacts, group);
928   if (contact == NULL)
929     return;
930
931   g_hash_table_remove (contacts, group);
932
933   if (g_hash_table_size (contacts) == 0)
934     {
935       add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED);
936     }
937
938   roster_group = lookup_roster_group (self, group);
939
940   if (roster_group != NULL)
941     {
942       update_group_widgets (self, roster_group,
943           EMPATHY_ROSTER_CONTACT (contact), FALSE);
944     }
945
946   gtk_container_remove (GTK_CONTAINER (self), contact);
947 }
948
949 static void
950 update_top_contacts (EmpathyRosterView *self)
951 {
952   GList *tops, *l;
953   GList *to_add = NULL, *to_remove = NULL;
954   EmpathyRosterGroup *group;
955
956   if (!self->priv->show_groups)
957     {
958       egg_list_box_resort (EGG_LIST_BOX (self));
959       return;
960     }
961
962   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
963
964   group = g_hash_table_lookup (self->priv->roster_groups,
965       EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
966   if (group == NULL)
967     {
968       to_add = g_list_copy (tops);
969     }
970   else
971     {
972       GList *contacts;
973
974       contacts = empathy_roster_group_get_widgets (group);
975
976       /* Check which EmpathyRosterContact have to be removed */
977       for (l = contacts; l != NULL; l = g_list_next (l))
978         {
979           EmpathyRosterContact *contact = l->data;
980           FolksIndividual *individual;
981
982           if (contact_is_favourite (contact))
983             continue;
984
985           individual = empathy_roster_contact_get_individual (contact);
986
987           if (g_list_find (tops, individual) == NULL)
988             to_remove = g_list_prepend (to_remove, individual);
989         }
990
991       /* Check which EmpathyRosterContact have to be added */
992       for (l = tops; l != NULL; l = g_list_next (l))
993         {
994           FolksIndividual *individual = l->data;
995
996           if (!individual_in_list (individual, contacts))
997             to_add = g_list_prepend (to_add, individual);
998         }
999     }
1000
1001   for (l = to_add; l != NULL; l = g_list_next (l))
1002     add_to_group (self, l->data, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1003
1004   for (l = to_remove; l != NULL; l = g_list_next (l))
1005     remove_from_group (self, l->data, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1006
1007   g_list_free (to_add);
1008   g_list_free (to_remove);
1009 }
1010
1011 static void
1012 groups_changed_cb (EmpathyRosterModel *model,
1013     FolksIndividual *individual,
1014     const gchar *group,
1015     gboolean is_member,
1016     EmpathyRosterView *self)
1017 {
1018   if (!self->priv->show_groups)
1019     return;
1020
1021   if (is_member)
1022     {
1023       add_to_group (self, individual, group);
1024     }
1025   else
1026     {
1027       remove_from_group (self, individual, group);
1028     }
1029 }
1030
1031 static void
1032 top_individuals_changed_cb (EmpathyIndividualManager *manager,
1033     GParamSpec *spec,
1034     EmpathyRosterView *self)
1035 {
1036   update_top_contacts (self);
1037 }
1038
1039 static void
1040 favourites_changed_cb (EmpathyIndividualManager *manager,
1041     FolksIndividual *individual,
1042     gboolean favourite,
1043     EmpathyRosterView *self)
1044 {
1045   GHashTable *contacts;
1046
1047   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1048   if (contacts == NULL)
1049     return;
1050
1051   if (self->priv->show_groups)
1052     {
1053       if (favourite)
1054         add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1055       else
1056         remove_from_group (self, individual,
1057             EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1058     }
1059   else
1060     {
1061       GtkWidget *contact;
1062
1063       contact = g_hash_table_lookup (contacts, NO_GROUP);
1064       if (contact == NULL)
1065         return;
1066
1067       egg_list_box_child_changed (EGG_LIST_BOX (self), contact);
1068     }
1069 }
1070
1071 static void
1072 empathy_roster_view_constructed (GObject *object)
1073 {
1074   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1075   void (*chain_up) (GObject *) =
1076       ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
1077
1078   if (chain_up != NULL)
1079     chain_up (object);
1080
1081   g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
1082   g_assert (EMPATHY_IS_ROSTER_MODEL (self->priv->model));
1083
1084   populate_view (self);
1085
1086   tp_g_signal_connect_object (self->priv->model, "individual-added",
1087       G_CALLBACK (individual_added_cb), self, 0);
1088   tp_g_signal_connect_object (self->priv->model, "individual-removed",
1089       G_CALLBACK (individual_removed_cb), self, 0);
1090   tp_g_signal_connect_object (self->priv->model, "groups-changed",
1091       G_CALLBACK (groups_changed_cb), self, 0);
1092   tp_g_signal_connect_object (self->priv->manager, "notify::top-individuals",
1093       G_CALLBACK (top_individuals_changed_cb), self, 0);
1094   tp_g_signal_connect_object (self->priv->manager, "notify::favourites-changed",
1095       G_CALLBACK (favourites_changed_cb), self, 0);
1096
1097   egg_list_box_set_sort_func (EGG_LIST_BOX (self),
1098       roster_view_sort, self, NULL);
1099
1100   egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
1101       self, NULL);
1102
1103   egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
1104
1105   egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE);
1106 }
1107
1108 static void
1109 empathy_roster_view_dispose (GObject *object)
1110 {
1111   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1112   void (*chain_up) (GObject *) =
1113       ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
1114
1115   stop_flashing (self);
1116
1117   empathy_roster_view_set_live_search (self, NULL);
1118   g_clear_object (&self->priv->manager);
1119   g_clear_object (&self->priv->model);
1120
1121   if (chain_up != NULL)
1122     chain_up (object);
1123 }
1124
1125 static void
1126 empathy_roster_view_finalize (GObject *object)
1127 {
1128   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1129   void (*chain_up) (GObject *) =
1130       ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
1131
1132   g_hash_table_unref (self->priv->roster_contacts);
1133   g_hash_table_unref (self->priv->roster_groups);
1134   g_hash_table_unref (self->priv->displayed_contacts);
1135   g_queue_free_full (self->priv->events, event_free);
1136
1137   if (chain_up != NULL)
1138     chain_up (object);
1139 }
1140
1141 static void
1142 empathy_roster_view_child_activated (EggListBox *box,
1143     GtkWidget *child)
1144 {
1145   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (box);
1146   EmpathyRosterContact *contact;
1147   FolksIndividual *individual;
1148   GList *l;
1149
1150   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1151     return;
1152
1153   contact = EMPATHY_ROSTER_CONTACT (child);
1154   individual = empathy_roster_contact_get_individual (contact);
1155
1156   /* Activate the oldest event associated with this contact, if any */
1157   for (l = g_queue_peek_tail_link (self->priv->events); l != NULL;
1158       l = g_list_previous (l))
1159     {
1160       Event *event = l->data;
1161
1162       if (event->individual == individual)
1163         {
1164           g_signal_emit (box, signals[SIG_EVENT_ACTIVATED], 0, individual,
1165               event->user_data);
1166           return;
1167         }
1168     }
1169
1170   g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual);
1171 }
1172
1173 static void
1174 fire_popup_individual_menu (EmpathyRosterView *self,
1175     GtkWidget *child,
1176     guint button,
1177     guint time)
1178 {
1179   EmpathyRosterContact *contact;
1180   FolksIndividual *individual;
1181
1182   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1183     return;
1184
1185   contact = EMPATHY_ROSTER_CONTACT (child);
1186   individual = empathy_roster_contact_get_individual (contact);
1187
1188   g_signal_emit (self, signals[SIG_POPUP_INDIVIDUAL_MENU], 0,
1189       individual, button, time);
1190 }
1191
1192 static gboolean
1193 empathy_roster_view_button_press_event (GtkWidget *widget,
1194     GdkEventButton *event)
1195 {
1196   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1197   gboolean (*chain_up) (GtkWidget *, GdkEventButton *) =
1198       ((GtkWidgetClass *) empathy_roster_view_parent_class)->button_press_event;
1199
1200   if (event->button == 3)
1201     {
1202       GtkWidget *child;
1203
1204       child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), event->y);
1205
1206       if (child != NULL)
1207         {
1208           egg_list_box_select_child (EGG_LIST_BOX (self), child);
1209
1210           fire_popup_individual_menu (self, child, event->button, event->time);
1211         }
1212     }
1213
1214   return chain_up (widget, event);
1215 }
1216
1217 static gboolean
1218 empathy_roster_view_key_press_event (GtkWidget *widget,
1219     GdkEventKey *event)
1220 {
1221   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1222   gboolean (*chain_up) (GtkWidget *, GdkEventKey *) =
1223       ((GtkWidgetClass *) empathy_roster_view_parent_class)->key_press_event;
1224
1225   if (event->keyval == GDK_KEY_Menu)
1226     {
1227       GtkWidget *child;
1228
1229       child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1230
1231       if (child != NULL)
1232         fire_popup_individual_menu (self, child, 0, event->time);
1233     }
1234
1235   return chain_up (widget, event);
1236 }
1237
1238 /**
1239  * @out_child: (out) (allow-none)
1240  */
1241 FolksIndividual *
1242 empathy_roster_view_get_individual_at_y (EmpathyRosterView *self,
1243     gint y,
1244     GtkWidget **out_child)
1245 {
1246   GtkWidget *child;
1247
1248   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1249
1250   if (out_child != NULL)
1251     *out_child = child;
1252
1253   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1254     return NULL;
1255
1256   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1257 }
1258
1259 /**
1260  * @out_child: (out) (allow-none)
1261  */
1262 const gchar *
1263 empathy_roster_view_get_group_at_y (EmpathyRosterView *self,
1264     gint y)
1265 {
1266   GtkWidget *child;
1267
1268   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1269
1270   if (EMPATHY_IS_ROSTER_CONTACT (child))
1271     return empathy_roster_contact_get_group (EMPATHY_ROSTER_CONTACT (child));
1272   else if (EMPATHY_IS_ROSTER_GROUP (child))
1273     return empathy_roster_group_get_name (EMPATHY_ROSTER_GROUP (child));
1274
1275   return NULL;
1276 }
1277
1278 static gboolean
1279 empathy_roster_view_query_tooltip (GtkWidget *widget,
1280     gint x,
1281     gint y,
1282     gboolean keyboard_mode,
1283     GtkTooltip *tooltip)
1284 {
1285   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1286   FolksIndividual *individual;
1287   gboolean result;
1288   GtkWidget *child;
1289
1290   individual = empathy_roster_view_get_individual_at_y (self, y, &child);
1291   if (individual == NULL)
1292     return FALSE;
1293
1294   g_signal_emit (self, signals[SIG_INDIVIDUAL_TOOLTIP], 0,
1295       individual, keyboard_mode, tooltip, &result);
1296
1297   if (result)
1298     {
1299       GtkAllocation allocation;
1300
1301       gtk_widget_get_allocation (child, &allocation);
1302       gtk_tooltip_set_tip_area (tooltip, (GdkRectangle *) &allocation);
1303     }
1304
1305   return result;
1306 }
1307
1308 static void
1309 empathy_roster_view_remove (GtkContainer *container,
1310     GtkWidget *widget)
1311 {
1312   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (container);
1313   void (*chain_up) (GtkContainer *, GtkWidget *) =
1314       ((GtkContainerClass *) empathy_roster_view_parent_class)->remove;
1315
1316   chain_up (container, widget);
1317
1318   if (EMPATHY_IS_ROSTER_CONTACT (widget))
1319     remove_from_displayed (self, (EmpathyRosterContact *) widget);
1320 }
1321
1322 static void
1323 empathy_roster_view_class_init (
1324     EmpathyRosterViewClass *klass)
1325 {
1326   GObjectClass *oclass = G_OBJECT_CLASS (klass);
1327   EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass);
1328   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1329   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
1330   GParamSpec *spec;
1331
1332   oclass->get_property = empathy_roster_view_get_property;
1333   oclass->set_property = empathy_roster_view_set_property;
1334   oclass->constructed = empathy_roster_view_constructed;
1335   oclass->dispose = empathy_roster_view_dispose;
1336   oclass->finalize = empathy_roster_view_finalize;
1337
1338   widget_class->button_press_event = empathy_roster_view_button_press_event;
1339   widget_class->key_press_event = empathy_roster_view_key_press_event;
1340   widget_class->query_tooltip = empathy_roster_view_query_tooltip;
1341
1342   container_class->remove = empathy_roster_view_remove;
1343
1344   box_class->child_activated = empathy_roster_view_child_activated;
1345
1346   spec = g_param_spec_object ("manager", "Manager",
1347       "EmpathyIndividualManager",
1348       EMPATHY_TYPE_INDIVIDUAL_MANAGER,
1349       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1350   g_object_class_install_property (oclass, PROP_MANAGER, spec);
1351
1352   spec = g_param_spec_object ("model", "Model",
1353       "EmpathyRosterModel",
1354       EMPATHY_TYPE_ROSTER_MODEL,
1355       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1356   g_object_class_install_property (oclass, PROP_MODEL, spec);
1357
1358   spec = g_param_spec_boolean ("show-offline", "Show Offline",
1359       "Show offline contacts",
1360       FALSE,
1361       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1362   g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
1363
1364   spec = g_param_spec_boolean ("show-groups", "Show Groups",
1365       "Show groups",
1366       FALSE,
1367       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1368   g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
1369
1370   spec = g_param_spec_boolean ("empty", "Empty",
1371       "Is the view currently empty?",
1372       FALSE,
1373       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
1374   g_object_class_install_property (oclass, PROP_EMPTY, spec);
1375
1376   signals[SIG_INDIVIDUAL_ACTIVATED] = g_signal_new ("individual-activated",
1377       G_OBJECT_CLASS_TYPE (klass),
1378       G_SIGNAL_RUN_LAST,
1379       0, NULL, NULL, NULL,
1380       G_TYPE_NONE,
1381       1, FOLKS_TYPE_INDIVIDUAL);
1382
1383   signals[SIG_POPUP_INDIVIDUAL_MENU] = g_signal_new ("popup-individual-menu",
1384       G_OBJECT_CLASS_TYPE (klass),
1385       G_SIGNAL_RUN_LAST,
1386       0, NULL, NULL, NULL,
1387       G_TYPE_NONE,
1388       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT, G_TYPE_UINT);
1389
1390   signals[SIG_EVENT_ACTIVATED] = g_signal_new ("event-activated",
1391       G_OBJECT_CLASS_TYPE (klass),
1392       G_SIGNAL_RUN_LAST,
1393       0, NULL, NULL, NULL,
1394       G_TYPE_NONE,
1395       2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_POINTER);
1396
1397   signals[SIG_INDIVIDUAL_TOOLTIP] = g_signal_new ("individual-tooltip",
1398       G_OBJECT_CLASS_TYPE (klass),
1399       G_SIGNAL_RUN_LAST,
1400       0, g_signal_accumulator_true_handled, NULL, NULL,
1401       G_TYPE_BOOLEAN,
1402       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_BOOLEAN, GTK_TYPE_TOOLTIP);
1403
1404   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
1405 }
1406
1407 static void
1408 empathy_roster_view_init (EmpathyRosterView *self)
1409 {
1410   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1411       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
1412
1413   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
1414       NULL, (GDestroyNotify) g_hash_table_unref);
1415   self->priv->roster_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
1416       g_free, NULL);
1417   self->priv->displayed_contacts = g_hash_table_new (NULL, NULL);
1418
1419   self->priv->events = g_queue_new ();
1420
1421   self->priv->empty = TRUE;
1422 }
1423
1424 GtkWidget *
1425 empathy_roster_view_new (EmpathyIndividualManager *manager,
1426     EmpathyRosterModel *model)
1427 {
1428   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
1429   g_return_val_if_fail (EMPATHY_IS_ROSTER_MODEL (model), NULL);
1430
1431   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
1432       "manager", manager,
1433       "model", model,
1434       NULL);
1435 }
1436
1437 EmpathyIndividualManager *
1438 empathy_roster_view_get_manager (EmpathyRosterView *self)
1439 {
1440   return self->priv->manager;
1441 }
1442
1443 void
1444 empathy_roster_view_show_offline (EmpathyRosterView *self,
1445     gboolean show)
1446 {
1447   if (self->priv->show_offline == show)
1448     return;
1449
1450   self->priv->show_offline = show;
1451   egg_list_box_refilter (EGG_LIST_BOX (self));
1452
1453   g_object_notify (G_OBJECT (self), "show-offline");
1454 }
1455
1456 static void
1457 clear_view (EmpathyRosterView *self)
1458 {
1459   gtk_container_foreach (GTK_CONTAINER (self),
1460       (GtkCallback) gtk_widget_destroy, NULL);
1461
1462   g_hash_table_remove_all (self->priv->roster_contacts);
1463   g_hash_table_remove_all (self->priv->roster_groups);
1464   g_hash_table_remove_all (self->priv->displayed_contacts);
1465 }
1466
1467 void
1468 empathy_roster_view_show_groups (EmpathyRosterView *self,
1469     gboolean show)
1470 {
1471   if (self->priv->show_groups == show)
1472     return;
1473
1474   self->priv->show_groups = show;
1475
1476   /* TODO: block sort/filter? */
1477   clear_view (self);
1478   populate_view (self);
1479
1480   g_object_notify (G_OBJECT (self), "show-groups");
1481 }
1482
1483 static void
1484 select_first_contact (EmpathyRosterView *self)
1485 {
1486   GList *children, *l;
1487
1488   children = gtk_container_get_children (GTK_CONTAINER (self));
1489   for (l = children; l != NULL; l = g_list_next (l))
1490     {
1491       GtkWidget *child = l->data;
1492
1493       if (!gtk_widget_get_child_visible (child))
1494         continue;
1495
1496       if (!EMPATHY_IS_ROSTER_CONTACT (child))
1497         continue;
1498
1499       egg_list_box_select_child (EGG_LIST_BOX (self), child);
1500       break;
1501     }
1502
1503   g_list_free (children);
1504 }
1505
1506 static void
1507 search_text_notify_cb (EmpathyLiveSearch *search,
1508     GParamSpec *pspec,
1509     EmpathyRosterView *self)
1510 {
1511   egg_list_box_refilter (EGG_LIST_BOX (self));
1512
1513   select_first_contact (self);
1514 }
1515
1516 static void
1517 search_activate_cb (GtkWidget *search,
1518   EmpathyRosterView *self)
1519 {
1520   EggListBox *box = EGG_LIST_BOX (self);
1521   GtkWidget *child;
1522
1523   child = egg_list_box_get_selected_child (box);
1524   if (child == NULL)
1525     return;
1526
1527   empathy_roster_view_child_activated (box, child);
1528 }
1529
1530 void
1531 empathy_roster_view_set_live_search (EmpathyRosterView *self,
1532     EmpathyLiveSearch *search)
1533 {
1534   if (self->priv->search != NULL)
1535     {
1536       g_signal_handlers_disconnect_by_func (self->priv->search,
1537           search_text_notify_cb, self);
1538       g_signal_handlers_disconnect_by_func (self->priv->search,
1539           search_activate_cb, self);
1540
1541       g_clear_object (&self->priv->search);
1542     }
1543
1544   if (search == NULL)
1545     return;
1546
1547   self->priv->search = g_object_ref (search);
1548
1549   g_signal_connect (self->priv->search, "notify::text",
1550       G_CALLBACK (search_text_notify_cb), self);
1551   g_signal_connect (self->priv->search, "activate",
1552       G_CALLBACK (search_activate_cb), self);
1553 }
1554
1555 gboolean
1556 empathy_roster_view_is_empty (EmpathyRosterView *self)
1557 {
1558   return self->priv->empty;
1559 }
1560
1561 gboolean
1562 empathy_roster_view_is_searching (EmpathyRosterView *self)
1563 {
1564   return (self->priv->search != NULL &&
1565       gtk_widget_get_visible (GTK_WIDGET (self->priv->search)));
1566 }
1567
1568 /* Don't use EmpathyEvent as I prefer to keep this object not too specific to
1569  * Empathy's internals. */
1570 guint
1571 empathy_roster_view_add_event (EmpathyRosterView *self,
1572     FolksIndividual *individual,
1573     const gchar *icon,
1574     gpointer user_data)
1575 {
1576   GHashTable *contacts;
1577
1578   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1579   if (contacts == NULL)
1580     return 0;
1581
1582   self->priv->last_event_id++;
1583
1584   g_queue_push_head (self->priv->events,
1585       event_new (self->priv->last_event_id, individual, icon, user_data));
1586
1587   start_flashing (self);
1588
1589   return self->priv->last_event_id;
1590 }
1591
1592 void
1593 empathy_roster_view_remove_event (EmpathyRosterView *self,
1594     guint event_id)
1595 {
1596   GList *l;
1597
1598   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
1599       l = g_list_next (l))
1600     {
1601       Event *event = l->data;
1602
1603       if (event->id == event_id)
1604         {
1605           remove_event (self, event);
1606           return;
1607         }
1608     }
1609 }
1610
1611 FolksIndividual *
1612 empathy_roster_view_get_selected_individual (EmpathyRosterView *self)
1613 {
1614   GtkWidget *child;
1615
1616   child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1617
1618   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1619     return NULL;
1620
1621   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1622 }