]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
roster-view: add one EmpathyRosterContact per group
[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
9 G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, EGG_TYPE_LIST_BOX)
10
11 enum
12 {
13   PROP_MANAGER = 1,
14   PROP_SHOW_OFFLINE,
15   PROP_SHOW_GROUPS,
16   N_PROPS
17 };
18
19 /*
20 enum
21 {
22   LAST_SIGNAL
23 };
24
25 static guint signals[LAST_SIGNAL];
26 */
27
28 #define NO_GROUP "X-no-group"
29 #define UNGROUPPED _("Ungroupped")
30
31 struct _EmpathyRosterViewPriv
32 {
33   EmpathyIndividualManager *manager;
34
35   /* FolksIndividual (borrowed) -> GHashTable (
36    * (gchar * group_name) -> EmpathyRosterContact (borrowed))
37    *
38    * When not using groups, this hash just have one element mapped
39    * from the special NO_GROUP key. We could use it as a set but
40    * I prefer to stay coherent in the way this hash is managed.
41    */
42   GHashTable *roster_contacts;
43
44   gboolean show_offline;
45   gboolean show_groups;
46 };
47
48 static void
49 empathy_roster_view_get_property (GObject *object,
50     guint property_id,
51     GValue *value,
52     GParamSpec *pspec)
53 {
54   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
55
56   switch (property_id)
57     {
58       case PROP_MANAGER:
59         g_value_set_object (value, self->priv->manager);
60         break;
61       case PROP_SHOW_OFFLINE:
62         g_value_set_boolean (value, self->priv->show_offline);
63         break;
64       case PROP_SHOW_GROUPS:
65         g_value_set_boolean (value, self->priv->show_groups);
66         break;
67       default:
68         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
69         break;
70     }
71 }
72
73 static void
74 empathy_roster_view_set_property (GObject *object,
75     guint property_id,
76     const GValue *value,
77     GParamSpec *pspec)
78 {
79   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
80
81   switch (property_id)
82     {
83       case PROP_MANAGER:
84         g_assert (self->priv->manager == NULL); /* construct only */
85         self->priv->manager = g_value_dup_object (value);
86         break;
87       case PROP_SHOW_OFFLINE:
88         empathy_roster_view_show_offline (self, g_value_get_boolean (value));
89         break;
90       case PROP_SHOW_GROUPS:
91         empathy_roster_view_show_groups (self, g_value_get_boolean (value));
92         break;
93       default:
94         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
95         break;
96     }
97 }
98
99 static void
100 roster_contact_changed_cb (GtkWidget *child,
101     GParamSpec *spec,
102     EmpathyRosterView *self)
103 {
104   egg_list_box_child_changed (EGG_LIST_BOX (self), child);
105 }
106
107 static GtkWidget *
108 add_roster_contact (EmpathyRosterView *self,
109     FolksIndividual *individual,
110     const gchar *group)
111 {
112   GtkWidget *contact;
113
114   contact = empathy_roster_contact_new (individual, group);
115
116   /* Need to refilter if online is changed */
117   g_signal_connect (contact, "notify::online",
118       G_CALLBACK (roster_contact_changed_cb), self);
119
120   /* Need to resort if alias is changed */
121   g_signal_connect (contact, "notify::alias",
122       G_CALLBACK (roster_contact_changed_cb), self);
123
124   gtk_widget_show (contact);
125   gtk_container_add (GTK_CONTAINER (self), contact);
126
127   return contact;
128 }
129
130 static void
131 add_to_group (EmpathyRosterView *self,
132     FolksIndividual *individual,
133     const gchar *group)
134 {
135   GtkWidget *contact;
136   GHashTable *contacts;
137
138   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
139   if (contacts == NULL)
140     return;
141
142   /* TODO: ensure group widget */
143   contact = add_roster_contact (self, individual, group);
144   g_hash_table_insert (contacts, g_strdup (group), contact);
145 }
146
147 static void
148 individual_added (EmpathyRosterView *self,
149     FolksIndividual *individual)
150 {
151   GHashTable *contacts;
152
153   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
154   if (contacts != NULL)
155     return;
156
157   contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
158
159   g_hash_table_insert (self->priv->roster_contacts, individual, contacts);
160
161   if (!self->priv->show_groups)
162     {
163       add_to_group (self, individual, NO_GROUP);
164     }
165   else
166     {
167       GeeSet *groups;
168
169       groups = folks_group_details_get_groups (
170           FOLKS_GROUP_DETAILS (individual));
171
172       if (gee_collection_get_size (GEE_COLLECTION (groups)) > 0)
173         {
174           GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (groups));
175
176           while (iter != NULL && gee_iterator_next (iter))
177             {
178               gchar *group = gee_iterator_get (iter);
179
180               add_to_group (self, individual, group);
181
182               g_free (group);
183             }
184
185           g_clear_object (&iter);
186         }
187       else
188         {
189           /* No group, adds to Ungroupped */
190           add_to_group (self, individual, UNGROUPPED);
191         }
192     }
193 }
194
195 static void
196 individual_removed (EmpathyRosterView *self,
197     FolksIndividual *individual)
198 {
199   GHashTable *contacts;
200   GHashTableIter iter;
201   gpointer value;
202
203   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
204   if (contacts == NULL)
205     return;
206
207   g_hash_table_iter_init (&iter, contacts);
208   while (g_hash_table_iter_next (&iter, NULL, &value))
209     {
210       GtkWidget *contact = value;
211
212       gtk_container_remove (GTK_CONTAINER (self), contact);
213     }
214
215   g_hash_table_remove (self->priv->roster_contacts, individual);
216 }
217
218 static void
219 members_changed_cb (EmpathyIndividualManager *manager,
220     const gchar *message,
221     GList *added,
222     GList *removed,
223     TpChannelGroupChangeReason reason,
224     EmpathyRosterView *self)
225 {
226   GList *l;
227
228   for (l = added; l != NULL; l = g_list_next (l))
229     {
230       FolksIndividual *individual = l->data;
231
232       individual_added (self, individual);
233     }
234
235   for (l = removed; l != NULL; l = g_list_next (l))
236     {
237       FolksIndividual *individual = l->data;
238
239       individual_removed (self, individual);
240     }
241 }
242
243 static gint
244 roster_view_sort (EmpathyRosterContact *a,
245     EmpathyRosterContact *b,
246     EmpathyRosterView *self)
247 {
248   FolksIndividual *ind_a, *ind_b;
249   const gchar *alias_a, *alias_b;
250
251   ind_a = empathy_roster_contact_get_individual (a);
252   ind_b = empathy_roster_contact_get_individual (b);
253
254   alias_a = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_a));
255   alias_b = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_b));
256
257   return g_ascii_strcasecmp (alias_a, alias_b);
258 }
259
260 static void
261 update_separator (GtkWidget **separator,
262     GtkWidget *child,
263     GtkWidget *before,
264     gpointer user_data)
265 {
266   if (before == NULL)
267     {
268       /* No separator before the first row */
269       g_clear_object (separator);
270       return;
271     }
272
273   if (*separator != NULL)
274     return;
275
276   *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
277   g_object_ref_sink (*separator);
278 }
279
280 static gboolean
281 filter_list (GtkWidget *child,
282     gpointer user_data)
283 {
284   EmpathyRosterView *self = user_data;
285   EmpathyRosterContact *contact = EMPATHY_ROSTER_CONTACT (child);
286
287   if (self->priv->show_offline)
288     return TRUE;
289
290   return empathy_roster_contact_is_online (contact);
291 }
292
293 static void
294 populate_view (EmpathyRosterView *self)
295 {
296   GList *individuals, *l;
297
298   individuals = empathy_individual_manager_get_members (self->priv->manager);
299   for (l = individuals; l != NULL; l = g_list_next (l))
300     {
301       FolksIndividual *individual = l->data;
302
303       individual_added (self, individual);
304     }
305
306   g_list_free (individuals);
307 }
308
309 static void
310 remove_from_group (EmpathyRosterView *self,
311     FolksIndividual *individual,
312     const gchar *group)
313 {
314   GHashTable *contacts;
315   GtkWidget *contact;
316
317   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
318   if (contacts == NULL)
319     return;
320
321   contact = g_hash_table_lookup (contacts, group);
322   if (contact == NULL)
323     return;
324
325   gtk_container_remove (GTK_CONTAINER (self), contact);
326   g_hash_table_remove (contacts, group);
327
328   if (g_hash_table_size (contacts) == 0)
329     {
330       add_to_group (self, individual, UNGROUPPED);
331     }
332 }
333
334 static void
335 groups_changed_cb (EmpathyIndividualManager *manager,
336     FolksIndividual *individual,
337     gchar *group,
338     gboolean is_member,
339     EmpathyRosterView *self)
340 {
341   if (!self->priv->show_groups)
342     return;
343
344   if (is_member)
345     {
346       add_to_group (self, individual, group);
347     }
348   else
349     {
350       remove_from_group (self, individual, group);
351     }
352 }
353
354 static void
355 empathy_roster_view_constructed (GObject *object)
356 {
357   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
358   void (*chain_up) (GObject *) =
359       ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
360
361   if (chain_up != NULL)
362     chain_up (object);
363
364   g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
365
366   populate_view (self);
367
368   tp_g_signal_connect_object (self->priv->manager, "members-changed",
369       G_CALLBACK (members_changed_cb), self, 0);
370   tp_g_signal_connect_object (self->priv->manager, "groups-changed",
371       G_CALLBACK (groups_changed_cb), self, 0);
372
373   egg_list_box_set_sort_func (EGG_LIST_BOX (self),
374       (GCompareDataFunc) roster_view_sort, self, NULL);
375
376   egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
377       self, NULL);
378
379   egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
380 }
381
382 static void
383 empathy_roster_view_dispose (GObject *object)
384 {
385   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
386   void (*chain_up) (GObject *) =
387       ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
388
389   g_clear_object (&self->priv->manager);
390
391   if (chain_up != NULL)
392     chain_up (object);
393 }
394
395 static void
396 empathy_roster_view_finalize (GObject *object)
397 {
398   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
399   void (*chain_up) (GObject *) =
400       ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
401
402   g_hash_table_unref (self->priv->roster_contacts);
403
404   if (chain_up != NULL)
405     chain_up (object);
406 }
407
408 static void
409 empathy_roster_view_class_init (
410     EmpathyRosterViewClass *klass)
411 {
412   GObjectClass *oclass = G_OBJECT_CLASS (klass);
413   GParamSpec *spec;
414
415   oclass->get_property = empathy_roster_view_get_property;
416   oclass->set_property = empathy_roster_view_set_property;
417   oclass->constructed = empathy_roster_view_constructed;
418   oclass->dispose = empathy_roster_view_dispose;
419   oclass->finalize = empathy_roster_view_finalize;
420
421   spec = g_param_spec_object ("manager", "Manager",
422       "EmpathyIndividualManager",
423       EMPATHY_TYPE_INDIVIDUAL_MANAGER,
424       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
425   g_object_class_install_property (oclass, PROP_MANAGER, spec);
426
427   spec = g_param_spec_boolean ("show-offline", "Show Offline",
428       "Show offline contacts",
429       FALSE,
430       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
431   g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
432
433   spec = g_param_spec_boolean ("show-groups", "Show Groups",
434       "Show groups",
435       FALSE,
436       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
437   g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
438
439   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
440 }
441
442 static void
443 empathy_roster_view_init (EmpathyRosterView *self)
444 {
445   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
446       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
447
448   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
449       NULL, (GDestroyNotify) g_hash_table_unref);
450 }
451
452 GtkWidget *
453 empathy_roster_view_new (EmpathyIndividualManager *manager)
454 {
455   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
456
457   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
458       "manager", manager,
459       NULL);
460 }
461
462 EmpathyIndividualManager *
463 empathy_roster_view_get_manager (EmpathyRosterView *self)
464 {
465   return self->priv->manager;
466 }
467
468 void
469 empathy_roster_view_show_offline (EmpathyRosterView *self,
470     gboolean show)
471 {
472   if (self->priv->show_offline == show)
473     return;
474
475   self->priv->show_offline = show;
476   egg_list_box_refilter (EGG_LIST_BOX (self));
477
478   g_object_notify (G_OBJECT (self), "show-offline");
479 }
480
481 static void
482 clear_view (EmpathyRosterView *self)
483 {
484   gtk_container_foreach (GTK_CONTAINER (self),
485       (GtkCallback) gtk_widget_destroy, NULL);
486
487   g_hash_table_remove_all (self->priv->roster_contacts);
488 }
489
490 void
491 empathy_roster_view_show_groups (EmpathyRosterView *self,
492     gboolean show)
493 {
494   if (self->priv->show_groups == show)
495     return;
496
497   self->priv->show_groups = show;
498
499   /* TODO: block sort/filter? */
500   clear_view (self);
501   populate_view (self);
502
503   g_object_notify (G_OBJECT (self), "show-groups");
504 }