]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
Install property active-group in EmpathyIndividualMenu
[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_utf8_collate (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_utf8_collate (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   const gchar *active_group;
1150
1151   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1152     return;
1153
1154   contact = EMPATHY_ROSTER_CONTACT (child);
1155   individual = empathy_roster_contact_get_individual (contact);
1156
1157   active_group = empathy_roster_contact_get_group (contact);
1158   g_signal_emit (self, signals[SIG_POPUP_INDIVIDUAL_MENU], 0,
1159       active_group, 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       4, G_TYPE_STRING, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT,
1353           G_TYPE_UINT);
1354
1355   signals[SIG_EVENT_ACTIVATED] = g_signal_new ("event-activated",
1356       G_OBJECT_CLASS_TYPE (klass),
1357       G_SIGNAL_RUN_LAST,
1358       0, NULL, NULL, NULL,
1359       G_TYPE_NONE,
1360       2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_POINTER);
1361
1362   signals[SIG_INDIVIDUAL_TOOLTIP] = g_signal_new ("individual-tooltip",
1363       G_OBJECT_CLASS_TYPE (klass),
1364       G_SIGNAL_RUN_LAST,
1365       0, g_signal_accumulator_true_handled, NULL, NULL,
1366       G_TYPE_BOOLEAN,
1367       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_BOOLEAN, GTK_TYPE_TOOLTIP);
1368
1369   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
1370 }
1371
1372 static void
1373 empathy_roster_view_init (EmpathyRosterView *self)
1374 {
1375   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1376       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
1377
1378   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
1379       NULL, (GDestroyNotify) g_hash_table_unref);
1380   self->priv->roster_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
1381       g_free, NULL);
1382   self->priv->displayed_contacts = g_hash_table_new (NULL, NULL);
1383
1384   self->priv->events = g_queue_new ();
1385
1386   self->priv->empty = TRUE;
1387 }
1388
1389 GtkWidget *
1390 empathy_roster_view_new (EmpathyRosterModel *model)
1391 {
1392   g_return_val_if_fail (EMPATHY_IS_ROSTER_MODEL (model), NULL);
1393
1394   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
1395       "model", model,
1396       NULL);
1397 }
1398
1399 void
1400 empathy_roster_view_show_offline (EmpathyRosterView *self,
1401     gboolean show)
1402 {
1403   if (self->priv->show_offline == show)
1404     return;
1405
1406   self->priv->show_offline = show;
1407   egg_list_box_refilter (EGG_LIST_BOX (self));
1408
1409   g_object_notify (G_OBJECT (self), "show-offline");
1410 }
1411
1412 void
1413 empathy_roster_view_show_groups (EmpathyRosterView *self,
1414     gboolean show)
1415 {
1416   if (self->priv->show_groups == show)
1417     return;
1418
1419   self->priv->show_groups = show;
1420
1421   /* TODO: block sort/filter? */
1422   clear_view (self);
1423   populate_view (self);
1424
1425   g_object_notify (G_OBJECT (self), "show-groups");
1426 }
1427
1428 static void
1429 select_first_contact (EmpathyRosterView *self)
1430 {
1431   GList *children, *l;
1432
1433   children = gtk_container_get_children (GTK_CONTAINER (self));
1434   for (l = children; l != NULL; l = g_list_next (l))
1435     {
1436       GtkWidget *child = l->data;
1437
1438       if (!gtk_widget_get_child_visible (child))
1439         continue;
1440
1441       if (!EMPATHY_IS_ROSTER_CONTACT (child))
1442         continue;
1443
1444       egg_list_box_select_child (EGG_LIST_BOX (self), child);
1445       break;
1446     }
1447
1448   g_list_free (children);
1449 }
1450
1451 static gboolean
1452 search_timeout_cb (EmpathyRosterView *self)
1453 {
1454   egg_list_box_refilter (EGG_LIST_BOX (self));
1455
1456   select_first_contact (self);
1457
1458   self->priv->search_id = 0;
1459   return G_SOURCE_REMOVE;
1460 }
1461
1462 static void
1463 search_text_notify_cb (EmpathyLiveSearch *search,
1464     GParamSpec *pspec,
1465     EmpathyRosterView *self)
1466 {
1467   if (self->priv->search_id != 0)
1468     g_source_remove (self->priv->search_id);
1469
1470   self->priv->search_id = g_timeout_add (SEARCH_TIMEOUT,
1471       (GSourceFunc) search_timeout_cb, self);
1472 }
1473
1474 static void
1475 search_activate_cb (GtkWidget *search,
1476   EmpathyRosterView *self)
1477 {
1478   EggListBox *box = EGG_LIST_BOX (self);
1479   GtkWidget *child;
1480
1481   child = egg_list_box_get_selected_child (box);
1482   if (child == NULL)
1483     return;
1484
1485   empathy_roster_view_child_activated (box, child);
1486 }
1487
1488 void
1489 empathy_roster_view_set_live_search (EmpathyRosterView *self,
1490     EmpathyLiveSearch *search)
1491 {
1492   if (self->priv->search != NULL)
1493     {
1494       g_signal_handlers_disconnect_by_func (self->priv->search,
1495           search_text_notify_cb, self);
1496       g_signal_handlers_disconnect_by_func (self->priv->search,
1497           search_activate_cb, self);
1498
1499       g_clear_object (&self->priv->search);
1500     }
1501
1502   if (search == NULL)
1503     return;
1504
1505   self->priv->search = g_object_ref (search);
1506
1507   g_signal_connect (self->priv->search, "notify::text",
1508       G_CALLBACK (search_text_notify_cb), self);
1509   g_signal_connect (self->priv->search, "activate",
1510       G_CALLBACK (search_activate_cb), self);
1511 }
1512
1513 gboolean
1514 empathy_roster_view_is_empty (EmpathyRosterView *self)
1515 {
1516   return self->priv->empty;
1517 }
1518
1519 gboolean
1520 empathy_roster_view_is_searching (EmpathyRosterView *self)
1521 {
1522   return (self->priv->search != NULL &&
1523       gtk_widget_get_visible (GTK_WIDGET (self->priv->search)));
1524 }
1525
1526 /* Don't use EmpathyEvent as I prefer to keep this object not too specific to
1527  * Empathy's internals. */
1528 guint
1529 empathy_roster_view_add_event (EmpathyRosterView *self,
1530     FolksIndividual *individual,
1531     const gchar *icon,
1532     gpointer user_data)
1533 {
1534   GHashTable *contacts;
1535
1536   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1537   if (contacts == NULL)
1538     return 0;
1539
1540   self->priv->last_event_id++;
1541
1542   g_queue_push_head (self->priv->events,
1543       event_new (self->priv->last_event_id, individual, icon, user_data));
1544
1545   start_flashing (self);
1546
1547   return self->priv->last_event_id;
1548 }
1549
1550 void
1551 empathy_roster_view_remove_event (EmpathyRosterView *self,
1552     guint event_id)
1553 {
1554   GList *l;
1555
1556   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
1557       l = g_list_next (l))
1558     {
1559       Event *event = l->data;
1560
1561       if (event->id == event_id)
1562         {
1563           remove_event (self, event);
1564           return;
1565         }
1566     }
1567 }
1568
1569 FolksIndividual *
1570 empathy_roster_view_get_selected_individual (EmpathyRosterView *self)
1571 {
1572   GtkWidget *child;
1573
1574   child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1575
1576   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1577     return NULL;
1578
1579   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1580 }