3 #include "empathy-roster-view.h"
5 #include <glib/gi18n-lib.h>
7 #include <libempathy-gtk/empathy-roster-contact.h>
8 #include <libempathy-gtk/empathy-roster-group.h>
9 #include <libempathy-gtk/empathy-ui-utils.h>
11 G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, EGG_TYPE_LIST_BOX)
23 SIG_INDIVIDUAL_ACTIVATED,
27 static guint signals[LAST_SIGNAL];
29 #define NO_GROUP "X-no-group"
30 #define UNGROUPPED _("Ungroupped")
31 #define TOP_GROUP _("Most Used")
33 struct _EmpathyRosterViewPriv
35 EmpathyIndividualManager *manager;
37 /* FolksIndividual (borrowed) -> GHashTable (
38 * (gchar * group_name) -> EmpathyRosterContact (borrowed))
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.
44 GHashTable *roster_contacts;
45 /* (gchar *group_name) -> EmpathyRosterGroup (borrowed) */
46 GHashTable *roster_groups;
48 gboolean show_offline;
51 EmpathyLiveSearch *search;
55 empathy_roster_view_get_property (GObject *object,
60 EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
65 g_value_set_object (value, self->priv->manager);
67 case PROP_SHOW_OFFLINE:
68 g_value_set_boolean (value, self->priv->show_offline);
70 case PROP_SHOW_GROUPS:
71 g_value_set_boolean (value, self->priv->show_groups);
74 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
80 empathy_roster_view_set_property (GObject *object,
85 EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
90 g_assert (self->priv->manager == NULL); /* construct only */
91 self->priv->manager = g_value_dup_object (value);
93 case PROP_SHOW_OFFLINE:
94 empathy_roster_view_show_offline (self, g_value_get_boolean (value));
96 case PROP_SHOW_GROUPS:
97 empathy_roster_view_show_groups (self, g_value_get_boolean (value));
100 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
106 roster_contact_changed_cb (GtkWidget *child,
108 EmpathyRosterView *self)
110 egg_list_box_child_changed (EGG_LIST_BOX (self), child);
114 add_roster_contact (EmpathyRosterView *self,
115 FolksIndividual *individual,
120 contact = empathy_roster_contact_new (individual, group);
122 /* Need to refilter if online is changed */
123 g_signal_connect (contact, "notify::online",
124 G_CALLBACK (roster_contact_changed_cb), self);
126 /* Need to resort if alias is changed */
127 g_signal_connect (contact, "notify::alias",
128 G_CALLBACK (roster_contact_changed_cb), self);
130 gtk_widget_show (contact);
131 gtk_container_add (GTK_CONTAINER (self), contact);
137 group_expanded_cb (EmpathyRosterGroup *group,
139 EmpathyRosterView *self)
143 widgets = empathy_roster_group_get_widgets (group);
144 for (l = widgets; l != NULL; l = g_list_next (l))
146 egg_list_box_child_changed (EGG_LIST_BOX (self), l->data);
149 g_list_free (widgets);
152 static EmpathyRosterGroup *
153 lookup_roster_group (EmpathyRosterView *self,
156 return g_hash_table_lookup (self->priv->roster_groups, group);
160 ensure_roster_group (EmpathyRosterView *self,
163 GtkWidget *roster_group;
165 roster_group = (GtkWidget *) lookup_roster_group (self, group);
166 if (roster_group != NULL)
169 roster_group = empathy_roster_group_new (group);
171 g_signal_connect (roster_group, "notify::expanded",
172 G_CALLBACK (group_expanded_cb), self);
174 gtk_widget_show (roster_group);
175 gtk_container_add (GTK_CONTAINER (self), roster_group);
177 g_hash_table_insert (self->priv->roster_groups, g_strdup (group),
182 add_to_group (EmpathyRosterView *self,
183 FolksIndividual *individual,
187 GHashTable *contacts;
189 contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
190 if (contacts == NULL)
193 if (tp_strdiff (group, NO_GROUP))
194 ensure_roster_group (self, group);
196 contact = add_roster_contact (self, individual, group);
197 g_hash_table_insert (contacts, g_strdup (group), contact);
201 individual_added (EmpathyRosterView *self,
202 FolksIndividual *individual)
204 GHashTable *contacts;
206 contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
207 if (contacts != NULL)
210 contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
212 g_hash_table_insert (self->priv->roster_contacts, individual, contacts);
214 if (!self->priv->show_groups)
216 add_to_group (self, individual, NO_GROUP);
222 groups = folks_group_details_get_groups (
223 FOLKS_GROUP_DETAILS (individual));
225 if (gee_collection_get_size (GEE_COLLECTION (groups)) > 0)
227 GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (groups));
229 while (iter != NULL && gee_iterator_next (iter))
231 gchar *group = gee_iterator_get (iter);
233 add_to_group (self, individual, group);
238 g_clear_object (&iter);
242 /* No group, adds to Ungroupped */
243 add_to_group (self, individual, UNGROUPPED);
249 update_group_widgets_count (EmpathyRosterView *self,
250 EmpathyRosterGroup *group,
251 EmpathyRosterContact *contact,
256 if (empathy_roster_group_add_widget (group, GTK_WIDGET (contact)) == 1)
258 egg_list_box_child_changed (EGG_LIST_BOX (self),
264 if (empathy_roster_group_remove_widget (group, GTK_WIDGET (contact)) == 0)
266 egg_list_box_child_changed (EGG_LIST_BOX (self),
273 individual_removed (EmpathyRosterView *self,
274 FolksIndividual *individual)
276 GHashTable *contacts;
280 contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
281 if (contacts == NULL)
284 g_hash_table_iter_init (&iter, contacts);
285 while (g_hash_table_iter_next (&iter, &key, &value))
287 const gchar *group_name = key;
288 GtkWidget *contact = value;
289 EmpathyRosterGroup *group;
291 group = lookup_roster_group (self, group_name);
294 update_group_widgets_count (self, group,
295 EMPATHY_ROSTER_CONTACT (contact), FALSE);
298 gtk_container_remove (GTK_CONTAINER (self), contact);
301 g_hash_table_remove (self->priv->roster_contacts, individual);
305 members_changed_cb (EmpathyIndividualManager *manager,
306 const gchar *message,
309 TpChannelGroupChangeReason reason,
310 EmpathyRosterView *self)
314 for (l = added; l != NULL; l = g_list_next (l))
316 FolksIndividual *individual = l->data;
318 individual_added (self, individual);
321 for (l = removed; l != NULL; l = g_list_next (l))
323 FolksIndividual *individual = l->data;
325 individual_removed (self, individual);
330 compare_roster_contacts_by_alias (EmpathyRosterContact *a,
331 EmpathyRosterContact *b)
333 FolksIndividual *ind_a, *ind_b;
334 const gchar *alias_a, *alias_b;
336 ind_a = empathy_roster_contact_get_individual (a);
337 ind_b = empathy_roster_contact_get_individual (b);
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));
342 return g_ascii_strcasecmp (alias_a, alias_b);
346 compare_individual_top_position (EmpathyRosterView *self,
347 EmpathyRosterContact *a,
348 EmpathyRosterContact *b)
350 FolksIndividual *ind_a, *ind_b;
352 gint index_a, index_b;
354 ind_a = empathy_roster_contact_get_individual (a);
355 ind_b = empathy_roster_contact_get_individual (b);
357 tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
359 index_a = g_list_index (tops, ind_a);
360 index_b = g_list_index (tops, ind_b);
362 if (index_a == index_b)
371 return index_a - index_b;
375 compare_roster_contacts_no_group (EmpathyRosterView *self,
376 EmpathyRosterContact *a,
377 EmpathyRosterContact *b)
381 top = compare_individual_top_position (self, a, b);
385 return compare_roster_contacts_by_alias (a, b);
389 compare_group_names (const gchar *group_a,
390 const gchar *group_b)
392 if (!tp_strdiff (group_a, TOP_GROUP))
395 if (!tp_strdiff (group_b, TOP_GROUP))
398 return g_ascii_strcasecmp (group_a, group_b);
402 compare_roster_contacts_with_groups (EmpathyRosterView *self,
403 EmpathyRosterContact *a,
404 EmpathyRosterContact *b)
406 const gchar *group_a, *group_b;
408 group_a = empathy_roster_contact_get_group (a);
409 group_b = empathy_roster_contact_get_group (b);
411 if (!tp_strdiff (group_a, group_b))
412 /* Same group, compare the contacts */
413 return compare_roster_contacts_by_alias (a, b);
416 return compare_group_names (group_a, group_b);
420 compare_roster_contacts (EmpathyRosterView *self,
421 EmpathyRosterContact *a,
422 EmpathyRosterContact *b)
424 if (!self->priv->show_groups)
425 return compare_roster_contacts_no_group (self, a, b);
427 return compare_roster_contacts_with_groups (self, a, b);
431 compare_roster_groups (EmpathyRosterGroup *a,
432 EmpathyRosterGroup *b)
434 const gchar *name_a, *name_b;
436 name_a = empathy_roster_group_get_name (a);
437 name_b = empathy_roster_group_get_name (b);
439 return compare_group_names (name_a, name_b);
443 compare_contact_group (EmpathyRosterContact *contact,
444 EmpathyRosterGroup *group)
446 const char *contact_group, *group_name;
448 contact_group = empathy_roster_contact_get_group (contact);
449 group_name = empathy_roster_group_get_name (group);
451 if (!tp_strdiff (contact_group, group_name))
452 /* @contact is in @group, @group has to be displayed first */
455 /* @contact is in a different group, sort by group name */
456 return compare_group_names (contact_group, group_name);
460 roster_view_sort (gconstpointer a,
464 EmpathyRosterView *self = user_data;
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));
479 g_return_val_if_reached (0);
483 update_separator (GtkWidget **separator,
490 /* No separator before the first row */
491 g_clear_object (separator);
495 if (*separator != NULL)
498 *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
499 g_object_ref_sink (*separator);
503 is_searching (EmpathyRosterView *self)
505 if (self->priv->search == NULL)
508 return gtk_widget_get_visible (GTK_WIDGET (self->priv->search));
512 filter_contact (EmpathyRosterView *self,
513 EmpathyRosterContact *contact)
517 if (is_searching (self))
519 FolksIndividual *individual;
521 individual = empathy_roster_contact_get_individual (contact);
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));
529 if (self->priv->show_offline)
535 displayed = empathy_roster_contact_is_online (contact);
539 if (self->priv->show_groups)
541 const gchar *group_name;
542 EmpathyRosterGroup *group;
544 group_name = empathy_roster_contact_get_group (contact);
545 group = lookup_roster_group (self, group_name);
549 update_group_widgets_count (self, group, contact, displayed);
551 /* When searching, always display even if the group is closed */
552 if (!is_searching (self) &&
553 !gtk_expander_get_expanded (GTK_EXPANDER (group)))
562 filter_group (EmpathyRosterView *self,
563 EmpathyRosterGroup *group)
565 return empathy_roster_group_get_widgets_count (group);
569 filter_list (GtkWidget *child,
572 EmpathyRosterView *self = user_data;
574 if (EMPATHY_IS_ROSTER_CONTACT (child))
575 return filter_contact (self, EMPATHY_ROSTER_CONTACT (child));
577 else if (EMPATHY_IS_ROSTER_GROUP (child))
578 return filter_group (self, EMPATHY_ROSTER_GROUP (child));
580 g_return_val_if_reached (FALSE);
583 /* @list: GList of EmpathyRosterContact
585 * Returns: %TRUE if @list contains an EmpathyRosterContact associated with
588 individual_in_list (FolksIndividual *individual,
593 for (l = list; l != NULL; l = g_list_next (l))
595 EmpathyRosterContact *contact = l->data;
597 if (empathy_roster_contact_get_individual (contact) == individual)
605 populate_view (EmpathyRosterView *self)
607 GList *individuals, *l;
609 individuals = empathy_individual_manager_get_members (self->priv->manager);
610 for (l = individuals; l != NULL; l = g_list_next (l))
612 FolksIndividual *individual = l->data;
614 individual_added (self, individual);
617 g_list_free (individuals);
621 remove_from_group (EmpathyRosterView *self,
622 FolksIndividual *individual,
625 GHashTable *contacts;
627 EmpathyRosterGroup *roster_group;
629 contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
630 if (contacts == NULL)
633 contact = g_hash_table_lookup (contacts, group);
637 g_hash_table_remove (contacts, group);
639 if (g_hash_table_size (contacts) == 0)
641 add_to_group (self, individual, UNGROUPPED);
644 roster_group = lookup_roster_group (self, group);
646 if (roster_group != NULL)
648 update_group_widgets_count (self, roster_group,
649 EMPATHY_ROSTER_CONTACT (contact), FALSE);
652 gtk_container_remove (GTK_CONTAINER (self), contact);
656 update_top_contacts (EmpathyRosterView *self)
659 GList *to_add = NULL, *to_remove = NULL;
660 EmpathyRosterGroup *group;
662 if (!self->priv->show_groups)
664 egg_list_box_resort (EGG_LIST_BOX (self));
668 tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
670 group = g_hash_table_lookup (self->priv->roster_groups, TOP_GROUP);
673 to_add = g_list_copy (tops);
679 contacts = empathy_roster_group_get_widgets (group);
681 /* Check which EmpathyRosterContact have to be removed */
682 for (l = contacts; l != NULL; l = g_list_next (l))
684 EmpathyRosterContact *contact = l->data;
685 FolksIndividual *individual;
687 individual = empathy_roster_contact_get_individual (contact);
689 if (g_list_find (tops, individual) == NULL)
690 to_remove = g_list_prepend (to_remove, individual);
693 /* Check which EmpathyRosterContact have to be added */
694 for (l = tops; l != NULL; l = g_list_next (l))
696 FolksIndividual *individual = l->data;
698 if (!individual_in_list (individual, contacts))
699 to_add = g_list_prepend (to_add, individual);
703 for (l = to_add; l != NULL; l = g_list_next (l))
704 add_to_group (self, l->data, TOP_GROUP);
706 for (l = to_remove; l != NULL; l = g_list_next (l))
707 remove_from_group (self, l->data, TOP_GROUP);
709 g_list_free (to_add);
710 g_list_free (to_remove);
714 groups_changed_cb (EmpathyIndividualManager *manager,
715 FolksIndividual *individual,
718 EmpathyRosterView *self)
720 if (!self->priv->show_groups)
725 add_to_group (self, individual, group);
729 remove_from_group (self, individual, group);
734 top_individuals_changed_cb (EmpathyIndividualManager *manager,
736 EmpathyRosterView *self)
738 update_top_contacts (self);
742 empathy_roster_view_constructed (GObject *object)
744 EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
745 void (*chain_up) (GObject *) =
746 ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
748 if (chain_up != NULL)
751 g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
753 populate_view (self);
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);
762 egg_list_box_set_sort_func (EGG_LIST_BOX (self),
763 roster_view_sort, self, NULL);
765 egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
768 egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
770 egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE);
774 empathy_roster_view_dispose (GObject *object)
776 EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
777 void (*chain_up) (GObject *) =
778 ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
780 empathy_roster_view_set_live_search (self, NULL);
781 g_clear_object (&self->priv->manager);
783 if (chain_up != NULL)
788 empathy_roster_view_finalize (GObject *object)
790 EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
791 void (*chain_up) (GObject *) =
792 ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
794 g_hash_table_unref (self->priv->roster_contacts);
795 g_hash_table_unref (self->priv->roster_groups);
797 if (chain_up != NULL)
802 empathy_roster_view_child_activated (EggListBox *box,
805 EmpathyRosterContact *contact;
806 FolksIndividual *individual;
808 if (!EMPATHY_IS_ROSTER_CONTACT (child))
811 contact = EMPATHY_ROSTER_CONTACT (child);
812 individual = empathy_roster_contact_get_individual (contact);
814 g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual);
818 empathy_roster_view_class_init (
819 EmpathyRosterViewClass *klass)
821 GObjectClass *oclass = G_OBJECT_CLASS (klass);
822 EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass);
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;
831 box_class->child_activated = empathy_roster_view_child_activated;
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);
839 spec = g_param_spec_boolean ("show-offline", "Show Offline",
840 "Show offline contacts",
842 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
843 g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
845 spec = g_param_spec_boolean ("show-groups", "Show Groups",
848 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
849 g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
851 signals[SIG_INDIVIDUAL_ACTIVATED] = g_signal_new ("individual-activated",
852 G_OBJECT_CLASS_TYPE (klass),
856 1, FOLKS_TYPE_INDIVIDUAL);
858 g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
862 empathy_roster_view_init (EmpathyRosterView *self)
864 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
865 EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
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,
874 empathy_roster_view_new (EmpathyIndividualManager *manager)
876 g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
878 return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
883 EmpathyIndividualManager *
884 empathy_roster_view_get_manager (EmpathyRosterView *self)
886 return self->priv->manager;
890 empathy_roster_view_show_offline (EmpathyRosterView *self,
893 if (self->priv->show_offline == show)
896 self->priv->show_offline = show;
897 egg_list_box_refilter (EGG_LIST_BOX (self));
899 g_object_notify (G_OBJECT (self), "show-offline");
903 clear_view (EmpathyRosterView *self)
905 gtk_container_foreach (GTK_CONTAINER (self),
906 (GtkCallback) gtk_widget_destroy, NULL);
908 g_hash_table_remove_all (self->priv->roster_contacts);
912 empathy_roster_view_show_groups (EmpathyRosterView *self,
915 if (self->priv->show_groups == show)
918 self->priv->show_groups = show;
920 /* TODO: block sort/filter? */
922 populate_view (self);
924 g_object_notify (G_OBJECT (self), "show-groups");
928 select_first_contact (EmpathyRosterView *self)
932 children = gtk_container_get_children (GTK_CONTAINER (self));
933 for (l = children; l != NULL; l = g_list_next (l))
935 GtkWidget *child = l->data;
937 if (!gtk_widget_get_child_visible (child))
940 if (!EMPATHY_IS_ROSTER_CONTACT (child))
943 egg_list_box_select_child (EGG_LIST_BOX (self), child);
947 g_list_free (children);
951 search_text_notify_cb (EmpathyLiveSearch *search,
953 EmpathyRosterView *self)
955 egg_list_box_refilter (EGG_LIST_BOX (self));
957 select_first_contact (self);
961 search_activate_cb (GtkWidget *search,
962 EmpathyRosterView *self)
964 EggListBox *box = EGG_LIST_BOX (self);
967 child = egg_list_box_get_selected_child (box);
971 empathy_roster_view_child_activated (box, child);
975 empathy_roster_view_set_live_search (EmpathyRosterView *self,
976 EmpathyLiveSearch *search)
978 if (self->priv->search != NULL)
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);
985 g_clear_object (&self->priv->search);
991 self->priv->search = g_object_ref (search);
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);