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