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