]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
add a signal when a contact is activated
[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 G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, EGG_TYPE_LIST_BOX)
12
13 enum
14 {
15   PROP_MANAGER = 1,
16   PROP_SHOW_OFFLINE,
17   PROP_SHOW_GROUPS,
18   N_PROPS
19 };
20
21 enum
22 {
23   SIG_INDIVIDUAL_ACTIVATED,
24   LAST_SIGNAL
25 };
26
27 static guint signals[LAST_SIGNAL];
28
29 #define NO_GROUP "X-no-group"
30 #define UNGROUPPED _("Ungroupped")
31 #define TOP_GROUP _("Most Used")
32
33 struct _EmpathyRosterViewPriv
34 {
35   EmpathyIndividualManager *manager;
36
37   /* FolksIndividual (borrowed) -> GHashTable (
38    * (gchar * group_name) -> EmpathyRosterContact (borrowed))
39    *
40    * When not using groups, this hash just have one element mapped
41    * from the special NO_GROUP key. We could use it as a set but
42    * I prefer to stay coherent in the way this hash is managed.
43    */
44   GHashTable *roster_contacts;
45   /* (gchar *group_name) -> EmpathyRosterGroup (borrowed) */
46   GHashTable *roster_groups;
47
48   gboolean show_offline;
49   gboolean show_groups;
50
51   EmpathyLiveSearch *search;
52 };
53
54 static void
55 empathy_roster_view_get_property (GObject *object,
56     guint property_id,
57     GValue *value,
58     GParamSpec *pspec)
59 {
60   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
61
62   switch (property_id)
63     {
64       case PROP_MANAGER:
65         g_value_set_object (value, self->priv->manager);
66         break;
67       case PROP_SHOW_OFFLINE:
68         g_value_set_boolean (value, self->priv->show_offline);
69         break;
70       case PROP_SHOW_GROUPS:
71         g_value_set_boolean (value, self->priv->show_groups);
72         break;
73       default:
74         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
75         break;
76     }
77 }
78
79 static void
80 empathy_roster_view_set_property (GObject *object,
81     guint property_id,
82     const GValue *value,
83     GParamSpec *pspec)
84 {
85   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
86
87   switch (property_id)
88     {
89       case PROP_MANAGER:
90         g_assert (self->priv->manager == NULL); /* construct only */
91         self->priv->manager = g_value_dup_object (value);
92         break;
93       case PROP_SHOW_OFFLINE:
94         empathy_roster_view_show_offline (self, g_value_get_boolean (value));
95         break;
96       case PROP_SHOW_GROUPS:
97         empathy_roster_view_show_groups (self, g_value_get_boolean (value));
98         break;
99       default:
100         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
101         break;
102     }
103 }
104
105 static void
106 roster_contact_changed_cb (GtkWidget *child,
107     GParamSpec *spec,
108     EmpathyRosterView *self)
109 {
110   egg_list_box_child_changed (EGG_LIST_BOX (self), child);
111 }
112
113 static GtkWidget *
114 add_roster_contact (EmpathyRosterView *self,
115     FolksIndividual *individual,
116     const gchar *group)
117 {
118   GtkWidget *contact;
119
120   contact = empathy_roster_contact_new (individual, group);
121
122   /* Need to refilter if online is changed */
123   g_signal_connect (contact, "notify::online",
124       G_CALLBACK (roster_contact_changed_cb), self);
125
126   /* Need to resort if alias is changed */
127   g_signal_connect (contact, "notify::alias",
128       G_CALLBACK (roster_contact_changed_cb), self);
129
130   gtk_widget_show (contact);
131   gtk_container_add (GTK_CONTAINER (self), contact);
132
133   return contact;
134 }
135
136 static void
137 group_expanded_cb (EmpathyRosterGroup *group,
138     GParamSpec *spec,
139     EmpathyRosterView *self)
140 {
141   GList *widgets, *l;
142
143   widgets = empathy_roster_group_get_widgets (group);
144   for (l = widgets; l != NULL; l = g_list_next (l))
145     {
146       egg_list_box_child_changed (EGG_LIST_BOX (self), l->data);
147     }
148
149   g_list_free (widgets);
150 }
151
152 static EmpathyRosterGroup *
153 lookup_roster_group (EmpathyRosterView *self,
154     const gchar *group)
155 {
156   return g_hash_table_lookup (self->priv->roster_groups, group);
157 }
158
159 static void
160 ensure_roster_group (EmpathyRosterView *self,
161     const gchar *group)
162 {
163   GtkWidget *roster_group;
164
165   roster_group = (GtkWidget *) lookup_roster_group (self, group);
166   if (roster_group != NULL)
167     return;
168
169   roster_group = empathy_roster_group_new (group);
170
171   g_signal_connect (roster_group, "notify::expanded",
172       G_CALLBACK (group_expanded_cb), self);
173
174   gtk_widget_show (roster_group);
175   gtk_container_add (GTK_CONTAINER (self), roster_group);
176
177   g_hash_table_insert (self->priv->roster_groups, g_strdup (group),
178       roster_group);
179 }
180
181 static void
182 add_to_group (EmpathyRosterView *self,
183     FolksIndividual *individual,
184     const gchar *group)
185 {
186   GtkWidget *contact;
187   GHashTable *contacts;
188
189   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
190   if (contacts == NULL)
191     return;
192
193   if (tp_strdiff (group, NO_GROUP))
194     ensure_roster_group (self, group);
195
196   contact = add_roster_contact (self, individual, group);
197   g_hash_table_insert (contacts, g_strdup (group), contact);
198 }
199
200 static void
201 individual_added (EmpathyRosterView *self,
202     FolksIndividual *individual)
203 {
204   GHashTable *contacts;
205
206   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
207   if (contacts != NULL)
208     return;
209
210   contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
211
212   g_hash_table_insert (self->priv->roster_contacts, individual, contacts);
213
214   if (!self->priv->show_groups)
215     {
216       add_to_group (self, individual, NO_GROUP);
217     }
218   else
219     {
220       GeeSet *groups;
221
222       groups = folks_group_details_get_groups (
223           FOLKS_GROUP_DETAILS (individual));
224
225       if (gee_collection_get_size (GEE_COLLECTION (groups)) > 0)
226         {
227           GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (groups));
228
229           while (iter != NULL && gee_iterator_next (iter))
230             {
231               gchar *group = gee_iterator_get (iter);
232
233               add_to_group (self, individual, group);
234
235               g_free (group);
236             }
237
238           g_clear_object (&iter);
239         }
240       else
241         {
242           /* No group, adds to Ungroupped */
243           add_to_group (self, individual, UNGROUPPED);
244         }
245     }
246 }
247
248 static void
249 update_group_widgets_count (EmpathyRosterView *self,
250     EmpathyRosterGroup *group,
251     EmpathyRosterContact *contact,
252     gboolean displayed)
253 {
254   if (displayed)
255     {
256       if (empathy_roster_group_add_widget (group, GTK_WIDGET (contact)) == 1)
257         {
258           egg_list_box_child_changed (EGG_LIST_BOX (self),
259               GTK_WIDGET (group));
260         }
261     }
262   else
263     {
264       if (empathy_roster_group_remove_widget (group, GTK_WIDGET (contact)) == 0)
265         {
266           egg_list_box_child_changed (EGG_LIST_BOX (self),
267               GTK_WIDGET (group));
268         }
269     }
270 }
271
272 static void
273 individual_removed (EmpathyRosterView *self,
274     FolksIndividual *individual)
275 {
276   GHashTable *contacts;
277   GHashTableIter iter;
278   gpointer key, value;
279
280   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
281   if (contacts == NULL)
282     return;
283
284   g_hash_table_iter_init (&iter, contacts);
285   while (g_hash_table_iter_next (&iter, &key, &value))
286     {
287       const gchar *group_name = key;
288       GtkWidget *contact = value;
289       EmpathyRosterGroup *group;
290
291       group = lookup_roster_group (self, group_name);
292       if (group != NULL)
293         {
294           update_group_widgets_count (self, group,
295               EMPATHY_ROSTER_CONTACT (contact), FALSE);
296         }
297
298       gtk_container_remove (GTK_CONTAINER (self), contact);
299     }
300
301   g_hash_table_remove (self->priv->roster_contacts, individual);
302 }
303
304 static void
305 members_changed_cb (EmpathyIndividualManager *manager,
306     const gchar *message,
307     GList *added,
308     GList *removed,
309     TpChannelGroupChangeReason reason,
310     EmpathyRosterView *self)
311 {
312   GList *l;
313
314   for (l = added; l != NULL; l = g_list_next (l))
315     {
316       FolksIndividual *individual = l->data;
317
318       individual_added (self, individual);
319     }
320
321   for (l = removed; l != NULL; l = g_list_next (l))
322     {
323       FolksIndividual *individual = l->data;
324
325       individual_removed (self, individual);
326     }
327 }
328
329 static gint
330 compare_roster_contacts_by_alias (EmpathyRosterContact *a,
331     EmpathyRosterContact *b)
332 {
333   FolksIndividual *ind_a, *ind_b;
334   const gchar *alias_a, *alias_b;
335
336   ind_a = empathy_roster_contact_get_individual (a);
337   ind_b = empathy_roster_contact_get_individual (b);
338
339   alias_a = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_a));
340   alias_b = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_b));
341
342   return g_ascii_strcasecmp (alias_a, alias_b);
343 }
344
345 static gint
346 compare_individual_top_position (EmpathyRosterView *self,
347     EmpathyRosterContact *a,
348     EmpathyRosterContact *b)
349 {
350   FolksIndividual *ind_a, *ind_b;
351   GList *tops;
352   gint index_a, index_b;
353
354   ind_a = empathy_roster_contact_get_individual (a);
355   ind_b = empathy_roster_contact_get_individual (b);
356
357   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
358
359   index_a = g_list_index (tops, ind_a);
360   index_b = g_list_index (tops, ind_b);
361
362   if (index_a == index_b)
363     return 0;
364
365   if (index_a == -1)
366     return 1;
367
368   if (index_b == -1)
369     return -1;
370
371   return index_a - index_b;
372 }
373
374 static gint
375 compare_roster_contacts_no_group (EmpathyRosterView *self,
376     EmpathyRosterContact *a,
377     EmpathyRosterContact *b)
378 {
379   gint top;
380
381   top = compare_individual_top_position (self, a, b);
382   if (top != 0)
383     return top;
384
385   return compare_roster_contacts_by_alias (a, b);
386 }
387
388 static gint
389 compare_group_names (const gchar *group_a,
390     const gchar *group_b)
391 {
392   if (!tp_strdiff (group_a, TOP_GROUP))
393     return -1;
394
395   if (!tp_strdiff (group_b, TOP_GROUP))
396     return 1;
397
398   return g_ascii_strcasecmp (group_a, group_b);
399 }
400
401 static gint
402 compare_roster_contacts_with_groups (EmpathyRosterView *self,
403     EmpathyRosterContact *a,
404     EmpathyRosterContact *b)
405 {
406   const gchar *group_a, *group_b;
407
408   group_a = empathy_roster_contact_get_group (a);
409   group_b = empathy_roster_contact_get_group (b);
410
411   if (!tp_strdiff (group_a, group_b))
412     /* Same group, compare the contacts */
413     return compare_roster_contacts_by_alias (a, b);
414
415   /* Sort by group */
416   return compare_group_names (group_a, group_b);
417 }
418
419 static gint
420 compare_roster_contacts (EmpathyRosterView *self,
421     EmpathyRosterContact *a,
422     EmpathyRosterContact *b)
423 {
424   if (!self->priv->show_groups)
425     return compare_roster_contacts_no_group (self, a, b);
426   else
427     return compare_roster_contacts_with_groups (self, a, b);
428 }
429
430 static gint
431 compare_roster_groups (EmpathyRosterGroup *a,
432     EmpathyRosterGroup *b)
433 {
434   const gchar *name_a, *name_b;
435
436   name_a = empathy_roster_group_get_name (a);
437   name_b = empathy_roster_group_get_name (b);
438
439   return compare_group_names (name_a, name_b);
440 }
441
442 static gint
443 compare_contact_group (EmpathyRosterContact *contact,
444     EmpathyRosterGroup *group)
445 {
446   const char *contact_group, *group_name;
447
448   contact_group = empathy_roster_contact_get_group (contact);
449   group_name = empathy_roster_group_get_name (group);
450
451   if (!tp_strdiff (contact_group, group_name))
452     /* @contact is in @group, @group has to be displayed first */
453     return 1;
454
455   /* @contact is in a different group, sort by group name */
456   return compare_group_names (contact_group, group_name);
457 }
458
459 static gint
460 roster_view_sort (gconstpointer a,
461     gconstpointer b,
462     gpointer user_data)
463 {
464   EmpathyRosterView *self = user_data;
465
466   if (EMPATHY_IS_ROSTER_CONTACT (a) && EMPATHY_IS_ROSTER_CONTACT (b))
467     return compare_roster_contacts (self, EMPATHY_ROSTER_CONTACT (a),
468         EMPATHY_ROSTER_CONTACT (b));
469   else if (EMPATHY_IS_ROSTER_GROUP (a) && EMPATHY_IS_ROSTER_GROUP (b))
470     return compare_roster_groups (EMPATHY_ROSTER_GROUP (a),
471         EMPATHY_ROSTER_GROUP (b));
472   else if (EMPATHY_IS_ROSTER_CONTACT (a) && EMPATHY_IS_ROSTER_GROUP (b))
473     return compare_contact_group (EMPATHY_ROSTER_CONTACT (a),
474         EMPATHY_ROSTER_GROUP (b));
475   else if (EMPATHY_IS_ROSTER_GROUP (a) && EMPATHY_IS_ROSTER_CONTACT (b))
476     return -1 * compare_contact_group (EMPATHY_ROSTER_CONTACT (b),
477         EMPATHY_ROSTER_GROUP (a));
478
479   g_return_val_if_reached (0);
480 }
481
482 static void
483 update_separator (GtkWidget **separator,
484     GtkWidget *child,
485     GtkWidget *before,
486     gpointer user_data)
487 {
488   if (before == NULL)
489     {
490       /* No separator before the first row */
491       g_clear_object (separator);
492       return;
493     }
494
495   if (*separator != NULL)
496     return;
497
498   *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
499   g_object_ref_sink (*separator);
500 }
501
502 static gboolean
503 is_searching (EmpathyRosterView *self)
504 {
505   if (self->priv->search == NULL)
506     return FALSE;
507
508   return gtk_widget_get_visible (GTK_WIDGET (self->priv->search));
509 }
510
511 static gboolean
512 filter_contact (EmpathyRosterView *self,
513     EmpathyRosterContact *contact)
514 {
515   gboolean displayed;
516
517   if (is_searching (self))
518     {
519       FolksIndividual *individual;
520
521       individual = empathy_roster_contact_get_individual (contact);
522
523       displayed = empathy_individual_match_string (individual,
524           empathy_live_search_get_text (self->priv->search),
525           empathy_live_search_get_words (self->priv->search));
526     }
527   else
528     {
529       if (self->priv->show_offline)
530         {
531           displayed = TRUE;
532         }
533       else
534         {
535           displayed = empathy_roster_contact_is_online (contact);
536         }
537     }
538
539   if (self->priv->show_groups)
540     {
541       const gchar *group_name;
542       EmpathyRosterGroup *group;
543
544       group_name = empathy_roster_contact_get_group (contact);
545       group = lookup_roster_group (self, group_name);
546
547       if (group != NULL)
548         {
549           update_group_widgets_count (self, group, contact, displayed);
550
551           /* When searching, always display even if the group is closed */
552           if (!is_searching (self) &&
553               !gtk_expander_get_expanded (GTK_EXPANDER (group)))
554             return FALSE;
555         }
556     }
557
558   return displayed;
559 }
560
561 static gboolean
562 filter_group (EmpathyRosterView *self,
563     EmpathyRosterGroup *group)
564 {
565   return empathy_roster_group_get_widgets_count (group);
566 }
567
568 static gboolean
569 filter_list (GtkWidget *child,
570     gpointer user_data)
571 {
572   EmpathyRosterView *self = user_data;
573
574   if (EMPATHY_IS_ROSTER_CONTACT (child))
575     return filter_contact (self, EMPATHY_ROSTER_CONTACT (child));
576
577   else if (EMPATHY_IS_ROSTER_GROUP (child))
578     return filter_group (self, EMPATHY_ROSTER_GROUP (child));
579
580   g_return_val_if_reached (FALSE);
581 }
582
583 /* @list: GList of EmpathyRosterContact
584  *
585  * Returns: %TRUE if @list contains an EmpathyRosterContact associated with
586  * @individual */
587 static gboolean
588 individual_in_list (FolksIndividual *individual,
589     GList *list)
590 {
591   GList *l;
592
593   for (l = list; l != NULL; l = g_list_next (l))
594     {
595       EmpathyRosterContact *contact = l->data;
596
597       if (empathy_roster_contact_get_individual (contact) == individual)
598         return TRUE;
599     }
600
601   return FALSE;
602 }
603
604 static void
605 populate_view (EmpathyRosterView *self)
606 {
607   GList *individuals, *l;
608
609   individuals = empathy_individual_manager_get_members (self->priv->manager);
610   for (l = individuals; l != NULL; l = g_list_next (l))
611     {
612       FolksIndividual *individual = l->data;
613
614       individual_added (self, individual);
615     }
616
617   g_list_free (individuals);
618 }
619
620 static void
621 remove_from_group (EmpathyRosterView *self,
622     FolksIndividual *individual,
623     const gchar *group)
624 {
625   GHashTable *contacts;
626   GtkWidget *contact;
627   EmpathyRosterGroup *roster_group;
628
629   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
630   if (contacts == NULL)
631     return;
632
633   contact = g_hash_table_lookup (contacts, group);
634   if (contact == NULL)
635     return;
636
637   g_hash_table_remove (contacts, group);
638
639   if (g_hash_table_size (contacts) == 0)
640     {
641       add_to_group (self, individual, UNGROUPPED);
642     }
643
644   roster_group = lookup_roster_group (self, group);
645
646   if (roster_group != NULL)
647     {
648       update_group_widgets_count (self, roster_group,
649           EMPATHY_ROSTER_CONTACT (contact), FALSE);
650     }
651
652   gtk_container_remove (GTK_CONTAINER (self), contact);
653 }
654
655 static void
656 update_top_contacts (EmpathyRosterView *self)
657 {
658   GList *tops, *l;
659   GList *to_add = NULL, *to_remove = NULL;
660   EmpathyRosterGroup *group;
661
662   if (!self->priv->show_groups)
663     {
664       egg_list_box_resort (EGG_LIST_BOX (self));
665       return;
666     }
667
668   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
669
670   group = g_hash_table_lookup (self->priv->roster_groups, TOP_GROUP);
671   if (group == NULL)
672     {
673       to_add = g_list_copy (tops);
674     }
675   else
676     {
677       GList *contacts;
678
679       contacts = empathy_roster_group_get_widgets (group);
680
681       /* Check which EmpathyRosterContact have to be removed */
682       for (l = contacts; l != NULL; l = g_list_next (l))
683         {
684           EmpathyRosterContact *contact = l->data;
685           FolksIndividual *individual;
686
687           individual = empathy_roster_contact_get_individual (contact);
688
689           if (g_list_find (tops, individual) == NULL)
690             to_remove = g_list_prepend (to_remove, individual);
691         }
692
693       /* Check which EmpathyRosterContact have to be added */
694       for (l = tops; l != NULL; l = g_list_next (l))
695         {
696           FolksIndividual *individual = l->data;
697
698           if (!individual_in_list (individual, contacts))
699             to_add = g_list_prepend (to_add, individual);
700         }
701     }
702
703   for (l = to_add; l != NULL; l = g_list_next (l))
704     add_to_group (self, l->data, TOP_GROUP);
705
706   for (l = to_remove; l != NULL; l = g_list_next (l))
707     remove_from_group (self, l->data, TOP_GROUP);
708
709   g_list_free (to_add);
710   g_list_free (to_remove);
711 }
712
713 static void
714 groups_changed_cb (EmpathyIndividualManager *manager,
715     FolksIndividual *individual,
716     gchar *group,
717     gboolean is_member,
718     EmpathyRosterView *self)
719 {
720   if (!self->priv->show_groups)
721     return;
722
723   if (is_member)
724     {
725       add_to_group (self, individual, group);
726     }
727   else
728     {
729       remove_from_group (self, individual, group);
730     }
731 }
732
733 static void
734 top_individuals_changed_cb (EmpathyIndividualManager *manager,
735     GParamSpec *spec,
736     EmpathyRosterView *self)
737 {
738   update_top_contacts (self);
739 }
740
741 static void
742 empathy_roster_view_constructed (GObject *object)
743 {
744   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
745   void (*chain_up) (GObject *) =
746       ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
747
748   if (chain_up != NULL)
749     chain_up (object);
750
751   g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
752
753   populate_view (self);
754
755   tp_g_signal_connect_object (self->priv->manager, "members-changed",
756       G_CALLBACK (members_changed_cb), self, 0);
757   tp_g_signal_connect_object (self->priv->manager, "groups-changed",
758       G_CALLBACK (groups_changed_cb), self, 0);
759   tp_g_signal_connect_object (self->priv->manager, "notify::top-individuals",
760       G_CALLBACK (top_individuals_changed_cb), self, 0);
761
762   egg_list_box_set_sort_func (EGG_LIST_BOX (self),
763       roster_view_sort, self, NULL);
764
765   egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
766       self, NULL);
767
768   egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
769
770   egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE);
771 }
772
773 static void
774 empathy_roster_view_dispose (GObject *object)
775 {
776   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
777   void (*chain_up) (GObject *) =
778       ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
779
780   empathy_roster_view_set_live_search (self, NULL);
781   g_clear_object (&self->priv->manager);
782
783   if (chain_up != NULL)
784     chain_up (object);
785 }
786
787 static void
788 empathy_roster_view_finalize (GObject *object)
789 {
790   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
791   void (*chain_up) (GObject *) =
792       ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
793
794   g_hash_table_unref (self->priv->roster_contacts);
795   g_hash_table_unref (self->priv->roster_groups);
796
797   if (chain_up != NULL)
798     chain_up (object);
799 }
800
801 static void
802 empathy_roster_view_child_activated (EggListBox *box,
803     GtkWidget *child)
804 {
805   EmpathyRosterContact *contact;
806   FolksIndividual *individual;
807
808   if (!EMPATHY_IS_ROSTER_CONTACT (child))
809     return;
810
811   contact = EMPATHY_ROSTER_CONTACT (child);
812   individual = empathy_roster_contact_get_individual (contact);
813
814   g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual);
815 }
816
817 static void
818 empathy_roster_view_class_init (
819     EmpathyRosterViewClass *klass)
820 {
821   GObjectClass *oclass = G_OBJECT_CLASS (klass);
822   EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass);
823   GParamSpec *spec;
824
825   oclass->get_property = empathy_roster_view_get_property;
826   oclass->set_property = empathy_roster_view_set_property;
827   oclass->constructed = empathy_roster_view_constructed;
828   oclass->dispose = empathy_roster_view_dispose;
829   oclass->finalize = empathy_roster_view_finalize;
830
831   box_class->child_activated = empathy_roster_view_child_activated;
832
833   spec = g_param_spec_object ("manager", "Manager",
834       "EmpathyIndividualManager",
835       EMPATHY_TYPE_INDIVIDUAL_MANAGER,
836       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
837   g_object_class_install_property (oclass, PROP_MANAGER, spec);
838
839   spec = g_param_spec_boolean ("show-offline", "Show Offline",
840       "Show offline contacts",
841       FALSE,
842       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
843   g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
844
845   spec = g_param_spec_boolean ("show-groups", "Show Groups",
846       "Show groups",
847       FALSE,
848       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
849   g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
850
851   signals[SIG_INDIVIDUAL_ACTIVATED] = g_signal_new ("individual-activated",
852       G_OBJECT_CLASS_TYPE (klass),
853       G_SIGNAL_RUN_LAST,
854       0, NULL, NULL, NULL,
855       G_TYPE_NONE,
856       1, FOLKS_TYPE_INDIVIDUAL);
857
858   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
859 }
860
861 static void
862 empathy_roster_view_init (EmpathyRosterView *self)
863 {
864   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
865       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
866
867   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
868       NULL, (GDestroyNotify) g_hash_table_unref);
869   self->priv->roster_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
870       g_free, NULL);
871 }
872
873 GtkWidget *
874 empathy_roster_view_new (EmpathyIndividualManager *manager)
875 {
876   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
877
878   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
879       "manager", manager,
880       NULL);
881 }
882
883 EmpathyIndividualManager *
884 empathy_roster_view_get_manager (EmpathyRosterView *self)
885 {
886   return self->priv->manager;
887 }
888
889 void
890 empathy_roster_view_show_offline (EmpathyRosterView *self,
891     gboolean show)
892 {
893   if (self->priv->show_offline == show)
894     return;
895
896   self->priv->show_offline = show;
897   egg_list_box_refilter (EGG_LIST_BOX (self));
898
899   g_object_notify (G_OBJECT (self), "show-offline");
900 }
901
902 static void
903 clear_view (EmpathyRosterView *self)
904 {
905   gtk_container_foreach (GTK_CONTAINER (self),
906       (GtkCallback) gtk_widget_destroy, NULL);
907
908   g_hash_table_remove_all (self->priv->roster_contacts);
909 }
910
911 void
912 empathy_roster_view_show_groups (EmpathyRosterView *self,
913     gboolean show)
914 {
915   if (self->priv->show_groups == show)
916     return;
917
918   self->priv->show_groups = show;
919
920   /* TODO: block sort/filter? */
921   clear_view (self);
922   populate_view (self);
923
924   g_object_notify (G_OBJECT (self), "show-groups");
925 }
926
927 static void
928 select_first_contact (EmpathyRosterView *self)
929 {
930   GList *children, *l;
931
932   children = gtk_container_get_children (GTK_CONTAINER (self));
933   for (l = children; l != NULL; l = g_list_next (l))
934     {
935       GtkWidget *child = l->data;
936
937       if (!gtk_widget_get_child_visible (child))
938         continue;
939
940       if (!EMPATHY_IS_ROSTER_CONTACT (child))
941         continue;
942
943       egg_list_box_select_child (EGG_LIST_BOX (self), child);
944       break;
945     }
946
947   g_list_free (children);
948 }
949
950 static void
951 search_text_notify_cb (EmpathyLiveSearch *search,
952     GParamSpec *pspec,
953     EmpathyRosterView *self)
954 {
955   egg_list_box_refilter (EGG_LIST_BOX (self));
956
957   select_first_contact (self);
958 }
959
960 static void
961 search_activate_cb (GtkWidget *search,
962   EmpathyRosterView *self)
963 {
964   EggListBox *box = EGG_LIST_BOX (self);
965   GtkWidget *child;
966
967   child = egg_list_box_get_selected_child (box);
968   if (child == NULL)
969     return;
970
971   empathy_roster_view_child_activated (box, child);
972 }
973
974 void
975 empathy_roster_view_set_live_search (EmpathyRosterView *self,
976     EmpathyLiveSearch *search)
977 {
978   if (self->priv->search != NULL)
979     {
980       g_signal_handlers_disconnect_by_func (self->priv->search,
981           search_text_notify_cb, self);
982       g_signal_handlers_disconnect_by_func (self->priv->search,
983           search_activate_cb, self);
984
985       g_clear_object (&self->priv->search);
986     }
987
988   if (search == NULL)
989     return;
990
991   self->priv->search = g_object_ref (search);
992
993   g_signal_connect (self->priv->search, "notify::text",
994       G_CALLBACK (search_text_notify_cb), self);
995   g_signal_connect (self->priv->search, "activate",
996       G_CALLBACK (search_activate_cb), self);
997 }