Tue, 07 Jan 2025 04:49:09 -0600
Make sure we notify on the n-items property for all objects that have it
Testing Done:
Called in the turtles.
Reviewed at https://reviews.imfreedom.org/r/3736/
/* * Purple - Internet Messaging Library * Copyright (C) Pidgin Developers <devel@pidgin.im> * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this library; if not, see <https://www.gnu.org/licenses/>. */ #include "purpleperson.h" #include "util.h" #include "util.h" struct _PurplePerson { GObject parent; char *id; gchar *alias; PurpleAvatar *avatar; char *color; PurpleTags *tags; GPtrArray *contacts; }; enum { PROP_0, PROP_ITEM_TYPE, PROP_N_ITEMS, PROP_ID, PROP_ALIAS, PROP_AVATAR, PROP_AVATAR_FOR_DISPLAY, PROP_COLOR, PROP_COLOR_FOR_DISPLAY, PROP_TAGS, PROP_NAME_FOR_DISPLAY, PROP_PRIORITY_CONTACT_INFO, N_PROPERTIES, }; static GParamSpec *properties[N_PROPERTIES] = {NULL, }; static void purple_person_priority_contact_info_notify_cb(GObject *obj, G_GNUC_UNUSED GParamSpec *pspec, gpointer data); /****************************************************************************** * Helpers *****************************************************************************/ static void purple_person_set_id(PurplePerson *person, const char *id) { g_return_if_fail(PURPLE_IS_PERSON(person)); g_free(person->id); if(id != NULL) { person->id = g_strdup(id); } else { person->id = g_uuid_string_random(); } g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_ID]); } static gint purple_person_contact_compare(gconstpointer a, gconstpointer b) { PurpleContactInfo *c1 = *(PurpleContactInfo **)a; PurpleContactInfo *c2 = *(PurpleContactInfo **)b; PurplePresence *p1 = NULL; PurplePresence *p2 = NULL; p1 = purple_contact_info_get_presence(c1); p2 = purple_contact_info_get_presence(c2); return purple_presence_compare(p1, p2); } static void purple_person_sort_contacts(PurplePerson *person, PurpleContactInfo *original_priority) { PurpleContactInfo *new_priority = NULL; guint n_items = person->contacts->len; g_ptr_array_sort(person->contacts, purple_person_contact_compare); /* Tell the list we update our stuff. */ g_list_model_items_changed(G_LIST_MODEL(person), 0, n_items, n_items); g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_N_ITEMS]); /* See if the priority contact changed. */ new_priority = g_ptr_array_index(person->contacts, 0); if(original_priority != new_priority) { PurpleAvatar *old_avatar = NULL; PurpleAvatar *new_avatar = NULL; GObject *obj = G_OBJECT(person); const char *old_color = NULL; const char *new_color = NULL; if(PURPLE_IS_CONTACT_INFO(original_priority)) { old_avatar = purple_contact_info_get_avatar(original_priority); old_color = purple_contact_info_get_color(original_priority); g_signal_handlers_disconnect_by_func(original_priority, purple_person_priority_contact_info_notify_cb, person); } if(PURPLE_IS_CONTACT_INFO(new_priority)) { new_avatar = purple_contact_info_get_avatar(new_priority); new_color = purple_contact_info_get_color(new_priority); g_signal_connect_object(new_priority, "notify", G_CALLBACK(purple_person_priority_contact_info_notify_cb), person, 0); } g_object_freeze_notify(obj); g_object_notify_by_pspec(obj, properties[PROP_NAME_FOR_DISPLAY]); g_object_notify_by_pspec(obj, properties[PROP_PRIORITY_CONTACT_INFO]); /* If the color isn't overridden by the person, check if it has * changed. */ if(purple_strempty(person->color)) { if(!purple_strequal(old_color, new_color)) { g_object_notify_by_pspec(obj, properties[PROP_COLOR_FOR_DISPLAY]); } } /* If the person doesn't have an avatar set, check if the avatar * changed and notify if it has. */ if(!PURPLE_IS_AVATAR(person->avatar)) { if(old_avatar != new_avatar) { g_object_notify_by_pspec(obj, properties[PROP_AVATAR_FOR_DISPLAY]); } } g_object_thaw_notify(obj); } } /* This function is used by purple_person_matches to determine if a contact info * matches the needle. */ static gboolean purple_person_matches_find_func(gconstpointer a, gconstpointer b) { PurpleContactInfo *info = (gpointer)a; const char *needle = b; return purple_contact_info_matches(info, needle); } /****************************************************************************** * Callbacks *****************************************************************************/ static void purple_person_priority_contact_info_notify_cb(G_GNUC_UNUSED GObject *obj, GParamSpec *pspec, gpointer data) { PurplePerson *person = data; const char *property = NULL; property = g_param_spec_get_name(pspec); if(purple_strequal(property, "name-for-display")) { g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_NAME_FOR_DISPLAY]); } else if(purple_strequal(property, "avatar")) { g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_AVATAR_FOR_DISPLAY]); } else if(purple_strequal(property, "color")) { g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_COLOR_FOR_DISPLAY]); } } static void purple_person_presence_notify_cb(G_GNUC_UNUSED GObject *obj, G_GNUC_UNUSED GParamSpec *pspec, gpointer data) { PurplePerson *person = data; PurpleContactInfo *current_priority = NULL; current_priority = purple_person_get_priority_contact_info(person); purple_person_sort_contacts(person, current_priority); } /****************************************************************************** * GListModel Implementation *****************************************************************************/ static GType purple_person_get_item_type(G_GNUC_UNUSED GListModel *list) { return PURPLE_TYPE_CONTACT_INFO; } static guint purple_person_get_n_items(GListModel *list) { PurplePerson *person = PURPLE_PERSON(list); return person->contacts->len; } static gpointer purple_person_get_item(GListModel *list, guint position) { PurplePerson *person = PURPLE_PERSON(list); PurpleContactInfo *info = NULL; if(position < person->contacts->len) { info = g_ptr_array_index(person->contacts, position); g_object_ref(info); } return info; } static void purple_person_list_model_init(GListModelInterface *iface) { iface->get_item_type = purple_person_get_item_type; iface->get_n_items = purple_person_get_n_items; iface->get_item = purple_person_get_item; } /****************************************************************************** * GObject Implementation *****************************************************************************/ G_DEFINE_FINAL_TYPE_WITH_CODE(PurplePerson, purple_person, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, purple_person_list_model_init)) static void purple_person_get_property(GObject *obj, guint param_id, GValue *value, GParamSpec *pspec) { PurplePerson *person = PURPLE_PERSON(obj); switch(param_id) { case PROP_ITEM_TYPE: g_value_set_gtype(value, purple_person_get_item_type(G_LIST_MODEL(person))); break; case PROP_N_ITEMS: g_value_set_uint(value, purple_person_get_n_items(G_LIST_MODEL(person))); break; case PROP_ID: g_value_set_string(value, purple_person_get_id(person)); break; case PROP_ALIAS: g_value_set_string(value, purple_person_get_alias(person)); break; case PROP_AVATAR: g_value_set_object(value, purple_person_get_avatar(person)); break; case PROP_AVATAR_FOR_DISPLAY: g_value_set_object(value, purple_person_get_avatar_for_display(person)); break; case PROP_COLOR: g_value_set_string(value, purple_person_get_color(person)); break; case PROP_COLOR_FOR_DISPLAY: g_value_set_string(value, purple_person_get_color_for_display(person)); break; case PROP_TAGS: g_value_set_object(value, purple_person_get_tags(person)); break; case PROP_NAME_FOR_DISPLAY: g_value_set_string(value, purple_person_get_name_for_display(person)); break; case PROP_PRIORITY_CONTACT_INFO: g_value_set_object(value, purple_person_get_priority_contact_info(person)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void purple_person_set_property(GObject *obj, guint param_id, const GValue *value, GParamSpec *pspec) { PurplePerson *person = PURPLE_PERSON(obj); switch(param_id) { case PROP_ID: purple_person_set_id(person, g_value_get_string(value)); break; case PROP_ALIAS: purple_person_set_alias(person, g_value_get_string(value)); break; case PROP_AVATAR: purple_person_set_avatar(person, g_value_get_object(value)); break; case PROP_COLOR: purple_person_set_color(person, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void purple_person_dispose(GObject *obj) { PurplePerson *person = PURPLE_PERSON(obj); g_clear_object(&person->avatar); g_clear_object(&person->tags); if(person->contacts != NULL) { g_ptr_array_free(person->contacts, TRUE); person->contacts = NULL; } G_OBJECT_CLASS(purple_person_parent_class)->dispose(obj); } static void purple_person_finalize(GObject *obj) { PurplePerson *person = PURPLE_PERSON(obj); g_clear_pointer(&person->id, g_free); g_clear_pointer(&person->alias, g_free); g_clear_pointer(&person->color, g_free); G_OBJECT_CLASS(purple_person_parent_class)->finalize(obj); } static void purple_person_constructed(GObject *obj) { PurplePerson *person = NULL; G_OBJECT_CLASS(purple_person_parent_class)->constructed(obj); person = PURPLE_PERSON(obj); if(person->id == NULL) { purple_person_set_id(person, NULL); } } static void purple_person_init(PurplePerson *person) { person->tags = purple_tags_new(); person->contacts = g_ptr_array_new_full(0, (GDestroyNotify)g_object_unref); } static void purple_person_class_init(PurplePersonClass *klass) { GObjectClass *obj_class = G_OBJECT_CLASS(klass); obj_class->get_property = purple_person_get_property; obj_class->set_property = purple_person_set_property; obj_class->constructed = purple_person_constructed; obj_class->dispose = purple_person_dispose; obj_class->finalize = purple_person_finalize; /** * PurplePerson:item-type: * * The type of items. See [vfunc@Gio.ListModel.get_item_type]. * * Since: 3.0 */ properties[PROP_ITEM_TYPE] = g_param_spec_gtype( "item-type", NULL, NULL, G_TYPE_OBJECT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:n-items: * * The number of items. See [vfunc@Gio.ListModel.get_n_items]. * * Since: 3.0 */ properties[PROP_N_ITEMS] = g_param_spec_uint( "n-items", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:id: * * The protocol specific id for the contact. * * Since: 3.0 */ properties[PROP_ID] = g_param_spec_string( "id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * PurplePerson:alias: * * The alias for this person. This is controlled by the libpurple user. * * Since: 3.0 */ properties[PROP_ALIAS] = g_param_spec_string( "alias", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:avatar: * * The avatar for this person. This is controlled by the libpurple user, * which they can use to set a custom avatar. * * Since: 3.0 */ properties[PROP_AVATAR] = g_param_spec_object( "avatar", NULL, NULL, PURPLE_TYPE_AVATAR, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:avatar-for-display * * The avatar to show for the person. If [property@Purple.Person:avatar] is * set, it will be returned. Otherwise the value of * [property@Purple.ContactInfo:avatar] for * [property@Purple.Person:priority-contact-info] will be returned. * * Since: 3.0 */ properties[PROP_AVATAR_FOR_DISPLAY] = g_param_spec_object( "avatar-for-display", NULL, NULL, PURPLE_TYPE_AVATAR, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:color: * * A custom color to use for this person which will override any colors for * the contacts that belong to this person. * * This is an RGB hex code that user interfaces can use when rendering the * person. * * Since: 3.0 */ properties[PROP_COLOR] = g_param_spec_string( "color", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:color-for-display: * * The color to use for this person. * * This will return the value of [property@Person:color] if it is set, * otherwise it will return the value of [property@ContactInfo:color] of * the priority contact info. * * Since: 3.0 */ properties[PROP_COLOR_FOR_DISPLAY] = g_param_spec_string( "color-for-display", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:tags: * * The [class@Purple.Tags] for this person. * * Since: 3.0 */ properties[PROP_TAGS] = g_param_spec_object( "tags", NULL, NULL, PURPLE_TYPE_TAGS, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:name-for-display: * * The name that should be displayed for this person. * * If [property@Purple.Person:alias] is set that will be returned. If not * the value of [method@Purple.ContactInfo.get_name_for_display] for * [property@Purple.Person:priority-contact-info] will be used. If * [property@Purple.Person:priority-contact-info] is %NULL, then %NULL will * be returned. * * Since: 3.0 */ properties[PROP_NAME_FOR_DISPLAY] = g_param_spec_string( "name-for-display", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PurplePerson:priority-contact-info: * * The [class@Purple.ContactInfo] that currently has the highest priority. * * This is used by user interfaces to determine which * [class@Purple.ContactInfo] to use when messaging and so on. * * Since: 3.0 */ properties[PROP_PRIORITY_CONTACT_INFO] = g_param_spec_object( "priority-contact-info", NULL, NULL, PURPLE_TYPE_CONTACT_INFO, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(obj_class, N_PROPERTIES, properties); } /****************************************************************************** * Public API *****************************************************************************/ PurplePerson * purple_person_new(void) { return g_object_new(PURPLE_TYPE_PERSON, NULL); } const char * purple_person_get_id(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); return person->id; } const char * purple_person_get_alias(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); return person->alias; } void purple_person_set_alias(PurplePerson *person, const char *alias) { g_return_if_fail(PURPLE_IS_PERSON(person)); if(g_set_str(&person->alias, alias)) { GObject *obj = G_OBJECT(person); g_object_freeze_notify(obj); g_object_notify_by_pspec(obj, properties[PROP_ALIAS]); g_object_notify_by_pspec(obj, properties[PROP_NAME_FOR_DISPLAY]); g_object_thaw_notify(obj); } } PurpleAvatar * purple_person_get_avatar_for_display(PurplePerson *person) { PurpleContactInfo *priority = NULL; g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); if(PURPLE_IS_AVATAR(person->avatar)) { return person->avatar; } priority = purple_person_get_priority_contact_info(person); if(PURPLE_IS_CONTACT_INFO(priority)) { return purple_contact_info_get_avatar(priority); } return NULL; } PurpleAvatar * purple_person_get_avatar(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); return person->avatar; } void purple_person_set_avatar(PurplePerson *person, PurpleAvatar *avatar) { g_return_if_fail(PURPLE_IS_PERSON(person)); if(g_set_object(&person->avatar, avatar)) { GObject *obj = G_OBJECT(person); g_object_freeze_notify(obj); g_object_notify_by_pspec(obj, properties[PROP_AVATAR]); g_object_notify_by_pspec(obj, properties[PROP_AVATAR_FOR_DISPLAY]); g_object_thaw_notify(obj); } } const char * purple_person_get_color(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); return person->color; } void purple_person_set_color(PurplePerson *person, const char *color) { g_return_if_fail(PURPLE_IS_PERSON(person)); if(g_set_str(&person->color, color)) { GObject *obj = G_OBJECT(person); g_object_freeze_notify(obj); g_object_notify_by_pspec(obj, properties[PROP_COLOR]); g_object_notify_by_pspec(obj, properties[PROP_COLOR_FOR_DISPLAY]); g_object_thaw_notify(obj); } } const char * purple_person_get_color_for_display(PurplePerson *person) { PurpleContactInfo *priority = NULL; g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); if(!purple_strempty(person->color)) { return person->color; } priority = purple_person_get_priority_contact_info(person); if(PURPLE_IS_CONTACT_INFO(priority)) { return purple_contact_info_get_color(priority); } return NULL; } PurpleTags * purple_person_get_tags(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); return person->tags; } const char * purple_person_get_name_for_display(PurplePerson *person) { PurpleContactInfo *priority = NULL; g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); if(!purple_strempty(person->alias)) { return person->alias; } priority = purple_person_get_priority_contact_info(person); if(PURPLE_IS_CONTACT_INFO(priority)) { return purple_contact_info_get_name_for_display(priority); } return NULL; } void purple_person_add_contact_info(PurplePerson *person, PurpleContactInfo *info) { PurplePresence *presence = NULL; PurpleContactInfo *current_priority = NULL; g_return_if_fail(PURPLE_IS_PERSON(person)); g_return_if_fail(PURPLE_IS_CONTACT_INFO(info)); current_priority = purple_person_get_priority_contact_info(person); g_ptr_array_add(person->contacts, g_object_ref(info)); presence = purple_contact_info_get_presence(info); g_signal_connect_object(presence, "notify", G_CALLBACK(purple_person_presence_notify_cb), person, 0); purple_contact_info_set_person(info, person); purple_person_sort_contacts(person, current_priority); } gboolean purple_person_remove_contact_info(PurplePerson *person, PurpleContactInfo *info) { PurpleContactInfo *current_priority = NULL; gboolean removed = FALSE; g_return_val_if_fail(PURPLE_IS_PERSON(person), FALSE); g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), FALSE); /* Ref the contact info to avoid a use-after free. */ g_object_ref(info); current_priority = purple_person_get_priority_contact_info(person); /* g_ptr_array_remove calls g_object_unref because we passed it in as a * GDestroyNotify. */ removed = g_ptr_array_remove(person->contacts, info); if(removed) { PurplePresence *presence = purple_contact_info_get_presence(info); g_signal_handlers_disconnect_by_func(presence, purple_person_presence_notify_cb, person); purple_contact_info_set_person(info, NULL); purple_person_sort_contacts(person, current_priority); } /* Remove our reference. */ g_object_unref(info); return removed; } PurpleContactInfo * purple_person_get_priority_contact_info(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL); if(person->contacts->len == 0) { return NULL; } return g_ptr_array_index(person->contacts, 0); } gboolean purple_person_has_contacts(PurplePerson *person) { g_return_val_if_fail(PURPLE_IS_PERSON(person), FALSE); return person->contacts->len > 0; } gboolean purple_person_matches(PurplePerson *person, const char *needle) { g_return_val_if_fail(PURPLE_IS_PERSON(person), FALSE); if(purple_strempty(needle)) { return TRUE; } /* Check if the person's alias matches. */ if(!purple_strempty(person->alias)) { if(purple_strmatches(needle, person->alias)) { return TRUE; } } /* See if any of the contact infos match. */ return g_ptr_array_find_with_equal_func(person->contacts, needle, purple_person_matches_find_func, NULL); }