]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
Added new function _get_top_individuals in the model
[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 (EmpathyIndividualManager *manager,
1032     GParamSpec *spec,
1033     EmpathyRosterView *self)
1034 {
1035   update_top_contacts (self);
1036 }
1037
1038 static void
1039 favourites_changed_cb (EmpathyIndividualManager *manager,
1040     FolksIndividual *individual,
1041     gboolean favourite,
1042     EmpathyRosterView *self)
1043 {
1044   GHashTable *contacts;
1045
1046   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1047   if (contacts == NULL)
1048     return;
1049
1050   if (self->priv->show_groups)
1051     {
1052       if (favourite)
1053         add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1054       else
1055         remove_from_group (self, individual,
1056             EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1057     }
1058   else
1059     {
1060       GtkWidget *contact;
1061
1062       contact = g_hash_table_lookup (contacts, NO_GROUP);
1063       if (contact == NULL)
1064         return;
1065
1066       egg_list_box_child_changed (EGG_LIST_BOX (self), contact);
1067     }
1068 }
1069
1070 static void
1071 empathy_roster_view_constructed (GObject *object)
1072 {
1073   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1074   void (*chain_up) (GObject *) =
1075       ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
1076
1077   if (chain_up != NULL)
1078     chain_up (object);
1079
1080   g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
1081   g_assert (EMPATHY_IS_ROSTER_MODEL (self->priv->model));
1082
1083   populate_view (self);
1084
1085   tp_g_signal_connect_object (self->priv->model, "individual-added",
1086       G_CALLBACK (individual_added_cb), self, 0);
1087   tp_g_signal_connect_object (self->priv->model, "individual-removed",
1088       G_CALLBACK (individual_removed_cb), self, 0);
1089   tp_g_signal_connect_object (self->priv->model, "groups-changed",
1090       G_CALLBACK (groups_changed_cb), self, 0);
1091   tp_g_signal_connect_object (self->priv->manager, "notify::top-individuals",
1092       G_CALLBACK (top_individuals_changed_cb), self, 0);
1093   tp_g_signal_connect_object (self->priv->manager, "notify::favourites-changed",
1094       G_CALLBACK (favourites_changed_cb), self, 0);
1095
1096   egg_list_box_set_sort_func (EGG_LIST_BOX (self),
1097       roster_view_sort, self, NULL);
1098
1099   egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
1100       self, NULL);
1101
1102   egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
1103
1104   egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE);
1105 }
1106
1107 static void
1108 empathy_roster_view_dispose (GObject *object)
1109 {
1110   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1111   void (*chain_up) (GObject *) =
1112       ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
1113
1114   stop_flashing (self);
1115
1116   empathy_roster_view_set_live_search (self, NULL);
1117   g_clear_object (&self->priv->manager);
1118   g_clear_object (&self->priv->model);
1119
1120   if (chain_up != NULL)
1121     chain_up (object);
1122 }
1123
1124 static void
1125 empathy_roster_view_finalize (GObject *object)
1126 {
1127   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1128   void (*chain_up) (GObject *) =
1129       ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
1130
1131   g_hash_table_unref (self->priv->roster_contacts);
1132   g_hash_table_unref (self->priv->roster_groups);
1133   g_hash_table_unref (self->priv->displayed_contacts);
1134   g_queue_free_full (self->priv->events, event_free);
1135
1136   if (chain_up != NULL)
1137     chain_up (object);
1138 }
1139
1140 static void
1141 empathy_roster_view_child_activated (EggListBox *box,
1142     GtkWidget *child)
1143 {
1144   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (box);
1145   EmpathyRosterContact *contact;
1146   FolksIndividual *individual;
1147   GList *l;
1148
1149   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1150     return;
1151
1152   contact = EMPATHY_ROSTER_CONTACT (child);
1153   individual = empathy_roster_contact_get_individual (contact);
1154
1155   /* Activate the oldest event associated with this contact, if any */
1156   for (l = g_queue_peek_tail_link (self->priv->events); l != NULL;
1157       l = g_list_previous (l))
1158     {
1159       Event *event = l->data;
1160
1161       if (event->individual == individual)
1162         {
1163           g_signal_emit (box, signals[SIG_EVENT_ACTIVATED], 0, individual,
1164               event->user_data);
1165           return;
1166         }
1167     }
1168
1169   g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual);
1170 }
1171
1172 static void
1173 fire_popup_individual_menu (EmpathyRosterView *self,
1174     GtkWidget *child,
1175     guint button,
1176     guint time)
1177 {
1178   EmpathyRosterContact *contact;
1179   FolksIndividual *individual;
1180
1181   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1182     return;
1183
1184   contact = EMPATHY_ROSTER_CONTACT (child);
1185   individual = empathy_roster_contact_get_individual (contact);
1186
1187   g_signal_emit (self, signals[SIG_POPUP_INDIVIDUAL_MENU], 0,
1188       individual, button, time);
1189 }
1190
1191 static gboolean
1192 empathy_roster_view_button_press_event (GtkWidget *widget,
1193     GdkEventButton *event)
1194 {
1195   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1196   gboolean (*chain_up) (GtkWidget *, GdkEventButton *) =
1197       ((GtkWidgetClass *) empathy_roster_view_parent_class)->button_press_event;
1198
1199   if (event->button == 3)
1200     {
1201       GtkWidget *child;
1202
1203       child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), event->y);
1204
1205       if (child != NULL)
1206         {
1207           egg_list_box_select_child (EGG_LIST_BOX (self), child);
1208
1209           fire_popup_individual_menu (self, child, event->button, event->time);
1210         }
1211     }
1212
1213   return chain_up (widget, event);
1214 }
1215
1216 static gboolean
1217 empathy_roster_view_key_press_event (GtkWidget *widget,
1218     GdkEventKey *event)
1219 {
1220   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1221   gboolean (*chain_up) (GtkWidget *, GdkEventKey *) =
1222       ((GtkWidgetClass *) empathy_roster_view_parent_class)->key_press_event;
1223
1224   if (event->keyval == GDK_KEY_Menu)
1225     {
1226       GtkWidget *child;
1227
1228       child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1229
1230       if (child != NULL)
1231         fire_popup_individual_menu (self, child, 0, event->time);
1232     }
1233
1234   return chain_up (widget, event);
1235 }
1236
1237 /**
1238  * @out_child: (out) (allow-none)
1239  */
1240 FolksIndividual *
1241 empathy_roster_view_get_individual_at_y (EmpathyRosterView *self,
1242     gint y,
1243     GtkWidget **out_child)
1244 {
1245   GtkWidget *child;
1246
1247   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1248
1249   if (out_child != NULL)
1250     *out_child = child;
1251
1252   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1253     return NULL;
1254
1255   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1256 }
1257
1258 /**
1259  * @out_child: (out) (allow-none)
1260  */
1261 const gchar *
1262 empathy_roster_view_get_group_at_y (EmpathyRosterView *self,
1263     gint y)
1264 {
1265   GtkWidget *child;
1266
1267   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1268
1269   if (EMPATHY_IS_ROSTER_CONTACT (child))
1270     return empathy_roster_contact_get_group (EMPATHY_ROSTER_CONTACT (child));
1271   else if (EMPATHY_IS_ROSTER_GROUP (child))
1272     return empathy_roster_group_get_name (EMPATHY_ROSTER_GROUP (child));
1273
1274   return NULL;
1275 }
1276
1277 static gboolean
1278 empathy_roster_view_query_tooltip (GtkWidget *widget,
1279     gint x,
1280     gint y,
1281     gboolean keyboard_mode,
1282     GtkTooltip *tooltip)
1283 {
1284   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1285   FolksIndividual *individual;
1286   gboolean result;
1287   GtkWidget *child;
1288
1289   individual = empathy_roster_view_get_individual_at_y (self, y, &child);
1290   if (individual == NULL)
1291     return FALSE;
1292
1293   g_signal_emit (self, signals[SIG_INDIVIDUAL_TOOLTIP], 0,
1294       individual, keyboard_mode, tooltip, &result);
1295
1296   if (result)
1297     {
1298       GtkAllocation allocation;
1299
1300       gtk_widget_get_allocation (child, &allocation);
1301       gtk_tooltip_set_tip_area (tooltip, (GdkRectangle *) &allocation);
1302     }
1303
1304   return result;
1305 }
1306
1307 static void
1308 empathy_roster_view_remove (GtkContainer *container,
1309     GtkWidget *widget)
1310 {
1311   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (container);
1312   void (*chain_up) (GtkContainer *, GtkWidget *) =
1313       ((GtkContainerClass *) empathy_roster_view_parent_class)->remove;
1314
1315   chain_up (container, widget);
1316
1317   if (EMPATHY_IS_ROSTER_CONTACT (widget))
1318     remove_from_displayed (self, (EmpathyRosterContact *) widget);
1319 }
1320
1321 static void
1322 empathy_roster_view_class_init (
1323     EmpathyRosterViewClass *klass)
1324 {
1325   GObjectClass *oclass = G_OBJECT_CLASS (klass);
1326   EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass);
1327   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1328   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
1329   GParamSpec *spec;
1330
1331   oclass->get_property = empathy_roster_view_get_property;
1332   oclass->set_property = empathy_roster_view_set_property;
1333   oclass->constructed = empathy_roster_view_constructed;
1334   oclass->dispose = empathy_roster_view_dispose;
1335   oclass->finalize = empathy_roster_view_finalize;
1336
1337   widget_class->button_press_event = empathy_roster_view_button_press_event;
1338   widget_class->key_press_event = empathy_roster_view_key_press_event;
1339   widget_class->query_tooltip = empathy_roster_view_query_tooltip;
1340
1341   container_class->remove = empathy_roster_view_remove;
1342
1343   box_class->child_activated = empathy_roster_view_child_activated;
1344
1345   spec = g_param_spec_object ("manager", "Manager",
1346       "EmpathyIndividualManager",
1347       EMPATHY_TYPE_INDIVIDUAL_MANAGER,
1348       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1349   g_object_class_install_property (oclass, PROP_MANAGER, spec);
1350
1351   spec = g_param_spec_object ("model", "Model",
1352       "EmpathyRosterModel",
1353       EMPATHY_TYPE_ROSTER_MODEL,
1354       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1355   g_object_class_install_property (oclass, PROP_MODEL, spec);
1356
1357   spec = g_param_spec_boolean ("show-offline", "Show Offline",
1358       "Show offline contacts",
1359       FALSE,
1360       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1361   g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
1362
1363   spec = g_param_spec_boolean ("show-groups", "Show Groups",
1364       "Show groups",
1365       FALSE,
1366       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1367   g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
1368
1369   spec = g_param_spec_boolean ("empty", "Empty",
1370       "Is the view currently empty?",
1371       FALSE,
1372       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
1373   g_object_class_install_property (oclass, PROP_EMPTY, spec);
1374
1375   signals[SIG_INDIVIDUAL_ACTIVATED] = g_signal_new ("individual-activated",
1376       G_OBJECT_CLASS_TYPE (klass),
1377       G_SIGNAL_RUN_LAST,
1378       0, NULL, NULL, NULL,
1379       G_TYPE_NONE,
1380       1, FOLKS_TYPE_INDIVIDUAL);
1381
1382   signals[SIG_POPUP_INDIVIDUAL_MENU] = g_signal_new ("popup-individual-menu",
1383       G_OBJECT_CLASS_TYPE (klass),
1384       G_SIGNAL_RUN_LAST,
1385       0, NULL, NULL, NULL,
1386       G_TYPE_NONE,
1387       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT, G_TYPE_UINT);
1388
1389   signals[SIG_EVENT_ACTIVATED] = g_signal_new ("event-activated",
1390       G_OBJECT_CLASS_TYPE (klass),
1391       G_SIGNAL_RUN_LAST,
1392       0, NULL, NULL, NULL,
1393       G_TYPE_NONE,
1394       2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_POINTER);
1395
1396   signals[SIG_INDIVIDUAL_TOOLTIP] = g_signal_new ("individual-tooltip",
1397       G_OBJECT_CLASS_TYPE (klass),
1398       G_SIGNAL_RUN_LAST,
1399       0, g_signal_accumulator_true_handled, NULL, NULL,
1400       G_TYPE_BOOLEAN,
1401       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_BOOLEAN, GTK_TYPE_TOOLTIP);
1402
1403   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
1404 }
1405
1406 static void
1407 empathy_roster_view_init (EmpathyRosterView *self)
1408 {
1409   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1410       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
1411
1412   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
1413       NULL, (GDestroyNotify) g_hash_table_unref);
1414   self->priv->roster_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
1415       g_free, NULL);
1416   self->priv->displayed_contacts = g_hash_table_new (NULL, NULL);
1417
1418   self->priv->events = g_queue_new ();
1419
1420   self->priv->empty = TRUE;
1421 }
1422
1423 GtkWidget *
1424 empathy_roster_view_new (EmpathyIndividualManager *manager,
1425     EmpathyRosterModel *model)
1426 {
1427   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
1428   g_return_val_if_fail (EMPATHY_IS_ROSTER_MODEL (model), NULL);
1429
1430   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
1431       "manager", manager,
1432       "model", model,
1433       NULL);
1434 }
1435
1436 EmpathyIndividualManager *
1437 empathy_roster_view_get_manager (EmpathyRosterView *self)
1438 {
1439   return self->priv->manager;
1440 }
1441
1442 void
1443 empathy_roster_view_show_offline (EmpathyRosterView *self,
1444     gboolean show)
1445 {
1446   if (self->priv->show_offline == show)
1447     return;
1448
1449   self->priv->show_offline = show;
1450   egg_list_box_refilter (EGG_LIST_BOX (self));
1451
1452   g_object_notify (G_OBJECT (self), "show-offline");
1453 }
1454
1455 static void
1456 clear_view (EmpathyRosterView *self)
1457 {
1458   gtk_container_foreach (GTK_CONTAINER (self),
1459       (GtkCallback) gtk_widget_destroy, NULL);
1460
1461   g_hash_table_remove_all (self->priv->roster_contacts);
1462   g_hash_table_remove_all (self->priv->roster_groups);
1463   g_hash_table_remove_all (self->priv->displayed_contacts);
1464 }
1465
1466 void
1467 empathy_roster_view_show_groups (EmpathyRosterView *self,
1468     gboolean show)
1469 {
1470   if (self->priv->show_groups == show)
1471     return;
1472
1473   self->priv->show_groups = show;
1474
1475   /* TODO: block sort/filter? */
1476   clear_view (self);
1477   populate_view (self);
1478
1479   g_object_notify (G_OBJECT (self), "show-groups");
1480 }
1481
1482 static void
1483 select_first_contact (EmpathyRosterView *self)
1484 {
1485   GList *children, *l;
1486
1487   children = gtk_container_get_children (GTK_CONTAINER (self));
1488   for (l = children; l != NULL; l = g_list_next (l))
1489     {
1490       GtkWidget *child = l->data;
1491
1492       if (!gtk_widget_get_child_visible (child))
1493         continue;
1494
1495       if (!EMPATHY_IS_ROSTER_CONTACT (child))
1496         continue;
1497
1498       egg_list_box_select_child (EGG_LIST_BOX (self), child);
1499       break;
1500     }
1501
1502   g_list_free (children);
1503 }
1504
1505 static void
1506 search_text_notify_cb (EmpathyLiveSearch *search,
1507     GParamSpec *pspec,
1508     EmpathyRosterView *self)
1509 {
1510   egg_list_box_refilter (EGG_LIST_BOX (self));
1511
1512   select_first_contact (self);
1513 }
1514
1515 static void
1516 search_activate_cb (GtkWidget *search,
1517   EmpathyRosterView *self)
1518 {
1519   EggListBox *box = EGG_LIST_BOX (self);
1520   GtkWidget *child;
1521
1522   child = egg_list_box_get_selected_child (box);
1523   if (child == NULL)
1524     return;
1525
1526   empathy_roster_view_child_activated (box, child);
1527 }
1528
1529 void
1530 empathy_roster_view_set_live_search (EmpathyRosterView *self,
1531     EmpathyLiveSearch *search)
1532 {
1533   if (self->priv->search != NULL)
1534     {
1535       g_signal_handlers_disconnect_by_func (self->priv->search,
1536           search_text_notify_cb, self);
1537       g_signal_handlers_disconnect_by_func (self->priv->search,
1538           search_activate_cb, self);
1539
1540       g_clear_object (&self->priv->search);
1541     }
1542
1543   if (search == NULL)
1544     return;
1545
1546   self->priv->search = g_object_ref (search);
1547
1548   g_signal_connect (self->priv->search, "notify::text",
1549       G_CALLBACK (search_text_notify_cb), self);
1550   g_signal_connect (self->priv->search, "activate",
1551       G_CALLBACK (search_activate_cb), self);
1552 }
1553
1554 gboolean
1555 empathy_roster_view_is_empty (EmpathyRosterView *self)
1556 {
1557   return self->priv->empty;
1558 }
1559
1560 gboolean
1561 empathy_roster_view_is_searching (EmpathyRosterView *self)
1562 {
1563   return (self->priv->search != NULL &&
1564       gtk_widget_get_visible (GTK_WIDGET (self->priv->search)));
1565 }
1566
1567 /* Don't use EmpathyEvent as I prefer to keep this object not too specific to
1568  * Empathy's internals. */
1569 guint
1570 empathy_roster_view_add_event (EmpathyRosterView *self,
1571     FolksIndividual *individual,
1572     const gchar *icon,
1573     gpointer user_data)
1574 {
1575   GHashTable *contacts;
1576
1577   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1578   if (contacts == NULL)
1579     return 0;
1580
1581   self->priv->last_event_id++;
1582
1583   g_queue_push_head (self->priv->events,
1584       event_new (self->priv->last_event_id, individual, icon, user_data));
1585
1586   start_flashing (self);
1587
1588   return self->priv->last_event_id;
1589 }
1590
1591 void
1592 empathy_roster_view_remove_event (EmpathyRosterView *self,
1593     guint event_id)
1594 {
1595   GList *l;
1596
1597   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
1598       l = g_list_next (l))
1599     {
1600       Event *event = l->data;
1601
1602       if (event->id == event_id)
1603         {
1604           remove_event (self, event);
1605           return;
1606         }
1607     }
1608 }
1609
1610 FolksIndividual *
1611 empathy_roster_view_get_selected_individual (EmpathyRosterView *self)
1612 {
1613   GtkWidget *child;
1614
1615   child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1616
1617   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1618     return NULL;
1619
1620   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1621 }