restrict-applications-dialog: Add support for searching the app list

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 <pwithnall@endlessos.org>

Fixes: #31
This commit is contained in:
Philip Withnall 2023-10-13 11:32:54 +01:00
parent 69253a2b2c
commit 73d8343a3e
5 changed files with 177 additions and 5 deletions

View File

@ -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

View File

@ -14,7 +14,21 @@
<!-- Translated dynamically: -->
<property name="description">Restrict {username} from using the following installed applications.</property>
<child>
<object class="MctRestrictApplicationsSelector" id="selector" />
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="placeholder-text" translatable="yes">Search for applications…</property>
<signal name="stop-search" handler="search_entry_stop_search_cb" />
</object>
</child>
<child>
<object class="MctRestrictApplicationsSelector" id="selector">
<property name="search" bind-source="search_entry" bind-property="text" />
</object>
</child>
</object>
</child>
</object>
</child>

View File

@ -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]);
}

View File

@ -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

View File

@ -26,4 +26,15 @@
<object class="GListStore" id="apps">
<property name="item-type">GAppInfo</property>
</object>
<object class="GtkFilterListModel" id="filtered_apps">
<property name="model">apps</property>
<property name="filter">search_filter</property>
</object>
<object class="GtkStringFilter" id="search_filter">
<property name="expression">
<closure type="gchararray" function="app_info_dup_name" />
</property>
</object>
</interface>