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