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