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