Merge branch '31-app-list-search' into 'main'

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

Closes #31

See merge request pwithnall/malcontent!155
This commit is contained in:
Philip Withnall 2023-10-13 12:31:20 +00:00
commit cbad81565e
7 changed files with 188 additions and 18 deletions

View File

@ -55,11 +55,18 @@ struct _MctRestrictApplicationsDialog
MctRestrictApplicationsSelector *selector; MctRestrictApplicationsSelector *selector;
AdwPreferencesGroup *group; AdwPreferencesGroup *group;
GtkSearchEntry *search_entry;
MctAppFilter *app_filter; /* (owned) (not nullable) */ MctAppFilter *app_filter; /* (owned) (not nullable) */
gchar *user_display_name; /* (owned) (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) G_DEFINE_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, ADW_TYPE_PREFERENCES_WINDOW)
typedef enum typedef enum
@ -140,6 +147,19 @@ mct_restrict_applications_dialog_dispose (GObject *object)
G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->dispose (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 static void
mct_restrict_applications_dialog_class_init (MctRestrictApplicationsDialogClass *klass) 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->set_property = mct_restrict_applications_dialog_set_property;
object_class->dispose = mct_restrict_applications_dialog_dispose; object_class->dispose = mct_restrict_applications_dialog_dispose;
widget_class->map = mct_restrict_applications_dialog_map;
/** /**
* MctRestrictApplicationsDialog:app-filter: (not nullable) * 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, selector);
gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsDialog, group); 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 static void
@ -206,6 +237,8 @@ mct_restrict_applications_dialog_init (MctRestrictApplicationsDialog *self)
g_type_ensure (MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR); g_type_ensure (MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR);
gtk_widget_init_template (GTK_WIDGET (self)); gtk_widget_init_template (GTK_WIDGET (self));
gtk_search_entry_set_key_capture_widget (self->search_entry, GTK_WIDGET (self));
} }
static void static void
@ -225,6 +258,25 @@ update_description (MctRestrictApplicationsDialog *self)
adw_preferences_group_set_description (self->group, description); 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: * mct_restrict_applications_dialog_new:
* @app_filter: (transfer none): the initial app filter configuration to show * @app_filter: (transfer none): the initial app filter configuration to show

View File

@ -14,7 +14,21 @@
<!-- Translated dynamically: --> <!-- Translated dynamically: -->
<property name="description">Restrict {username} from using the following installed applications.</property> <property name="description">Restrict {username} from using the following installed applications.</property>
<child> <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> </child>
</object> </object>
</child> </child>

View File

@ -41,6 +41,7 @@ static void app_info_changed_cb (GAppInfoMonitor *monitor,
static void reload_apps (MctRestrictApplicationsSelector *self); static void reload_apps (MctRestrictApplicationsSelector *self);
static GtkWidget *create_row_for_app_cb (gpointer item, static GtkWidget *create_row_for_app_cb (gpointer item,
gpointer user_data); gpointer user_data);
static char *app_info_dup_name (GAppInfo *app_info);
/** /**
* MctRestrictApplicationsSelector: * MctRestrictApplicationsSelector:
@ -54,6 +55,11 @@ static GtkWidget *create_row_for_app_cb (gpointer item,
* #MctAppFilterBuilder using * #MctAppFilterBuilder using
* mct_restrict_applications_selector_build_app_filter(). * 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 * Since: 0.5.0
*/ */
struct _MctRestrictApplicationsSelector struct _MctRestrictApplicationsSelector
@ -61,10 +67,11 @@ struct _MctRestrictApplicationsSelector
GtkBox parent_instance; GtkBox parent_instance;
GtkListBox *listbox; GtkListBox *listbox;
GtkLabel *placeholder;
GList *cached_apps; /* (nullable) (owned) (element-type GAppInfo) */ GList *cached_apps; /* (nullable) (owned) (element-type GAppInfo) */
GListStore *apps; /* (owned) */ GListStore *apps;
GtkFilterListModel *filtered_apps;
GtkStringFilter *search_filter;
GAppInfoMonitor *app_info_monitor; /* (owned) */ GAppInfoMonitor *app_info_monitor; /* (owned) */
gulong app_info_monitor_changed_id; gulong app_info_monitor_changed_id;
GHashTable *blocklisted_apps; /* (owned) (element-type GAppInfo) */ GHashTable *blocklisted_apps; /* (owned) (element-type GAppInfo) */
@ -75,6 +82,8 @@ struct _MctRestrictApplicationsSelector
FlatpakInstallation *user_installation; /* (owned) */ FlatpakInstallation *user_installation; /* (owned) */
GtkCssProvider *css_provider; /* (owned) */ GtkCssProvider *css_provider; /* (owned) */
gchar *search; /* (nullable) (owned) */
}; };
G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_selector, GTK_TYPE_BOX) G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_selector, GTK_TYPE_BOX)
@ -82,9 +91,10 @@ G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_select
typedef enum typedef enum
{ {
PROP_APP_FILTER = 1, PROP_APP_FILTER = 1,
PROP_SEARCH,
} MctRestrictApplicationsSelectorProperty; } MctRestrictApplicationsSelectorProperty;
static GParamSpec *properties[PROP_APP_FILTER + 1]; static GParamSpec *properties[PROP_SEARCH + 1];
enum { enum {
SIGNAL_CHANGED, SIGNAL_CHANGED,
@ -125,6 +135,9 @@ mct_restrict_applications_selector_get_property (GObject *object,
case PROP_APP_FILTER: case PROP_APP_FILTER:
g_value_set_boxed (value, self->app_filter); g_value_set_boxed (value, self->app_filter);
break; break;
case PROP_SEARCH:
g_value_set_string (value, self->search);
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@ -144,6 +157,9 @@ mct_restrict_applications_selector_set_property (GObject *object,
case PROP_APP_FILTER: case PROP_APP_FILTER:
mct_restrict_applications_selector_set_app_filter (self, g_value_get_boxed (value)); mct_restrict_applications_selector_set_app_filter (self, g_value_get_boxed (value));
break; break;
case PROP_SEARCH:
mct_restrict_applications_selector_set_search (self, g_value_get_string (value));
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@ -156,7 +172,6 @@ mct_restrict_applications_selector_dispose (GObject *object)
MctRestrictApplicationsSelector *self = (MctRestrictApplicationsSelector *)object; MctRestrictApplicationsSelector *self = (MctRestrictApplicationsSelector *)object;
g_clear_pointer (&self->blocklisted_apps, g_hash_table_unref); g_clear_pointer (&self->blocklisted_apps, g_hash_table_unref);
g_clear_object (&self->apps);
g_clear_list (&self->cached_apps, g_object_unref); g_clear_list (&self->cached_apps, g_object_unref);
if (self->app_info_monitor != NULL && self->app_info_monitor_changed_id != 0) if (self->app_info_monitor != NULL && self->app_info_monitor_changed_id != 0)
@ -169,6 +184,7 @@ mct_restrict_applications_selector_dispose (GObject *object)
g_clear_object (&self->system_installation); g_clear_object (&self->system_installation);
g_clear_object (&self->user_installation); g_clear_object (&self->user_installation);
g_clear_object (&self->css_provider); g_clear_object (&self->css_provider);
g_clear_pointer (&self->search, g_free);
G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->dispose (object); G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->dispose (object);
} }
@ -203,6 +219,23 @@ mct_restrict_applications_selector_class_init (MctRestrictApplicationsSelectorCl
G_PARAM_STATIC_STRINGS | G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY); 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); g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
/** /**
@ -224,34 +257,30 @@ mct_restrict_applications_selector_class_init (MctRestrictApplicationsSelectorCl
gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentUi/ui/restrict-applications-selector.ui"); gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentUi/ui/restrict-applications-selector.ui");
gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, listbox); gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, listbox);
gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, placeholder); 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 static void
mct_restrict_applications_selector_init (MctRestrictApplicationsSelector *self) mct_restrict_applications_selector_init (MctRestrictApplicationsSelector *self)
{ {
guint n_apps;
gtk_widget_init_template (GTK_WIDGET (self)); gtk_widget_init_template (GTK_WIDGET (self));
self->apps = g_list_store_new (G_TYPE_APP_INFO);
self->cached_apps = NULL;
self->app_info_monitor = g_app_info_monitor_get (); self->app_info_monitor = g_app_info_monitor_get ();
self->app_info_monitor_changed_id = self->app_info_monitor_changed_id =
g_signal_connect (self->app_info_monitor, "changed", g_signal_connect (self->app_info_monitor, "changed",
(GCallback) app_info_changed_cb, self); (GCallback) app_info_changed_cb, self);
gtk_list_box_bind_model (self->listbox, gtk_list_box_bind_model (self->listbox,
G_LIST_MODEL (self->apps), G_LIST_MODEL (self->filtered_apps),
create_row_for_app_cb, create_row_for_app_cb,
self, self,
NULL); NULL);
/* Hide placeholder if not empty */
n_apps = g_list_model_get_n_items (G_LIST_MODEL (self->apps));
gtk_widget_set_visible (GTK_WIDGET (self->placeholder), n_apps != 0);
self->blocklisted_apps = g_hash_table_new_full (g_direct_hash, self->blocklisted_apps = g_hash_table_new_full (g_direct_hash,
g_direct_equal, g_direct_equal,
g_object_unref, g_object_unref,
@ -368,6 +397,12 @@ create_row_for_app_cb (gpointer item,
return row; return row;
} }
static char *
app_info_dup_name (GAppInfo *app_info)
{
return g_strdup (g_app_info_get_name (app_info));
}
static gint static gint
compare_app_info_cb (gconstpointer a, compare_app_info_cb (gconstpointer a,
gconstpointer b, gconstpointer b,
@ -776,7 +811,7 @@ mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelect
self->app_filter = mct_app_filter_ref (app_filter); self->app_filter = mct_app_filter_ref (app_filter);
/* Update the status of each app row. */ /* 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++) for (guint i = 0; i < n_apps; i++)
{ {
@ -795,3 +830,50 @@ mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelect
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_APP_FILTER]); 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, void mct_restrict_applications_selector_build_app_filter (MctRestrictApplicationsSelector *self,
MctAppFilterBuilder *builder); 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 G_END_DECLS

View File

@ -9,8 +9,10 @@
<property name="selection-mode">none</property> <property name="selection-mode">none</property>
<child type="placeholder"> <child type="placeholder">
<object class="GtkLabel" id="placeholder"> <object class="GtkLabel">
<property name="label" translatable="yes">No applications found to restrict.</property> <property name="label" translatable="yes">No applications found to restrict.</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
</object> </object>
</child> </child>
@ -20,4 +22,19 @@
</object> </object>
</child> </child>
</template> </template>
<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> </interface>

View File

@ -702,6 +702,7 @@ mct_user_controls_finalize (GObject *object)
g_clear_object (&self->user); g_clear_object (&self->user);
g_clear_pointer (&self->user_locale, g_free); g_clear_pointer (&self->user_locale, g_free);
g_clear_pointer (&self->user_display_name, g_free); g_clear_pointer (&self->user_display_name, g_free);
g_clear_pointer (&self->description, g_free);
if (self->permission != NULL && self->permission_allowed_id != 0) if (self->permission != NULL && self->permission_allowed_id != 0)
{ {

View File

@ -1,5 +1,5 @@
project('malcontent', 'c', project('malcontent', 'c',
version : '0.11.1', version : '0.12.0',
meson_version : '>= 0.59.0', meson_version : '>= 0.59.0',
license: ['LGPL-2.1-or-later', 'GPL-2.0-or-later'], license: ['LGPL-2.1-or-later', 'GPL-2.0-or-later'],
default_options : [ default_options : [