diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:57:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:57:27 +0000 |
commit | 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 (patch) | |
tree | d423850ae901365e582137bdf2b5cbdffd7ca266 /src/gs-search-page.c | |
parent | Initial commit. (diff) | |
download | gnome-software-upstream.tar.xz gnome-software-upstream.zip |
Adding upstream version 43.5.upstream/43.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/gs-search-page.c | 566 |
1 files changed, 566 insertions, 0 deletions
diff --git a/src/gs-search-page.c b/src/gs-search-page.c new file mode 100644 index 0000000..3b54c2c --- /dev/null +++ b/src/gs-search-page.c @@ -0,0 +1,566 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-search-page.h" +#include "gs-shell.h" +#include "gs-common.h" +#include "gs-app-row.h" + +#define GS_SEARCH_PAGE_MAX_RESULTS 50 + +struct _GsSearchPage +{ + GsPage parent_instance; + + GsPluginLoader *plugin_loader; + GCancellable *cancellable; + GCancellable *search_cancellable; + GtkSizeGroup *sizegroup_name; + GtkSizeGroup *sizegroup_button_label; + GtkSizeGroup *sizegroup_button_image; + GsShell *shell; + gchar *appid_to_show; + gchar *value; + guint waiting_id; + guint max_results; + guint stamp; + gboolean changed; + + GtkWidget *list_box_search; + GtkWidget *scrolledwindow_search; + GtkWidget *spinner_search; + GtkWidget *stack_search; +}; + +G_DEFINE_TYPE (GsSearchPage, gs_search_page, GS_TYPE_PAGE) + +typedef enum { + PROP_VADJUSTMENT = 1, +} GsSearchPageProperty; + +static void +gs_search_page_app_row_clicked_cb (GsAppRow *app_row, + GsSearchPage *self) +{ + GsApp *app; + app = gs_app_row_get_app (app_row); + if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE) + gs_page_install_app (GS_PAGE (self), app, GS_SHELL_INTERACTION_FULL, + self->cancellable); + else if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED) + gs_page_remove_app (GS_PAGE (self), app, self->cancellable); + else if (gs_app_get_state (app) == GS_APP_STATE_UNAVAILABLE) { + if (gs_app_get_url_missing (app) == NULL) { + gs_page_install_app (GS_PAGE (self), app, + GS_SHELL_INTERACTION_FULL, + self->cancellable); + return; + } + gs_shell_show_uri (self->shell, + gs_app_get_url_missing (app)); + } +} + +static void +gs_search_page_waiting_cancel (GsSearchPage *self) +{ + if (self->waiting_id > 0) + g_source_remove (self->waiting_id); + self->waiting_id = 0; +} + +static void +gs_search_page_app_to_show_created_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GsSearchPage *self = user_data; + g_autoptr(GsApp) app = NULL; + g_autoptr(GError) error = NULL; + + app = gs_plugin_loader_app_create_finish (GS_PLUGIN_LOADER (source_object), result, &error); + if (app != NULL) { + g_return_if_fail (GS_IS_SEARCH_PAGE (self)); + + gs_shell_show_app (self->shell, app); + } +} + +typedef struct { + GsSearchPage *self; + guint stamp; +} GetSearchData; + +static void +gs_search_page_get_search_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + guint i; + g_autofree GetSearchData *search_data = user_data; + GsApp *app; + GsSearchPage *self = search_data->self; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); + GtkWidget *app_row; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + + /* different stamps means another search had been started before this one finished */ + if (search_data->stamp != self->stamp) + return; + + /* don't do the delayed spinner */ + gs_search_page_waiting_cancel (self); + + list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error); + if (list == NULL) { + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_debug ("search cancelled"); + return; + } + g_warning ("failed to get search apps: %s", error->message); + gtk_spinner_stop (GTK_SPINNER (self->spinner_search)); + if (self->value && self->value[0]) + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "no-results"); + else + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "no-search"); + return; + } + + /* no results */ + if (gs_app_list_length (list) == 0) { + g_debug ("no search results to show"); + if (self->value && self->value[0]) + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "no-results"); + else + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "no-search"); + return; + } + + /* remove old entries */ + gs_widget_remove_all (self->list_box_search, (GsRemoveFunc) gtk_list_box_remove); + + gtk_spinner_stop (GTK_SPINNER (self->spinner_search)); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "results"); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + app_row = gs_app_row_new (app); + gs_app_row_set_show_rating (GS_APP_ROW (app_row), TRUE); + g_signal_connect (app_row, "button-clicked", + G_CALLBACK (gs_search_page_app_row_clicked_cb), + self); + gtk_list_box_append (GTK_LIST_BOX (self->list_box_search), app_row); + gs_app_row_set_size_groups (GS_APP_ROW (app_row), + self->sizegroup_name, + self->sizegroup_button_label, + self->sizegroup_button_image); + gtk_widget_show (app_row); + } + + /* too many results */ + if (gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)) { + GtkStyleContext *context; + GtkWidget *w = gtk_label_new (NULL); + g_autofree gchar *str = NULL; + + /* TRANSLATORS: this is when there are too many search results + * to show in in the search page */ + str = g_strdup_printf (ngettext("%u more match", + "%u more matches", + gs_app_list_get_size_peak (list) - gs_app_list_length (list)), + gs_app_list_get_size_peak (list) - gs_app_list_length (list)); + gtk_label_set_label (GTK_LABEL (w), str); + gtk_widget_set_margin_bottom (w, 20); + gtk_widget_set_margin_top (w, 20); + gtk_widget_set_margin_start (w, 20); + gtk_widget_set_margin_end (w, 20); + context = gtk_widget_get_style_context (w); + gtk_style_context_add_class (context, "dim-label"); + gtk_list_box_append (GTK_LIST_BOX (self->list_box_search), w); + gtk_widget_show (w); + } else { + /* reset to default */ + self->max_results = GS_SEARCH_PAGE_MAX_RESULTS; + } + + if (self->appid_to_show != NULL) { + g_autoptr (GsApp) a = NULL; + if (as_utils_data_id_valid (self->appid_to_show)) { + gs_plugin_loader_app_create_async (self->plugin_loader, self->appid_to_show, self->cancellable, + gs_search_page_app_to_show_created_cb, self); + } else { + a = gs_app_new (self->appid_to_show); + } + + if (a) + gs_shell_show_app (self->shell, a); + + g_clear_pointer (&self->appid_to_show, g_free); + } +} + +static gboolean +gs_search_page_waiting_show_cb (gpointer user_data) +{ + GsSearchPage *self = GS_SEARCH_PAGE (user_data); + + /* show spinner */ + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_search), "spinner"); + gtk_spinner_start (GTK_SPINNER (self->spinner_search)); + gs_search_page_waiting_cancel (self); + return FALSE; +} + +static gchar * +gs_search_page_get_app_sort_key (GsApp *app) +{ + GString *key = g_string_sized_new (64); + + /* sort apps before runtimes and extensions */ + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_DESKTOP_APP: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* sort missing codecs before applications */ + switch (gs_app_get_state (app)) { + case GS_APP_STATE_UNAVAILABLE: + g_string_append (key, "9:"); + break; + default: + g_string_append (key, "1:"); + break; + } + + /* sort by the search key */ + g_string_append_printf (key, "%05x:", gs_app_get_match_value (app)); + + /* sort by rating */ + g_string_append_printf (key, "%03i:", gs_app_get_rating (app)); + + /* sort by kudos */ + g_string_append_printf (key, "%03u:", gs_app_get_kudos_percentage (app)); + + return g_string_free (key, FALSE); +} + +static gint +gs_search_page_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data) +{ + g_autofree gchar *key1 = NULL; + g_autofree gchar *key2 = NULL; + key1 = gs_search_page_get_app_sort_key (app1); + key2 = gs_search_page_get_app_sort_key (app2); + return g_strcmp0 (key2, key1); +} + +static void +gs_search_page_load (GsSearchPage *self) +{ + g_autoptr(GsPluginJob) plugin_job = NULL; + g_autoptr(GsAppQuery) query = NULL; + const gchar *keywords[2] = { NULL, }; + g_autofree GetSearchData *search_data = NULL; + + self->changed = FALSE; + + /* cancel any pending searches */ + g_cancellable_cancel (self->search_cancellable); + g_clear_object (&self->search_cancellable); + self->search_cancellable = g_cancellable_new (); + self->stamp++; + + /* search for apps */ + gs_search_page_waiting_cancel (self); + self->waiting_id = g_timeout_add (250, gs_search_page_waiting_show_cb, self); + + search_data = g_new0 (GetSearchData, 1); + search_data->self = self; + search_data->stamp = self->stamp; + + keywords[0] = self->value; + query = gs_app_query_new ("keywords", keywords, + "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING, + "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED | + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES, + "max-results", self->max_results, + "sort-func", gs_search_page_sort_cb, + "sort-user-data", self, + NULL); + plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_NONE); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->search_cancellable, + gs_search_page_get_search_cb, + g_steal_pointer (&search_data)); +} + +static void +gs_search_page_app_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsSearchPage *self) +{ + GsApp *app; + + /* increase the maximum allowed, and re-request the search */ + if (!GS_IS_APP_ROW (row)) { + self->max_results *= 4; + gs_search_page_load (self); + return; + } + + app = gs_app_row_get_app (GS_APP_ROW (row)); + gs_shell_show_app (self->shell, app); +} + +static void +gs_search_page_reload (GsPage *page) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + if (self->value != NULL) + gs_search_page_load (self); +} + +/** + * gs_search_page_set_appid_to_show: + * + * Switch to the specified app id after loading the search results. + **/ +void +gs_search_page_set_appid_to_show (GsSearchPage *self, const gchar *appid) +{ + if (appid == self->appid_to_show || + g_strcmp0 (appid, self->appid_to_show) == 0) + return; + + g_free (self->appid_to_show); + self->appid_to_show = g_strdup (appid); + + self->changed = TRUE; +} + +const gchar * +gs_search_page_get_text (GsSearchPage *self) +{ + return self->value; +} + +void +gs_search_page_set_text (GsSearchPage *self, const gchar *value) +{ + if (value == self->value || + g_strcmp0 (value, self->value) == 0) + return; + + g_free (self->value); + self->value = g_strdup (value); + + /* Load immediately, when the page is active */ + if (self->value && gs_page_is_active (GS_PAGE (self))) + gs_search_page_load (self); + else + self->changed = TRUE; +} + +static void +gs_search_page_switch_to (GsPage *page) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_SEARCH) { + g_warning ("Called switch_to(search) when in mode %s", + gs_shell_get_mode_string (self->shell)); + return; + } + + if (self->value && self->changed) + gs_search_page_load (self); +} + +static void +gs_search_page_switch_from (GsPage *page) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + + g_cancellable_cancel (self->search_cancellable); + g_clear_object (&self->search_cancellable); +} + +static void +gs_search_page_cancel_cb (GCancellable *cancellable, + GsSearchPage *self) +{ + g_cancellable_cancel (self->search_cancellable); +} + +static void +gs_search_page_app_installed (GsPage *page, GsApp *app) +{ + gs_search_page_reload (page); +} + +static void +gs_search_page_app_removed (GsPage *page, GsApp *app) +{ + gs_search_page_reload (page); +} + +static gboolean +gs_search_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GError **error) +{ + GsSearchPage *self = GS_SEARCH_PAGE (page); + + g_return_val_if_fail (GS_IS_SEARCH_PAGE (self), TRUE); + + self->plugin_loader = g_object_ref (plugin_loader); + self->cancellable = g_object_ref (cancellable); + self->shell = shell; + + /* connect the cancellables */ + g_cancellable_connect (self->cancellable, + G_CALLBACK (gs_search_page_cancel_cb), + self, NULL); + + /* setup search */ + g_signal_connect (self->list_box_search, "row-activated", + G_CALLBACK (gs_search_page_app_row_activated_cb), self); + return TRUE; +} + +static void +gs_search_page_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsSearchPage *self = GS_SEARCH_PAGE (object); + + switch ((GsSearchPageProperty) prop_id) { + case PROP_VADJUSTMENT: + g_value_set_object (value, gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_search))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_search_page_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + switch ((GsSearchPageProperty) prop_id) { + case PROP_VADJUSTMENT: + /* Not supported yet */ + g_assert_not_reached (); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_search_page_dispose (GObject *object) +{ + GsSearchPage *self = GS_SEARCH_PAGE (object); + + g_clear_object (&self->sizegroup_name); + g_clear_object (&self->sizegroup_button_label); + g_clear_object (&self->sizegroup_button_image); + + g_clear_object (&self->plugin_loader); + g_clear_object (&self->cancellable); + g_clear_object (&self->search_cancellable); + + G_OBJECT_CLASS (gs_search_page_parent_class)->dispose (object); +} + +static void +gs_search_page_finalize (GObject *object) +{ + GsSearchPage *self = GS_SEARCH_PAGE (object); + + g_free (self->appid_to_show); + g_free (self->value); + + G_OBJECT_CLASS (gs_search_page_parent_class)->finalize (object); +} + +static void +gs_search_page_class_init (GsSearchPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GsPageClass *page_class = GS_PAGE_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gs_search_page_get_property; + object_class->set_property = gs_search_page_set_property; + object_class->dispose = gs_search_page_dispose; + object_class->finalize = gs_search_page_finalize; + + page_class->app_installed = gs_search_page_app_installed; + page_class->app_removed = gs_search_page_app_removed; + page_class->switch_to = gs_search_page_switch_to; + page_class->switch_from = gs_search_page_switch_from; + page_class->reload = gs_search_page_reload; + page_class->setup = gs_search_page_setup; + + g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment"); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-search-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, list_box_search); + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, scrolledwindow_search); + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, spinner_search); + gtk_widget_class_bind_template_child (widget_class, GsSearchPage, stack_search); +} + +static void +gs_search_page_init (GsSearchPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button_label = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->sizegroup_button_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + self->max_results = GS_SEARCH_PAGE_MAX_RESULTS; +} + +GsSearchPage * +gs_search_page_new (void) +{ + GsSearchPage *self; + self = g_object_new (GS_TYPE_SEARCH_PAGE, NULL); + return GS_SEARCH_PAGE (self); +} |