Fri, 17 Feb 2023 19:32:38 -0600
Add search support to the contact list
This searches everything in `PurplePerson` and all of it's contacts, but does not yet cover tags. I am planning on covering that in another review request.
Right now the matching is just using `strstr`, but will will update this as part of [PIDGIN-17737](https://issues.imfreedom.org/issue/PIDGIN-17737/).
I also tried to figure out how to get focus back to the list view, but didn't come up with any viable solutions.
Testing Done:
Ran the unit tests and did a bunch of searches in the contact list with the demo protocol plugin.
Bugs closed: PIDGIN-17717
Reviewed at https://reviews.imfreedom.org/r/2211/
--- a/libpurple/purplecontactinfo.c Fri Feb 17 04:54:38 2023 -0600 +++ b/libpurple/purplecontactinfo.c Fri Feb 17 19:32:38 2023 -0600 @@ -797,3 +797,43 @@ return purple_utf8_strcasecmp(name_a, name_b); } + +gboolean +purple_contact_info_matches(PurpleContactInfo *info, const char *needle) { + PurpleContactInfoPrivate *priv = NULL; + + g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), FALSE); + + if(purple_strempty(needle)) { + return TRUE; + } + + priv = purple_contact_info_get_instance_private(info); + + if(!purple_strempty(priv->id)) { + if(strstr(priv->id, needle) != NULL) { + return TRUE; + } + } + + if(!purple_strempty(priv->username)) { + if(strstr(priv->username, needle) != NULL) { + return TRUE; + } + } + + if(!purple_strempty(priv->alias)) { + if(strstr(priv->alias, needle) != NULL) { + return TRUE; + } + } + + if(!purple_strempty(priv->display_name)) { + if(strstr(priv->display_name, needle) != NULL) { + return TRUE; + } + } + + /* Nothing matched, so return FALSE. */ + return FALSE; +}
--- a/libpurple/purplecontactinfo.h Fri Feb 17 04:54:38 2023 -0600 +++ b/libpurple/purplecontactinfo.h Fri Feb 17 19:32:38 2023 -0600 @@ -355,6 +355,22 @@ */ int purple_contact_info_compare(PurpleContactInfo *a, PurpleContactInfo *b); +/** + * purple_contact_info_matches: + * @info: The instance. + * @needle: (nullable): The string to match. + * + * This will determine if the alias, display name, or username matches @needle. + * + * If @needle is %NULL or empty string, %TRUE will be returned. + * + * Returns: %TRUE if @needle matches any of the above properties, otherwise + * %FALSE. + * + * Since: 3.0.0 + */ +gboolean purple_contact_info_matches(PurpleContactInfo *info, const char *needle); + G_END_DECLS #endif /* PURPLE_CONTACT_INFO_H */
--- a/libpurple/purpleperson.c Fri Feb 17 04:54:38 2023 -0600 +++ b/libpurple/purpleperson.c Fri Feb 17 19:32:38 2023 -0600 @@ -20,6 +20,8 @@ #include "util.h" +#include "util.h" + struct _PurplePerson { GObject parent; @@ -122,6 +124,17 @@ } } +/* 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 *****************************************************************************/ @@ -569,3 +582,24 @@ 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(strstr(person->alias, needle) != NULL) { + 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); +}
--- a/libpurple/purpleperson.h Fri Feb 17 04:54:38 2023 -0600 +++ b/libpurple/purpleperson.h Fri Feb 17 19:32:38 2023 -0600 @@ -220,6 +220,23 @@ */ gboolean purple_person_has_contacts(PurplePerson *person); +/** + * purple_person_matches: + * @person: The instance. + * @needle: (nullable): The string to match on. + * + * Checks if the alias matches @needle. This also checks @needle against + * [method@Purple.ContactInfo.matches] for each [class@Purple.ContactInfo] that + * @person is tracking. + * + * If @needle is %NULL or empty string, %TRUE will be returned. + * + * Returns: %TRUE if @person matches @needle in any way. + * + * Since: 3.0.0 + */ +gboolean purple_person_matches(PurplePerson *person, const char *needle); + G_END_DECLS #endif /* PURPLE_PERSON_H */
--- a/libpurple/tests/test_contact_info.c Fri Feb 17 04:54:38 2023 -0600 +++ b/libpurple/tests/test_contact_info.c Fri Feb 17 19:32:38 2023 -0600 @@ -315,6 +315,82 @@ } /****************************************************************************** + * Matches + *****************************************************************************/ +static void +test_purple_contact_info_matches_accepts_null(void) { + PurpleContactInfo *info = purple_contact_info_new(NULL); + + g_assert_true(purple_contact_info_matches(info, NULL)); + + g_clear_object(&info); +} + +static void +test_purple_contact_info_matches_empty_string(void) { + PurpleContactInfo *info = purple_contact_info_new(NULL); + + g_assert_true(purple_contact_info_matches(info, "")); + + g_clear_object(&info); +} + +static void +test_purple_contact_info_matches_id(void) { + PurpleContactInfo *info = purple_contact_info_new("this is an id"); + + g_assert_true(purple_contact_info_matches(info, "an")); + + g_clear_object(&info); +} + +static void +test_purple_contact_info_matches_username(void) { + PurpleContactInfo *info = purple_contact_info_new(NULL); + + purple_contact_info_set_username(info, "username"); + + g_assert_true(purple_contact_info_matches(info, "name")); + + g_clear_object(&info); +} + +static void +test_purple_contact_info_matches_alias(void) { + PurpleContactInfo *info = purple_contact_info_new(NULL); + + purple_contact_info_set_alias(info, "alias"); + + g_assert_true(purple_contact_info_matches(info, "lia")); + + g_clear_object(&info); +} + +static void +test_purple_contact_info_matches_display_name(void) { + PurpleContactInfo *info = purple_contact_info_new(NULL); + + purple_contact_info_set_display_name(info, "display name"); + + g_assert_true(purple_contact_info_matches(info, "play")); + + g_clear_object(&info); +} + +static void +test_purple_contact_info_matches_none(void) { + PurpleContactInfo *info = purple_contact_info_new("id"); + + purple_contact_info_set_username(info, "username"); + purple_contact_info_set_alias(info, "alias"); + purple_contact_info_set_display_name(info, "display name"); + + g_assert_false(purple_contact_info_matches(info, "nothing")); + + g_clear_object(&info); +} + +/****************************************************************************** * Main *****************************************************************************/ gint @@ -352,5 +428,20 @@ g_test_add_func("/contact-info/compare/name__name", test_purple_contact_info_compare_name__name); + g_test_add_func("/contact-info/matches/accepts_null", + test_purple_contact_info_matches_accepts_null); + g_test_add_func("/contact-info/matches/emptry_string", + test_purple_contact_info_matches_empty_string); + g_test_add_func("/contact-info/matches/id", + test_purple_contact_info_matches_id); + g_test_add_func("/contact-info/matches/username", + test_purple_contact_info_matches_username); + g_test_add_func("/contact-info/matches/alias", + test_purple_contact_info_matches_alias); + g_test_add_func("/contact-info/matches/display_name", + test_purple_contact_info_matches_display_name); + g_test_add_func("/contact-info/matches/none", + test_purple_contact_info_matches_none); + return g_test_run(); }
--- a/libpurple/tests/test_person.c Fri Feb 17 04:54:38 2023 -0600 +++ b/libpurple/tests/test_person.c Fri Feb 17 19:32:38 2023 -0600 @@ -443,6 +443,53 @@ } /****************************************************************************** + * Matches tests + *****************************************************************************/ +static void +test_purple_person_matches_accepts_null(void) { + PurplePerson *person = purple_person_new(); + + g_assert_true(purple_person_matches(person, NULL)); + + g_clear_object(&person); +} + +static void +test_purple_person_matches_empty_string(void) { + PurplePerson *person = purple_person_new(); + + g_assert_true(purple_person_matches(person, "")); + + g_clear_object(&person); +} + +static void +test_purple_person_matches_alias(void) { + PurplePerson *person = purple_person_new(); + + purple_person_set_alias(person, "this is the alias"); + + g_assert_true(purple_person_matches(person, "the")); + g_assert_false(purple_person_matches(person, "what")); + + g_clear_object(&person); +} + +static void +test_purple_person_matches_contact_info(void) { + PurplePerson *person = purple_person_new(); + PurpleContactInfo *info = purple_contact_info_new(NULL); + + purple_contact_info_set_username(info, "user1"); + purple_person_add_contact_info(person, info); + g_clear_object(&info); + + g_assert_true(purple_person_matches(person, "user1")); + + g_clear_object(&person); +} + +/****************************************************************************** * Main *****************************************************************************/ gint @@ -476,5 +523,14 @@ g_test_add_func("/person/priority/multiple-with-change", test_purple_person_priority_multiple_with_change); + g_test_add_func("/person/matches/accepts_null", + test_purple_person_matches_accepts_null); + g_test_add_func("/person/matches/empty_string", + test_purple_person_matches_empty_string); + g_test_add_func("/person/matches/alias", + test_purple_person_matches_alias); + g_test_add_func("/person/matches/contact_info", + test_purple_person_matches_contact_info); + return g_test_run(); -} \ No newline at end of file +}
--- a/pidgin/pidgincontactlist.c Fri Feb 17 04:54:38 2023 -0600 +++ b/pidgin/pidgincontactlist.c Fri Feb 17 19:32:38 2023 -0600 @@ -27,16 +27,44 @@ #include "pidgin/pidgincontactlist.h" struct _PidginContactList { - AdwBin parent; + GtkBox parent; + GtkFilterListModel *filter_model; + GtkCustomFilter *search_filter; + + GtkWidget *search_entry; GtkWidget *view; }; -G_DEFINE_TYPE(PidginContactList, pidgin_contact_list, ADW_TYPE_BIN) +G_DEFINE_TYPE(PidginContactList, pidgin_contact_list, GTK_TYPE_BOX) + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static gboolean +pidgin_contact_list_search_filter(GObject *item, gpointer data) { + PidginContactList *list = data; + PurplePerson *person = PURPLE_PERSON(item); + const char *needle = NULL; + + needle = gtk_editable_get_text(GTK_EDITABLE(list->search_entry)); + + return purple_person_matches(person, needle); +} /****************************************************************************** * Callbacks *****************************************************************************/ +static void +pidgin_contact_list_search_changed_cb(G_GNUC_UNUSED GtkSearchEntry *self, + gpointer data) +{ + PidginContactList *list = data; + + gtk_filter_changed(GTK_FILTER(list->search_filter), + GTK_FILTER_CHANGE_DIFFERENT); +} + static GdkTexture * pidgin_contact_list_avatar_cb(G_GNUC_UNUSED GObject *self, PurplePerson *person, @@ -53,6 +81,13 @@ return gdk_texture_new_for_pixbuf(pixbuf); } + /* When filtering we get called for rows that have been filtered out. I'm + * not sure why, but this does appear to be the case. + */ + if(person == NULL) { + return NULL; + } + info = purple_person_get_priority_contact_info(person); /* All of the contact info in the manager are PurpleContact's so this cast @@ -135,15 +170,15 @@ static void pidgin_contact_list_init(PidginContactList *list) { PurpleContactManager *manager = NULL; - GtkSingleSelection *selection = NULL; gtk_widget_init_template(GTK_WIDGET(list)); manager = purple_contact_manager_get_default(); - selection = gtk_single_selection_new(G_LIST_MODEL(manager)); + gtk_filter_list_model_set_model(list->filter_model, G_LIST_MODEL(manager)); - gtk_list_view_set_model(GTK_LIST_VIEW(list->view), - GTK_SELECTION_MODEL(selection)); + gtk_custom_filter_set_filter_func(list->search_filter, + (GtkCustomFilterFunc)pidgin_contact_list_search_filter, + list, NULL); } static void @@ -156,9 +191,18 @@ ); gtk_widget_class_bind_template_child(widget_class, PidginContactList, + filter_model); + gtk_widget_class_bind_template_child(widget_class, PidginContactList, + search_filter); + + gtk_widget_class_bind_template_child(widget_class, PidginContactList, + search_entry); + gtk_widget_class_bind_template_child(widget_class, PidginContactList, view); gtk_widget_class_bind_template_callback(widget_class, + pidgin_contact_list_search_changed_cb); + gtk_widget_class_bind_template_callback(widget_class, pidgin_contact_list_avatar_cb); gtk_widget_class_bind_template_callback(widget_class, pidgin_contact_list_activate_cb);
--- a/pidgin/pidgincontactlist.h Fri Feb 17 04:54:38 2023 -0600 +++ b/pidgin/pidgincontactlist.h Fri Feb 17 19:32:38 2023 -0600 @@ -46,7 +46,7 @@ #define PIDGIN_TYPE_CONTACT_LIST (pidgin_contact_list_get_type()) G_DECLARE_FINAL_TYPE(PidginContactList, pidgin_contact_list, PIDGIN, - CONTACT_LIST, AdwBin) + CONTACT_LIST, GtkBox) /** * pidgin_contact_list_new:
--- a/pidgin/resources/ContactList/widget.ui Fri Feb 17 04:54:38 2023 -0600 +++ b/pidgin/resources/ContactList/widget.ui Fri Feb 17 19:32:38 2023 -0600 @@ -24,9 +24,23 @@ <!-- interface-name Pidgin --> <!-- interface-description Internet Messenger --> <!-- interface-copyright Pidgin Developers <devel@pidgin.im> --> - <template class="PidginContactList" parent="AdwBin"> + <template class="PidginContactList" parent="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkSearchBar" id="search_bar"> + <property name="key-capture-widget">PidginContactList</property> + <property name="show-close-button">1</property> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <signal name="search-changed" handler="pidgin_contact_list_search_changed_cb"/> + </object> + </child> + </object> + </child> <child> <object class="GtkListView" id="view"> + <property name="hexpand">1</property> + <property name="vexpand">1</property> <property name="factory"> <object class="GtkBuilderListItemFactory"> <property name="bytes"> @@ -105,7 +119,20 @@ </property> </object> </property> - <signal name="activate" handler="pidgin_contact_list_activate_cb" swapped="no"/> + <property name="model"> + <object class="GtkSingleSelection"> + <property name="autoselect">1</property> + <property name="model"> + <object class="GtkFilterListModel" id="filter_model"> + <property name="incremental">0</property> + <property name="filter"> + <object class="GtkCustomFilter" id="search_filter"/> + </property> + </object> + </property> + </object> + </property> + <signal name="activate" handler="pidgin_contact_list_activate_cb"/> </object> </child> </template>