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