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