X-Git-Url: https://git.0d.be/?p=empathy.git;a=blobdiff_plain;f=libempathy-gtk%2Fempathy-roster-view.c;h=e463bd9f0ac539f53a2437b1c1e1112902f4213a;hp=bb10655b7036b399f73f1a902b7c2fbad53ae5eb;hb=ac5be92f345b767b423f70befdb9bfbb199474b4;hpb=ab756d6ee9c60b803d8b7785cbc507b6a96580bf diff --git a/libempathy-gtk/empathy-roster-view.c b/libempathy-gtk/empathy-roster-view.c index bb10655b..e463bd9f 100644 --- a/libempathy-gtk/empathy-roster-view.c +++ b/libempathy-gtk/empathy-roster-view.c @@ -1,18 +1,25 @@ #include "config.h" - #include "empathy-roster-view.h" #include -#include -#include -#include +#include "empathy-contact-groups.h" +#include "empathy-roster-contact.h" +#include "empathy-roster-group.h" +#include "empathy-ui-utils.h" + +G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, GTK_TYPE_LIST_BOX) -G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, EGG_TYPE_LIST_BOX) +/* Flashing delay for icons (milliseconds). */ +#define FLASH_TIMEOUT 500 + +/* Delay in milliseconds between the last stroke on the keyboard and the start + * of the live search. */ +#define SEARCH_TIMEOUT 500 enum { - PROP_MANAGER = 1, + PROP_MODEL = 1, PROP_SHOW_OFFLINE, PROP_SHOW_GROUPS, PROP_EMPTY, @@ -23,19 +30,17 @@ enum { SIG_INDIVIDUAL_ACTIVATED, SIG_POPUP_INDIVIDUAL_MENU, + SIG_EVENT_ACTIVATED, + SIG_INDIVIDUAL_TOOLTIP, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; #define NO_GROUP "X-no-group" -#define UNGROUPPED _("Ungroupped") -#define TOP_GROUP _("Most Used") struct _EmpathyRosterViewPriv { - EmpathyIndividualManager *manager; - /* FolksIndividual (borrowed) -> GHashTable ( * (gchar * group_name) -> EmpathyRosterContact (borrowed)) * @@ -49,16 +54,62 @@ struct _EmpathyRosterViewPriv /* Hash of the EmpathyRosterContact currently displayed */ GHashTable *displayed_contacts; + guint last_event_id; + /* queue of (Event *). The most recent events are in the head of the queue + * so we always display the icon of the oldest one. */ + GQueue *events; + guint flash_id; + gboolean display_flash_event; + + guint search_id; + gboolean show_offline; gboolean show_groups; gboolean empty; - EmpathyLiveSearch *search; + TpawLiveSearch *search; - EmpathyRosterViewIndividualTooltipCb individual_tooltip_cb; - gpointer individual_tooltip_data; + EmpathyRosterModel *model; }; +/* Prototypes to break cycles */ +static void remove_from_group (EmpathyRosterView *self, + FolksIndividual *individual, + const gchar *group); + +typedef struct +{ + guint id; + FolksIndividual *individual; + gchar *icon; + gpointer user_data; +} Event; + +static Event * +event_new (guint id, + FolksIndividual *individual, + const gchar *icon, + gpointer user_data) +{ + Event *event = g_slice_new (Event); + + event->id = id; + event->individual = g_object_ref (individual); + event->icon = g_strdup (icon); + event->user_data = user_data; + return event; +} + +static void +event_free (gpointer data) +{ + Event *event = data; + g_object_unref (event->individual); + g_free (event->icon); + + g_slice_free (Event, event); +} + static void empathy_roster_view_get_property (GObject *object, guint property_id, @@ -69,8 +120,8 @@ empathy_roster_view_get_property (GObject *object, switch (property_id) { - case PROP_MANAGER: - g_value_set_object (value, self->priv->manager); + case PROP_MODEL: + g_value_set_object (value, self->priv->model); break; case PROP_SHOW_OFFLINE: g_value_set_boolean (value, self->priv->show_offline); @@ -97,9 +148,9 @@ empathy_roster_view_set_property (GObject *object, switch (property_id) { - case PROP_MANAGER: - g_assert (self->priv->manager == NULL); /* construct only */ - self->priv->manager = g_value_dup_object (value); + case PROP_MODEL: + g_assert (self->priv->model == NULL); + self->priv->model = g_value_dup_object (value); break; case PROP_SHOW_OFFLINE: empathy_roster_view_show_offline (self, g_value_get_boolean (value)); @@ -114,11 +165,11 @@ empathy_roster_view_set_property (GObject *object, } static void -roster_contact_changed_cb (GtkWidget *child, +roster_contact_changed_cb (GtkListBoxRow *child, GParamSpec *spec, EmpathyRosterView *self) { - egg_list_box_child_changed (EGG_LIST_BOX (self), child); + gtk_list_box_row_changed (child); } static GtkWidget * @@ -138,6 +189,10 @@ add_roster_contact (EmpathyRosterView *self, g_signal_connect (contact, "notify::alias", G_CALLBACK (roster_contact_changed_cb), self); + /* Need to resort if most recent event changed */ + g_signal_connect (contact, "notify::most-recent-event", + G_CALLBACK (roster_contact_changed_cb), self); + gtk_widget_show (contact); gtk_container_add (GTK_CONTAINER (self), contact); @@ -145,19 +200,22 @@ add_roster_contact (EmpathyRosterView *self, } static void -group_expanded_cb (EmpathyRosterGroup *group, +group_expanded_cb (GtkWidget *expander, GParamSpec *spec, - EmpathyRosterView *self) + EmpathyRosterGroup *group) { GList *widgets, *l; widgets = empathy_roster_group_get_widgets (group); for (l = widgets; l != NULL; l = g_list_next (l)) { - egg_list_box_child_changed (EGG_LIST_BOX (self), l->data); + gtk_list_box_row_changed (l->data); } g_list_free (widgets); + + empathy_contact_group_set_expanded (empathy_roster_group_get_name (group), + gtk_expander_get_expanded (group->expander)); } static EmpathyRosterGroup * @@ -167,7 +225,7 @@ lookup_roster_group (EmpathyRosterView *self, return g_hash_table_lookup (self->priv->roster_groups, group); } -static void +static EmpathyRosterGroup * ensure_roster_group (EmpathyRosterView *self, const gchar *group) { @@ -175,18 +233,98 @@ ensure_roster_group (EmpathyRosterView *self, roster_group = (GtkWidget *) lookup_roster_group (self, group); if (roster_group != NULL) - return; + return EMPATHY_ROSTER_GROUP (roster_group); - roster_group = empathy_roster_group_new (group); + if (!tp_strdiff (group, EMPATHY_ROSTER_MODEL_GROUP_TOP_GROUP)) + roster_group = empathy_roster_group_new (group, "emblem-favorite-symbolic"); + else if (!tp_strdiff (group, EMPATHY_ROSTER_MODEL_GROUP_PEOPLE_NEARBY)) + roster_group = empathy_roster_group_new (group, "im-local-xmpp"); + else + roster_group = empathy_roster_group_new (group, NULL); + + gtk_expander_set_expanded (EMPATHY_ROSTER_GROUP (roster_group)->expander, + empathy_contact_group_get_expanded (group)); - g_signal_connect (roster_group, "notify::expanded", - G_CALLBACK (group_expanded_cb), self); + g_signal_connect (EMPATHY_ROSTER_GROUP (roster_group)->expander, + "notify::expanded", G_CALLBACK (group_expanded_cb), roster_group); gtk_widget_show (roster_group); gtk_container_add (GTK_CONTAINER (self), roster_group); g_hash_table_insert (self->priv->roster_groups, g_strdup (group), roster_group); + + return EMPATHY_ROSTER_GROUP (roster_group); +} + +static void +update_empty (EmpathyRosterView *self, + gboolean empty) +{ + if (self->priv->empty == empty) + return; + + self->priv->empty = empty; + g_object_notify (G_OBJECT (self), "empty"); +} + +static gboolean filter_group (EmpathyRosterView *self, + EmpathyRosterGroup *group); + +static gboolean +at_least_one_group_displayed (EmpathyRosterView *self) +{ + GHashTableIter iter; + gpointer v; + + g_hash_table_iter_init (&iter, self->priv->roster_groups); + while (g_hash_table_iter_next (&iter, NULL, &v)) + { + EmpathyRosterGroup *group = EMPATHY_ROSTER_GROUP (v); + + if (filter_group (self, group)) + return TRUE; + } + + return FALSE; +} + +static void +check_if_empty (EmpathyRosterView *self) +{ + /* Roster is considered as empty if there is no contact *and* no group + * currently displayed. */ + if (g_hash_table_size (self->priv->displayed_contacts) != 0 || + at_least_one_group_displayed (self)) + { + update_empty (self, FALSE); + return; + } + + update_empty (self, TRUE); +} + +static void +update_group_widgets (EmpathyRosterView *self, + EmpathyRosterGroup *group, + EmpathyRosterContact *contact, + gboolean add) +{ + guint old_count, count; + + old_count = empathy_roster_group_get_widgets_count (group); + + if (add) + count = empathy_roster_group_add_widget (group, GTK_WIDGET (contact)); + else + count = empathy_roster_group_remove_widget (group, GTK_WIDGET (contact)); + + if (count != old_count) + { + gtk_list_box_row_changed (GTK_LIST_BOX_ROW (group)); + + check_if_empty (self); + } } static void @@ -196,16 +334,60 @@ add_to_group (EmpathyRosterView *self, { GtkWidget *contact; GHashTable *contacts; + EmpathyRosterGroup *roster_group = NULL; contacts = g_hash_table_lookup (self->priv->roster_contacts, individual); if (contacts == NULL) return; + if (g_hash_table_lookup (contacts, group) != NULL) + return; + if (tp_strdiff (group, NO_GROUP)) - ensure_roster_group (self, group); + roster_group = ensure_roster_group (self, group); contact = add_roster_contact (self, individual, group); g_hash_table_insert (contacts, g_strdup (group), contact); + + if (roster_group != NULL) + { + update_group_widgets (self, roster_group, + EMPATHY_ROSTER_CONTACT (contact), TRUE); + } + + if (tp_strdiff (group, NO_GROUP) && + tp_strdiff (group, EMPATHY_ROSTER_MODEL_GROUP_UNGROUPED) && + g_hash_table_size (contacts) == 2 /* 1:Ungrouped and 2:first group */) + { + remove_from_group (self, individual, + EMPATHY_ROSTER_MODEL_GROUP_UNGROUPED); + } +} + +static void +individual_favourite_change_cb (FolksIndividual *individual, + GParamSpec *spec, + EmpathyRosterView *self) +{ + /* We may have to refilter the contact as only favorite contacts are always + * displayed regardless of their presence. */ + GHashTable *contacts; + GtkWidget *contact; + + contacts = g_hash_table_lookup (self->priv->roster_contacts, individual); + if (contacts == NULL) + return; + + if (self->priv->show_groups) + contact = g_hash_table_lookup (contacts, + EMPATHY_ROSTER_MODEL_GROUP_TOP_GROUP); + else + contact = g_hash_table_lookup (contacts, NO_GROUP); + + if (contact == NULL) + return; + + gtk_list_box_row_changed (GTK_LIST_BOX_ROW (contact)); } static void @@ -228,54 +410,136 @@ individual_added (EmpathyRosterView *self, } else { - GeeSet *groups; + GList *groups, *l; - groups = folks_group_details_get_groups ( - FOLKS_GROUP_DETAILS (individual)); + groups = empathy_roster_model_dup_groups_for_individual (self->priv->model, + individual); - if (gee_collection_get_size (GEE_COLLECTION (groups)) > 0) + if (g_list_length (groups) > 0) { - GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (groups)); - - while (iter != NULL && gee_iterator_next (iter)) + for (l = groups; l != NULL; l = g_list_next (l)) { - gchar *group = gee_iterator_get (iter); - - add_to_group (self, individual, group); - - g_free (group); + add_to_group (self, individual, l->data); } - - g_clear_object (&iter); } else { - /* No group, adds to Ungroupped */ - add_to_group (self, individual, UNGROUPPED); + /* No group, adds to Ungrouped */ + add_to_group (self, individual, EMPATHY_ROSTER_MODEL_GROUP_UNGROUPED); } + + g_list_free_full (groups, g_free); } + + tp_g_signal_connect_object (individual, "notify::is-favourite", + G_CALLBACK (individual_favourite_change_cb), self, 0); } static void -update_group_widgets_count (EmpathyRosterView *self, - EmpathyRosterGroup *group, - EmpathyRosterContact *contact, - gboolean displayed) +set_event_icon_on_individual (EmpathyRosterView *self, + FolksIndividual *individual, + const gchar *icon) { - if (displayed) + GHashTable *contacts; + GHashTableIter iter; + gpointer v; + + contacts = g_hash_table_lookup (self->priv->roster_contacts, individual); + if (contacts == NULL) + return; + + g_hash_table_iter_init (&iter, contacts); + while (g_hash_table_iter_next (&iter, NULL, &v)) { - if (empathy_roster_group_add_widget (group, GTK_WIDGET (contact)) == 1) - { - egg_list_box_child_changed (EGG_LIST_BOX (self), - GTK_WIDGET (group)); - } + EmpathyRosterContact *contact =v; + + empathy_roster_contact_set_event_icon (contact, icon); + } +} + +static void +flash_event (Event *event, + EmpathyRosterView *self) +{ + set_event_icon_on_individual (self, event->individual, event->icon); +} + +static void +unflash_event (Event *event, + EmpathyRosterView *self) +{ + set_event_icon_on_individual (self, event->individual, NULL); +} + +static gboolean +flash_cb (gpointer data) +{ + EmpathyRosterView *self = data; + + if (self->priv->display_flash_event) + { + g_queue_foreach (self->priv->events, (GFunc) flash_event, self); + self->priv->display_flash_event = FALSE; } else { - if (empathy_roster_group_remove_widget (group, GTK_WIDGET (contact)) == 0) + g_queue_foreach (self->priv->events, (GFunc) unflash_event, self); + self->priv->display_flash_event = TRUE; + } + + return TRUE; +} + +static void +start_flashing (EmpathyRosterView *self) +{ + if (self->priv->flash_id != 0) + return; + + self->priv->display_flash_event = TRUE; + + self->priv->flash_id = g_timeout_add (FLASH_TIMEOUT, + flash_cb, self); +} + +static void +stop_flashing (EmpathyRosterView *self) +{ + if (self->priv->flash_id == 0) + return; + + g_source_remove (self->priv->flash_id); + self->priv->flash_id = 0; +} + +static void +remove_event (EmpathyRosterView *self, + Event *event) +{ + unflash_event (event, self); + g_queue_remove (self->priv->events, event); + + if (g_queue_get_length (self->priv->events) == 0) + { + stop_flashing (self); + } +} + +static void +remove_all_individual_event (EmpathyRosterView *self, + FolksIndividual *individual) +{ + GList *l; + + for (l = g_queue_peek_head_link (self->priv->events); l != NULL; + l = g_list_next (l)) + { + Event *event = l->data; + + if (event->individual == individual) { - egg_list_box_child_changed (EGG_LIST_BOX (self), - GTK_WIDGET (group)); + remove_event (self, event); + return; } } } @@ -292,6 +556,8 @@ individual_removed (EmpathyRosterView *self, if (contacts == NULL) return; + remove_all_individual_event (self, individual); + g_hash_table_iter_init (&iter, contacts); while (g_hash_table_iter_next (&iter, &key, &value)) { @@ -302,7 +568,7 @@ individual_removed (EmpathyRosterView *self, group = lookup_roster_group (self, group_name); if (group != NULL) { - update_group_widgets_count (self, group, + update_group_widgets (self, group, EMPATHY_ROSTER_CONTACT (contact), FALSE); } @@ -313,73 +579,84 @@ individual_removed (EmpathyRosterView *self, } static void -members_changed_cb (EmpathyIndividualManager *manager, - const gchar *message, - GList *added, - GList *removed, - TpChannelGroupChangeReason reason, +individual_added_cb (EmpathyRosterModel *model, + FolksIndividual *individual, + EmpathyRosterView *self) +{ + individual_added (self, individual); +} + +static void +individual_removed_cb (EmpathyRosterModel *model, + FolksIndividual *individual, EmpathyRosterView *self) { - GList *l; + individual_removed (self, individual); +} - for (l = added; l != NULL; l = g_list_next (l)) +static gboolean +contact_in_top (EmpathyRosterView *self, + EmpathyRosterContact *contact) +{ + if (!self->priv->show_groups) { - FolksIndividual *individual = l->data; + /* Always display top contacts in non-group mode. */ + GList *groups; + FolksIndividual *individual; + gboolean result = FALSE; - individual_added (self, individual); - } + individual = empathy_roster_contact_get_individual (contact); - for (l = removed; l != NULL; l = g_list_next (l)) - { - FolksIndividual *individual = l->data; + groups = empathy_roster_model_dup_groups_for_individual ( + self->priv->model, individual); - individual_removed (self, individual); + if (g_list_find_custom (groups, EMPATHY_ROSTER_MODEL_GROUP_TOP_GROUP, + (GCompareFunc) g_strcmp0) != NULL) + result = TRUE; + + g_list_free_full (groups, g_free); + + return result; } + + if (!tp_strdiff (empathy_roster_contact_get_group (contact), + EMPATHY_ROSTER_MODEL_GROUP_TOP_GROUP)) + /* If we are displaying contacts, we only want to *always* display the + * RosterContact which is displayed at the top; not the ones displayed in + * the 'normal' group sections */ + return TRUE; + + return FALSE; } static gint -compare_roster_contacts_by_alias (EmpathyRosterContact *a, +compare_roster_contacts_by_conversation_time (EmpathyRosterContact *a, EmpathyRosterContact *b) { - FolksIndividual *ind_a, *ind_b; - const gchar *alias_a, *alias_b; + gint64 ts_a, ts_b; - ind_a = empathy_roster_contact_get_individual (a); - ind_b = empathy_roster_contact_get_individual (b); + ts_a = empathy_roster_contact_get_most_recent_timestamp (a); + ts_b = empathy_roster_contact_get_most_recent_timestamp (b); - alias_a = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_a)); - alias_b = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_b)); - - return g_ascii_strcasecmp (alias_a, alias_b); + if (ts_a == ts_b) return 0; + if (ts_a > ts_b) return -1; + return 1; } static gint -compare_individual_top_position (EmpathyRosterView *self, - EmpathyRosterContact *a, +compare_roster_contacts_by_alias (EmpathyRosterContact *a, EmpathyRosterContact *b) { FolksIndividual *ind_a, *ind_b; - GList *tops; - gint index_a, index_b; + const gchar *alias_a, *alias_b; ind_a = empathy_roster_contact_get_individual (a); ind_b = empathy_roster_contact_get_individual (b); - tops = empathy_individual_manager_get_top_individuals (self->priv->manager); - - index_a = g_list_index (tops, ind_a); - index_b = g_list_index (tops, ind_b); - - if (index_a == index_b) - return 0; - - if (index_a == -1) - return 1; - - if (index_b == -1) - return -1; + alias_a = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_a)); + alias_b = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_b)); - return index_a - index_b; + return g_utf8_collate (alias_a, alias_b); } static gint @@ -387,26 +664,37 @@ compare_roster_contacts_no_group (EmpathyRosterView *self, EmpathyRosterContact *a, EmpathyRosterContact *b) { - gint top; + gboolean top_a, top_b; - top = compare_individual_top_position (self, a, b); - if (top != 0) - return top; + top_a = contact_in_top (self, a); + top_b = contact_in_top (self, b); - return compare_roster_contacts_by_alias (a, b); + if (top_a == top_b) + /* Both contacts are in the top of the roster (or not). Sort them + * alphabetically */ + return compare_roster_contacts_by_conversation_time (a, b); + else if (top_a) + return -1; + else + return 1; } static gint compare_group_names (const gchar *group_a, const gchar *group_b) { - if (!tp_strdiff (group_a, TOP_GROUP)) + if (!tp_strdiff (group_a, EMPATHY_ROSTER_MODEL_GROUP_TOP_GROUP)) return -1; - if (!tp_strdiff (group_b, TOP_GROUP)) + if (!tp_strdiff (group_b, EMPATHY_ROSTER_MODEL_GROUP_TOP_GROUP)) return 1; - return g_ascii_strcasecmp (group_a, group_b); + if (!tp_strdiff (group_a, EMPATHY_ROSTER_MODEL_GROUP_UNGROUPED)) + return 1; + else if (!tp_strdiff (group_b, EMPATHY_ROSTER_MODEL_GROUP_UNGROUPED)) + return -1; + + return g_utf8_collate (group_a, group_b); } static gint @@ -421,7 +709,7 @@ compare_roster_contacts_with_groups (EmpathyRosterView *self, if (!tp_strdiff (group_a, group_b)) /* Same group, compare the contacts */ - return compare_roster_contacts_by_alias (a, b); + return compare_roster_contacts_by_conversation_time (a, b); /* Sort by group */ return compare_group_names (group_a, group_b); @@ -468,8 +756,8 @@ compare_contact_group (EmpathyRosterContact *contact, } static gint -roster_view_sort (gconstpointer a, - gconstpointer b, +roster_view_sort (GtkListBoxRow *a, + GtkListBoxRow *b, gpointer user_data) { EmpathyRosterView *self = user_data; @@ -491,23 +779,22 @@ roster_view_sort (gconstpointer a, } static void -update_separator (GtkWidget **separator, - GtkWidget *child, - GtkWidget *before, +update_header (GtkListBoxRow *row, + GtkListBoxRow *before, gpointer user_data) { if (before == NULL) { /* No separator before the first row */ - g_clear_object (separator); + gtk_list_box_row_set_header (row, NULL); return; } - if (*separator != NULL) + if (gtk_list_box_row_get_header (row) != NULL) return; - *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); - g_object_ref_sink (*separator); + gtk_list_box_row_set_header (row, + gtk_separator_new (GTK_ORIENTATION_HORIZONTAL)); } static gboolean @@ -519,23 +806,44 @@ is_searching (EmpathyRosterView *self) return gtk_widget_get_visible (GTK_WIDGET (self->priv->search)); } -static void -update_empty (EmpathyRosterView *self, - gboolean empty) -{ - if (self->priv->empty == empty) - return; - - self->priv->empty = empty; - g_object_notify (G_OBJECT (self), "empty"); -} - static void add_to_displayed (EmpathyRosterView *self, EmpathyRosterContact *contact) { + FolksIndividual *individual; + GHashTable *contacts; + GHashTableIter iter; + gpointer k; + + if (g_hash_table_lookup (self->priv->displayed_contacts, contact) != NULL) + return; + g_hash_table_add (self->priv->displayed_contacts, contact); update_empty (self, FALSE); + + /* Groups of this contact may now be displayed if we just displays the first + * child in this group. */ + + if (!self->priv->show_groups) + return; + + individual = empathy_roster_contact_get_individual (contact); + contacts = g_hash_table_lookup (self->priv->roster_contacts, individual); + if (contacts == NULL) + return; + + g_hash_table_iter_init (&iter, contacts); + while (g_hash_table_iter_next (&iter, &k, NULL)) + { + const gchar *group_name = k; + GtkListBoxRow *group; + + group = g_hash_table_lookup (self->priv->roster_groups, group_name); + if (group == NULL) + continue; + + gtk_list_box_row_changed (group); + } } static void @@ -544,38 +852,59 @@ remove_from_displayed (EmpathyRosterView *self, { g_hash_table_remove (self->priv->displayed_contacts, contact); - if (g_hash_table_size (self->priv->displayed_contacts) == 0) - update_empty (self, TRUE); + check_if_empty (self); } static gboolean -filter_contact (EmpathyRosterView *self, - EmpathyRosterContact *contact) +contact_is_favorite (EmpathyRosterContact *contact) { - gboolean displayed; + FolksIndividual *individual; + + individual = empathy_roster_contact_get_individual (contact); + return folks_favourite_details_get_is_favourite ( + FOLKS_FAVOURITE_DETAILS (individual)); +} + +/** + * check if @contact should be displayed according to @self's current status + * and without consideration for the state of @contact's groups. + */ +static gboolean +contact_should_be_displayed (EmpathyRosterView *self, + EmpathyRosterContact *contact) +{ if (is_searching (self)) { FolksIndividual *individual; individual = empathy_roster_contact_get_individual (contact); - displayed = empathy_individual_match_string (individual, - empathy_live_search_get_text (self->priv->search), - empathy_live_search_get_words (self->priv->search)); - } - else - { - if (self->priv->show_offline) - { - displayed = TRUE; - } - else - { - displayed = empathy_roster_contact_is_online (contact); - } + return empathy_individual_match_string (individual, + tpaw_live_search_get_text (self->priv->search), + tpaw_live_search_get_words (self->priv->search)); } + if (self->priv->show_offline) + return TRUE; + + if (contact_in_top (self, contact) && + contact_is_favorite (contact)) + /* Favorite top contacts are always displayed */ + return TRUE; + + return empathy_roster_contact_is_online (contact); +} + + +static gboolean +filter_contact (EmpathyRosterView *self, + EmpathyRosterContact *contact) +{ + gboolean displayed; + + displayed = contact_should_be_displayed (self, contact); + if (self->priv->show_groups) { const gchar *group_name; @@ -586,11 +915,9 @@ filter_contact (EmpathyRosterView *self, if (group != NULL) { - update_group_widgets_count (self, group, contact, displayed); - /* When searching, always display even if the group is closed */ if (!is_searching (self) && - !gtk_expander_get_expanded (GTK_EXPANDER (group))) + !gtk_expander_get_expanded (group->expander)) displayed = FALSE; } } @@ -611,11 +938,29 @@ static gboolean filter_group (EmpathyRosterView *self, EmpathyRosterGroup *group) { - return empathy_roster_group_get_widgets_count (group); + GList *widgets, *l; + gboolean result = FALSE; + + /* Display the group if it contains at least one displayed contact */ + widgets = empathy_roster_group_get_widgets (group); + for (l = widgets; l != NULL; l = g_list_next (l)) + { + EmpathyRosterContact *contact = l->data; + + if (contact_should_be_displayed (self, contact)) + { + result = TRUE; + break; + } + } + + g_list_free (widgets); + + return result; } static gboolean -filter_list (GtkWidget *child, +filter_list (GtkListBoxRow *child, gpointer user_data) { EmpathyRosterView *self = user_data; @@ -629,33 +974,12 @@ filter_list (GtkWidget *child, g_return_val_if_reached (FALSE); } -/* @list: GList of EmpathyRosterContact - * - * Returns: %TRUE if @list contains an EmpathyRosterContact associated with - * @individual */ -static gboolean -individual_in_list (FolksIndividual *individual, - GList *list) -{ - GList *l; - - for (l = list; l != NULL; l = g_list_next (l)) - { - EmpathyRosterContact *contact = l->data; - - if (empathy_roster_contact_get_individual (contact) == individual) - return TRUE; - } - - return FALSE; -} - static void populate_view (EmpathyRosterView *self) { GList *individuals, *l; - individuals = empathy_individual_manager_get_members (self->priv->manager); + individuals = empathy_roster_model_get_individuals (self->priv->model); for (l = individuals; l != NULL; l = g_list_next (l)) { FolksIndividual *individual = l->data; @@ -687,14 +1011,14 @@ remove_from_group (EmpathyRosterView *self, if (g_hash_table_size (contacts) == 0) { - add_to_group (self, individual, UNGROUPPED); + add_to_group (self, individual, EMPATHY_ROSTER_MODEL_GROUP_UNGROUPED); } roster_group = lookup_roster_group (self, group); if (roster_group != NULL) { - update_group_widgets_count (self, roster_group, + update_group_widgets (self, roster_group, EMPATHY_ROSTER_CONTACT (contact), FALSE); } @@ -702,72 +1026,17 @@ remove_from_group (EmpathyRosterView *self, } static void -update_top_contacts (EmpathyRosterView *self) -{ - GList *tops, *l; - GList *to_add = NULL, *to_remove = NULL; - EmpathyRosterGroup *group; - - if (!self->priv->show_groups) - { - egg_list_box_resort (EGG_LIST_BOX (self)); - return; - } - - tops = empathy_individual_manager_get_top_individuals (self->priv->manager); - - group = g_hash_table_lookup (self->priv->roster_groups, TOP_GROUP); - if (group == NULL) - { - to_add = g_list_copy (tops); - } - else - { - GList *contacts; - - contacts = empathy_roster_group_get_widgets (group); - - /* Check which EmpathyRosterContact have to be removed */ - for (l = contacts; l != NULL; l = g_list_next (l)) - { - EmpathyRosterContact *contact = l->data; - FolksIndividual *individual; - - individual = empathy_roster_contact_get_individual (contact); - - if (g_list_find (tops, individual) == NULL) - to_remove = g_list_prepend (to_remove, individual); - } - - /* Check which EmpathyRosterContact have to be added */ - for (l = tops; l != NULL; l = g_list_next (l)) - { - FolksIndividual *individual = l->data; - - if (!individual_in_list (individual, contacts)) - to_add = g_list_prepend (to_add, individual); - } - } - - for (l = to_add; l != NULL; l = g_list_next (l)) - add_to_group (self, l->data, TOP_GROUP); - - for (l = to_remove; l != NULL; l = g_list_next (l)) - remove_from_group (self, l->data, TOP_GROUP); - - g_list_free (to_add); - g_list_free (to_remove); -} - -static void -groups_changed_cb (EmpathyIndividualManager *manager, +groups_changed_cb (EmpathyRosterModel *model, FolksIndividual *individual, - gchar *group, + const gchar *group, gboolean is_member, EmpathyRosterView *self) { if (!self->priv->show_groups) - return; + { + gtk_list_box_invalidate_sort (GTK_LIST_BOX (self)); + return; + } if (is_member) { @@ -779,14 +1048,6 @@ groups_changed_cb (EmpathyIndividualManager *manager, } } -static void -top_individuals_changed_cb (EmpathyIndividualManager *manager, - GParamSpec *spec, - EmpathyRosterView *self) -{ - update_top_contacts (self); -} - static void empathy_roster_view_constructed (GObject *object) { @@ -797,26 +1058,39 @@ empathy_roster_view_constructed (GObject *object) if (chain_up != NULL) chain_up (object); - g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager)); + g_assert (EMPATHY_IS_ROSTER_MODEL (self->priv->model)); + + /* Get saved group states. */ + empathy_contact_groups_get_all (); populate_view (self); - tp_g_signal_connect_object (self->priv->manager, "members-changed", - G_CALLBACK (members_changed_cb), self, 0); - tp_g_signal_connect_object (self->priv->manager, "groups-changed", + tp_g_signal_connect_object (self->priv->model, "individual-added", + G_CALLBACK (individual_added_cb), self, 0); + tp_g_signal_connect_object (self->priv->model, "individual-removed", + G_CALLBACK (individual_removed_cb), self, 0); + tp_g_signal_connect_object (self->priv->model, "groups-changed", G_CALLBACK (groups_changed_cb), self, 0); - tp_g_signal_connect_object (self->priv->manager, "notify::top-individuals", - G_CALLBACK (top_individuals_changed_cb), self, 0); - egg_list_box_set_sort_func (EGG_LIST_BOX (self), + gtk_list_box_set_sort_func (GTK_LIST_BOX (self), roster_view_sort, self, NULL); - egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator, - self, NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (self), update_header, self, NULL); - egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL); + gtk_list_box_set_filter_func (GTK_LIST_BOX (self), filter_list, self, NULL); - egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE); + gtk_list_box_set_activate_on_single_click (GTK_LIST_BOX (self), FALSE); +} + +static void +clear_view (EmpathyRosterView *self) +{ + g_hash_table_remove_all (self->priv->roster_contacts); + g_hash_table_remove_all (self->priv->roster_groups); + g_hash_table_remove_all (self->priv->displayed_contacts); + + gtk_container_foreach (GTK_CONTAINER (self), + (GtkCallback) gtk_widget_destroy, NULL); } static void @@ -826,8 +1100,20 @@ empathy_roster_view_dispose (GObject *object) void (*chain_up) (GObject *) = ((GObjectClass *) empathy_roster_view_parent_class)->dispose; + /* Start by clearing the view so our internal hash tables are cleared from + * objects being destroyed. */ + clear_view (self); + + stop_flashing (self); + empathy_roster_view_set_live_search (self, NULL); - g_clear_object (&self->priv->manager); + g_clear_object (&self->priv->model); + + if (self->priv->search_id != 0) + { + g_source_remove (self->priv->search_id); + self->priv->search_id = 0; + } if (chain_up != NULL) chain_up (object); @@ -843,44 +1129,63 @@ empathy_roster_view_finalize (GObject *object) g_hash_table_unref (self->priv->roster_contacts); g_hash_table_unref (self->priv->roster_groups); g_hash_table_unref (self->priv->displayed_contacts); + g_queue_free_full (self->priv->events, event_free); if (chain_up != NULL) chain_up (object); } static void -empathy_roster_view_child_activated (EggListBox *box, - GtkWidget *child) +empathy_roster_view_row_activated (GtkListBox *box, + GtkListBoxRow *row) { + EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (box); EmpathyRosterContact *contact; FolksIndividual *individual; + GList *l; - if (!EMPATHY_IS_ROSTER_CONTACT (child)) + if (!EMPATHY_IS_ROSTER_CONTACT (row)) return; - contact = EMPATHY_ROSTER_CONTACT (child); + contact = EMPATHY_ROSTER_CONTACT (row); individual = empathy_roster_contact_get_individual (contact); + /* Activate the oldest event associated with this contact, if any */ + for (l = g_queue_peek_tail_link (self->priv->events); l != NULL; + l = g_list_previous (l)) + { + Event *event = l->data; + + if (event->individual == individual) + { + g_signal_emit (box, signals[SIG_EVENT_ACTIVATED], 0, individual, + event->user_data); + return; + } + } + g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual); } static void fire_popup_individual_menu (EmpathyRosterView *self, - GtkWidget *child, + GtkListBoxRow *row, guint button, guint time) { EmpathyRosterContact *contact; FolksIndividual *individual; + const gchar *active_group; - if (!EMPATHY_IS_ROSTER_CONTACT (child)) + if (!EMPATHY_IS_ROSTER_CONTACT (row)) return; - contact = EMPATHY_ROSTER_CONTACT (child); + contact = EMPATHY_ROSTER_CONTACT (row); individual = empathy_roster_contact_get_individual (contact); + active_group = empathy_roster_contact_get_group (contact); g_signal_emit (self, signals[SIG_POPUP_INDIVIDUAL_MENU], 0, - individual, button, time); + active_group, individual, button, time); } static gboolean @@ -893,12 +1198,16 @@ empathy_roster_view_button_press_event (GtkWidget *widget, if (event->button == 3) { - GtkWidget *child; + GtkListBoxRow *row; - child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), event->y); + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (self), event->y); - if (child != NULL) - fire_popup_individual_menu (self, child, event->button, event->time); + if (row != NULL) + { + gtk_list_box_select_row (GTK_LIST_BOX (self), row); + + fire_popup_individual_menu (self, row, event->button, event->time); + } } return chain_up (widget, event); @@ -914,17 +1223,54 @@ empathy_roster_view_key_press_event (GtkWidget *widget, if (event->keyval == GDK_KEY_Menu) { - GtkWidget *child; + GtkListBoxRow *row; - child = egg_list_box_get_selected_child (EGG_LIST_BOX (self)); + row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self)); - if (child != NULL) - fire_popup_individual_menu (self, child, 0, event->time); + if (row != NULL) + fire_popup_individual_menu (self, row, 0, event->time); } return chain_up (widget, event); } +/** + * @out_row: (out) (allow-none) + */ +FolksIndividual * +empathy_roster_view_get_individual_at_y (EmpathyRosterView *self, + gint y, + GtkListBoxRow **out_row) +{ + GtkListBoxRow *row; + + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (self), y); + + if (out_row != NULL) + *out_row = row; + + if (!EMPATHY_IS_ROSTER_CONTACT (row)) + return NULL; + + return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (row)); +} + +const gchar * +empathy_roster_view_get_group_at_y (EmpathyRosterView *self, + gint y) +{ + GtkListBoxRow *row; + + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (self), y); + + if (EMPATHY_IS_ROSTER_CONTACT (row)) + return empathy_roster_contact_get_group (EMPATHY_ROSTER_CONTACT (row)); + else if (EMPATHY_IS_ROSTER_GROUP (row)) + return empathy_roster_group_get_name (EMPATHY_ROSTER_GROUP (row)); + + return NULL; +} + static gboolean empathy_roster_view_query_tooltip (GtkWidget *widget, gint x, @@ -933,33 +1279,26 @@ empathy_roster_view_query_tooltip (GtkWidget *widget, GtkTooltip *tooltip) { EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget); - GtkWidget *child; - EmpathyRosterContact *contact; FolksIndividual *individual; + gboolean result; + GtkListBoxRow *row; - if (self->priv->individual_tooltip_cb == NULL) + individual = empathy_roster_view_get_individual_at_y (self, y, &row); + if (individual == NULL) return FALSE; - child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y); - if (!EMPATHY_IS_ROSTER_CONTACT (child)) - return FALSE; + g_signal_emit (self, signals[SIG_INDIVIDUAL_TOOLTIP], 0, + individual, keyboard_mode, tooltip, &result); - contact = EMPATHY_ROSTER_CONTACT (child); - individual = empathy_roster_contact_get_individual (contact); - - return self->priv->individual_tooltip_cb (self, individual, keyboard_mode, - tooltip, self->priv->individual_tooltip_data); -} + if (result) + { + GtkAllocation allocation; -void -empathy_roster_view_set_individual_tooltip_cb (EmpathyRosterView *self, - EmpathyRosterViewIndividualTooltipCb callback, - gpointer user_data) -{ - self->priv->individual_tooltip_cb = callback; - self->priv->individual_tooltip_data = user_data; + gtk_widget_get_allocation (GTK_WIDGET (row), &allocation); + gtk_tooltip_set_tip_area (tooltip, (GdkRectangle *) &allocation); + } - gtk_widget_set_has_tooltip (GTK_WIDGET (self), callback != NULL); + return result; } static void @@ -981,7 +1320,7 @@ empathy_roster_view_class_init ( EmpathyRosterViewClass *klass) { GObjectClass *oclass = G_OBJECT_CLASS (klass); - EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass); + GtkListBoxClass *box_class = GTK_LIST_BOX_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); GParamSpec *spec; @@ -998,13 +1337,13 @@ empathy_roster_view_class_init ( container_class->remove = empathy_roster_view_remove; - box_class->child_activated = empathy_roster_view_child_activated; + box_class->row_activated = empathy_roster_view_row_activated; - spec = g_param_spec_object ("manager", "Manager", - "EmpathyIndividualManager", - EMPATHY_TYPE_INDIVIDUAL_MANAGER, + spec = g_param_spec_object ("model", "Model", + "EmpathyRosterModel", + EMPATHY_TYPE_ROSTER_MODEL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); - g_object_class_install_property (oclass, PROP_MANAGER, spec); + g_object_class_install_property (oclass, PROP_MODEL, spec); spec = g_param_spec_boolean ("show-offline", "Show Offline", "Show offline contacts", @@ -1036,7 +1375,22 @@ empathy_roster_view_class_init ( G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, - 3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT, G_TYPE_UINT); + 4, G_TYPE_STRING, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT, + G_TYPE_UINT); + + signals[SIG_EVENT_ACTIVATED] = g_signal_new ("event-activated", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_POINTER); + + signals[SIG_INDIVIDUAL_TOOLTIP] = g_signal_new ("individual-tooltip", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, g_signal_accumulator_true_handled, NULL, NULL, + G_TYPE_BOOLEAN, + 3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_BOOLEAN, GTK_TYPE_TOOLTIP); g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv)); } @@ -1053,25 +1407,21 @@ empathy_roster_view_init (EmpathyRosterView *self) g_free, NULL); self->priv->displayed_contacts = g_hash_table_new (NULL, NULL); + self->priv->events = g_queue_new (); + self->priv->empty = TRUE; } GtkWidget * -empathy_roster_view_new (EmpathyIndividualManager *manager) +empathy_roster_view_new (EmpathyRosterModel *model) { - g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL); + g_return_val_if_fail (EMPATHY_IS_ROSTER_MODEL (model), NULL); return g_object_new (EMPATHY_TYPE_ROSTER_VIEW, - "manager", manager, + "model", model, NULL); } -EmpathyIndividualManager * -empathy_roster_view_get_manager (EmpathyRosterView *self) -{ - return self->priv->manager; -} - void empathy_roster_view_show_offline (EmpathyRosterView *self, gboolean show) @@ -1080,20 +1430,11 @@ empathy_roster_view_show_offline (EmpathyRosterView *self, return; self->priv->show_offline = show; - egg_list_box_refilter (EGG_LIST_BOX (self)); + gtk_list_box_invalidate_filter (GTK_LIST_BOX (self)); g_object_notify (G_OBJECT (self), "show-offline"); } -static void -clear_view (EmpathyRosterView *self) -{ - gtk_container_foreach (GTK_CONTAINER (self), - (GtkCallback) gtk_widget_destroy, NULL); - - g_hash_table_remove_all (self->priv->roster_contacts); -} - void empathy_roster_view_show_groups (EmpathyRosterView *self, gboolean show) @@ -1126,40 +1467,53 @@ select_first_contact (EmpathyRosterView *self) if (!EMPATHY_IS_ROSTER_CONTACT (child)) continue; - egg_list_box_select_child (EGG_LIST_BOX (self), child); + gtk_list_box_select_row (GTK_LIST_BOX (self), GTK_LIST_BOX_ROW (child)); break; } g_list_free (children); } +static gboolean +search_timeout_cb (EmpathyRosterView *self) +{ + gtk_list_box_invalidate_filter (GTK_LIST_BOX (self)); + + select_first_contact (self); + + self->priv->search_id = 0; + return G_SOURCE_REMOVE; +} + static void -search_text_notify_cb (EmpathyLiveSearch *search, +search_text_notify_cb (TpawLiveSearch *search, GParamSpec *pspec, EmpathyRosterView *self) { - egg_list_box_refilter (EGG_LIST_BOX (self)); + if (self->priv->search_id != 0) + g_source_remove (self->priv->search_id); - select_first_contact (self); + self->priv->search_id = g_timeout_add (SEARCH_TIMEOUT, + (GSourceFunc) search_timeout_cb, self); } static void search_activate_cb (GtkWidget *search, EmpathyRosterView *self) { - EggListBox *box = EGG_LIST_BOX (self); - GtkWidget *child; + GtkListBox *box = GTK_LIST_BOX (self); + GtkListBoxRow *row; - child = egg_list_box_get_selected_child (box); - if (child == NULL) + row = gtk_list_box_get_selected_row (box); + if (row == NULL) return; - empathy_roster_view_child_activated (box, child); + empathy_roster_view_row_activated (box, row); } void empathy_roster_view_set_live_search (EmpathyRosterView *self, - EmpathyLiveSearch *search) + TpawLiveSearch *search) { if (self->priv->search != NULL) { @@ -1187,3 +1541,66 @@ empathy_roster_view_is_empty (EmpathyRosterView *self) { return self->priv->empty; } + +gboolean +empathy_roster_view_is_searching (EmpathyRosterView *self) +{ + return (self->priv->search != NULL && + gtk_widget_get_visible (GTK_WIDGET (self->priv->search))); +} + +/* Don't use EmpathyEvent as I prefer to keep this object not too specific to + * Empathy's internals. */ +guint +empathy_roster_view_add_event (EmpathyRosterView *self, + FolksIndividual *individual, + const gchar *icon, + gpointer user_data) +{ + GHashTable *contacts; + + contacts = g_hash_table_lookup (self->priv->roster_contacts, individual); + if (contacts == NULL) + return 0; + + self->priv->last_event_id++; + + g_queue_push_head (self->priv->events, + event_new (self->priv->last_event_id, individual, icon, user_data)); + + start_flashing (self); + + return self->priv->last_event_id; +} + +void +empathy_roster_view_remove_event (EmpathyRosterView *self, + guint event_id) +{ + GList *l; + + for (l = g_queue_peek_head_link (self->priv->events); l != NULL; + l = g_list_next (l)) + { + Event *event = l->data; + + if (event->id == event_id) + { + remove_event (self, event); + return; + } + } +} + +FolksIndividual * +empathy_roster_view_get_selected_individual (EmpathyRosterView *self) +{ + GtkListBoxRow *row; + + row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self)); + + if (!EMPATHY_IS_ROSTER_CONTACT (row)) + return NULL; + + return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (row)); +}