From 73d8343a3e3a61c259967268becc8de3ba9659eb Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 13 Oct 2023 11:32:54 +0100 Subject: [PATCH] restrict-applications-dialog: Add support for searching the app list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a type-ahead search which filters the app list by app name. It’s always visible because search is a very obvious interaction the user might want to do to find an app in the app list. That’s why `GtkSearchBar` is not being used here. Signed-off-by: Philip Withnall Fixes: #31 --- .../restrict-applications-dialog.c | 52 ++++++++++ .../restrict-applications-dialog.ui | 16 ++- .../restrict-applications-selector.c | 99 ++++++++++++++++++- .../restrict-applications-selector.h | 4 + .../restrict-applications-selector.ui | 11 +++ 5 files changed, 177 insertions(+), 5 deletions(-) diff --git a/libmalcontent-ui/restrict-applications-dialog.c b/libmalcontent-ui/restrict-applications-dialog.c index 83e3da2..d09b0cb 100644 --- a/libmalcontent-ui/restrict-applications-dialog.c +++ b/libmalcontent-ui/restrict-applications-dialog.c @@ -55,11 +55,18 @@ struct _MctRestrictApplicationsDialog MctRestrictApplicationsSelector *selector; AdwPreferencesGroup *group; + GtkSearchEntry *search_entry; MctAppFilter *app_filter; /* (owned) (not nullable) */ gchar *user_display_name; /* (owned) (nullable) */ }; +static void search_entry_stop_search_cb (GtkSearchEntry *search_entry, + gpointer user_data); +static gboolean focus_search_cb (GtkWidget *widget, + GVariant *arguments, + gpointer user_data); + G_DEFINE_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, ADW_TYPE_PREFERENCES_WINDOW) typedef enum @@ -140,6 +147,19 @@ mct_restrict_applications_dialog_dispose (GObject *object) G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->dispose (object); } +static void +mct_restrict_applications_dialog_map (GtkWidget *widget) +{ + MctRestrictApplicationsDialog *self = (MctRestrictApplicationsDialog *)widget; + + GTK_WIDGET_CLASS (mct_restrict_applications_dialog_parent_class)->map (widget); + + /* Clear and focus the search entry, in case the dialogue is being shown for + * a second time. */ + gtk_editable_set_text (GTK_EDITABLE (self->search_entry), ""); + gtk_widget_grab_focus (GTK_WIDGET (self->search_entry)); +} + static void mct_restrict_applications_dialog_class_init (MctRestrictApplicationsDialogClass *klass) { @@ -151,6 +171,8 @@ mct_restrict_applications_dialog_class_init (MctRestrictApplicationsDialogClass object_class->set_property = mct_restrict_applications_dialog_set_property; object_class->dispose = mct_restrict_applications_dialog_dispose; + widget_class->map = mct_restrict_applications_dialog_map; + /** * MctRestrictApplicationsDialog:app-filter: (not nullable) * @@ -197,6 +219,15 @@ mct_restrict_applications_dialog_class_init (MctRestrictApplicationsDialogClass gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsDialog, selector); gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsDialog, group); + gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsDialog, search_entry); + + gtk_widget_class_bind_template_callback (widget_class, search_entry_stop_search_cb); + + gtk_widget_class_add_binding (widget_class, + GDK_KEY_f, GDK_CONTROL_MASK, + focus_search_cb, + NULL); + } static void @@ -206,6 +237,8 @@ mct_restrict_applications_dialog_init (MctRestrictApplicationsDialog *self) g_type_ensure (MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR); gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_search_entry_set_key_capture_widget (self->search_entry, GTK_WIDGET (self)); } static void @@ -225,6 +258,25 @@ update_description (MctRestrictApplicationsDialog *self) adw_preferences_group_set_description (self->group, description); } +static void +search_entry_stop_search_cb (GtkSearchEntry *search_entry, + gpointer user_data) +{ + /* Clear the search text as the search filtering is bound to that. */ + gtk_editable_set_text (GTK_EDITABLE (search_entry), ""); +} + +static gboolean +focus_search_cb (GtkWidget *widget, + GVariant *arguments, + gpointer user_data) +{ + MctRestrictApplicationsDialog *self = MCT_RESTRICT_APPLICATIONS_DIALOG (widget); + + gtk_widget_grab_focus (GTK_WIDGET (self->search_entry)); + return TRUE; +} + /** * mct_restrict_applications_dialog_new: * @app_filter: (transfer none): the initial app filter configuration to show diff --git a/libmalcontent-ui/restrict-applications-dialog.ui b/libmalcontent-ui/restrict-applications-dialog.ui index 2fd728f..744d3a5 100644 --- a/libmalcontent-ui/restrict-applications-dialog.ui +++ b/libmalcontent-ui/restrict-applications-dialog.ui @@ -14,7 +14,21 @@ Restrict {username} from using the following installed applications. - + + vertical + 12 + + + Search for applications… + + + + + + + + + diff --git a/libmalcontent-ui/restrict-applications-selector.c b/libmalcontent-ui/restrict-applications-selector.c index 30f2dd9..c151066 100644 --- a/libmalcontent-ui/restrict-applications-selector.c +++ b/libmalcontent-ui/restrict-applications-selector.c @@ -41,6 +41,7 @@ static void app_info_changed_cb (GAppInfoMonitor *monitor, static void reload_apps (MctRestrictApplicationsSelector *self); static GtkWidget *create_row_for_app_cb (gpointer item, gpointer user_data); +static char *app_info_dup_name (GAppInfo *app_info); /** * MctRestrictApplicationsSelector: @@ -54,6 +55,11 @@ static GtkWidget *create_row_for_app_cb (gpointer item, * #MctAppFilterBuilder using * mct_restrict_applications_selector_build_app_filter(). * + * Search terms may be applied using #MctRestrictApplicationsSelector:search. + * These will filter the list of displayed apps so that only ones matching the + * search terms (by name, using UTF-8 normalisation and casefolding) will be + * displayed. + * * Since: 0.5.0 */ struct _MctRestrictApplicationsSelector @@ -64,6 +70,8 @@ struct _MctRestrictApplicationsSelector GList *cached_apps; /* (nullable) (owned) (element-type GAppInfo) */ GListStore *apps; + GtkFilterListModel *filtered_apps; + GtkStringFilter *search_filter; GAppInfoMonitor *app_info_monitor; /* (owned) */ gulong app_info_monitor_changed_id; GHashTable *blocklisted_apps; /* (owned) (element-type GAppInfo) */ @@ -74,6 +82,8 @@ struct _MctRestrictApplicationsSelector FlatpakInstallation *user_installation; /* (owned) */ GtkCssProvider *css_provider; /* (owned) */ + + gchar *search; /* (nullable) (owned) */ }; G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_selector, GTK_TYPE_BOX) @@ -81,9 +91,10 @@ G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_select typedef enum { PROP_APP_FILTER = 1, + PROP_SEARCH, } MctRestrictApplicationsSelectorProperty; -static GParamSpec *properties[PROP_APP_FILTER + 1]; +static GParamSpec *properties[PROP_SEARCH + 1]; enum { SIGNAL_CHANGED, @@ -124,6 +135,9 @@ mct_restrict_applications_selector_get_property (GObject *object, case PROP_APP_FILTER: g_value_set_boxed (value, self->app_filter); break; + case PROP_SEARCH: + g_value_set_string (value, self->search); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -143,6 +157,9 @@ mct_restrict_applications_selector_set_property (GObject *object, case PROP_APP_FILTER: mct_restrict_applications_selector_set_app_filter (self, g_value_get_boxed (value)); break; + case PROP_SEARCH: + mct_restrict_applications_selector_set_search (self, g_value_get_string (value)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -167,6 +184,7 @@ mct_restrict_applications_selector_dispose (GObject *object) g_clear_object (&self->system_installation); g_clear_object (&self->user_installation); g_clear_object (&self->css_provider); + g_clear_pointer (&self->search, g_free); G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->dispose (object); } @@ -201,6 +219,23 @@ mct_restrict_applications_selector_class_init (MctRestrictApplicationsSelectorCl G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * MctRestrictApplicationsSelector:search: (nullable) + * + * Search terms to filter the displayed list of apps by, or %NULL to not + * filter the search. + * + * Since: 0.12.0 + */ + properties[PROP_SEARCH] = + g_param_spec_string ("search", + "Search", + "Search terms to filter the displayed list of apps by.", + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); /** @@ -223,12 +258,15 @@ mct_restrict_applications_selector_class_init (MctRestrictApplicationsSelectorCl gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, listbox); gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, apps); + gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, filtered_apps); + gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, search_filter); + + gtk_widget_class_bind_template_callback (widget_class, app_info_dup_name); } static void mct_restrict_applications_selector_init (MctRestrictApplicationsSelector *self) { - guint n_apps; gtk_widget_init_template (GTK_WIDGET (self)); @@ -238,7 +276,7 @@ mct_restrict_applications_selector_init (MctRestrictApplicationsSelector *self) (GCallback) app_info_changed_cb, self); gtk_list_box_bind_model (self->listbox, - G_LIST_MODEL (self->apps), + G_LIST_MODEL (self->filtered_apps), create_row_for_app_cb, self, NULL); @@ -359,6 +397,12 @@ create_row_for_app_cb (gpointer item, return row; } +static char * +app_info_dup_name (GAppInfo *app_info) +{ + return g_strdup (g_app_info_get_name (app_info)); +} + static gint compare_app_info_cb (gconstpointer a, gconstpointer b, @@ -767,7 +811,7 @@ mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelect self->app_filter = mct_app_filter_ref (app_filter); /* Update the status of each app row. */ - n_apps = g_list_model_get_n_items (G_LIST_MODEL (self->apps)); + n_apps = g_list_model_get_n_items (G_LIST_MODEL (self->filtered_apps)); for (guint i = 0; i < n_apps; i++) { @@ -786,3 +830,50 @@ mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelect g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_APP_FILTER]); } + +/** + * mct_restrict_applications_selector_get_search: + * @self: an #MctRestrictApplicationsSelector + * + * Get the value of #MctRestrictApplicationsSelector:search. + * + * Returns: current search terms, or %NULL if no search filtering is active + * Since: 0.12.0 + */ +const gchar * +mct_restrict_applications_selector_get_search (MctRestrictApplicationsSelector *self) +{ + g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self), NULL); + + return self->search; +} + +/** + * mct_restrict_applications_selector_set_search: + * @self: an #MctRestrictApplicationsSelector + * @search: (nullable): search terms, or %NULL to not filter the app list + * + * Set the value of #MctRestrictApplicationsSelector:search, or clear it to + * %NULL. + * + * Since: 0.12.0 + */ +void +mct_restrict_applications_selector_set_search (MctRestrictApplicationsSelector *self, + const gchar *search) +{ + g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self)); + + /* Squash empty search terms down to nothing. */ + if (search != NULL && *search == '\0') + search = NULL; + + if (g_strcmp0 (search, self->search) == 0) + return; + + g_clear_pointer (&self->search, g_free); + self->search = g_strdup (search); + + gtk_string_filter_set_search (self->search_filter, self->search); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH]); +} diff --git a/libmalcontent-ui/restrict-applications-selector.h b/libmalcontent-ui/restrict-applications-selector.h index e02a10a..8a37916 100644 --- a/libmalcontent-ui/restrict-applications-selector.h +++ b/libmalcontent-ui/restrict-applications-selector.h @@ -41,4 +41,8 @@ void mct_restrict_applications_selector_set_app_filter (MctRestrictAppl void mct_restrict_applications_selector_build_app_filter (MctRestrictApplicationsSelector *self, MctAppFilterBuilder *builder); +const gchar *mct_restrict_applications_selector_get_search (MctRestrictApplicationsSelector *self); +void mct_restrict_applications_selector_set_search (MctRestrictApplicationsSelector *self, + const gchar *search); + G_END_DECLS diff --git a/libmalcontent-ui/restrict-applications-selector.ui b/libmalcontent-ui/restrict-applications-selector.ui index a45ea64..0c75dfb 100644 --- a/libmalcontent-ui/restrict-applications-selector.ui +++ b/libmalcontent-ui/restrict-applications-selector.ui @@ -26,4 +26,15 @@ GAppInfo + + + apps + search_filter + + + + + + +