[gnome-software/1243-searching-for-an-app-results-in-a-never-ending-spinner: 28/28] gs-search-page: Provide partial search results as soon as known




commit 2de3e2a9bf081637e875569e023e198f44a9113a
Author: Milan Crha <mcrha redhat com>
Date:   Wed May 19 16:01:09 2021 +0200

    gs-search-page: Provide partial search results as soon as known
    
    As soon as any plugin returns new search results show them in the search
    page, instead of waiting until all the plugins finish the search.
    
    Closes https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1243

 lib/gs-app-list-private.h |   3 ++
 lib/gs-app-list.c         |  40 +++++++++++++++
 lib/gs-plugin-loader.c    | 127 ++++++++++++++++++++++++++++++++++++++++++++++
 src/gs-search-page.c      | 101 +++++++++++++++++++++++++++---------
 4 files changed, 247 insertions(+), 24 deletions(-)
---
diff --git a/lib/gs-app-list-private.h b/lib/gs-app-list-private.h
index 3bf908c16..0d94c9c8e 100644
--- a/lib/gs-app-list-private.h
+++ b/lib/gs-app-list-private.h
@@ -34,6 +34,9 @@ typedef enum {
 } GsAppListFlags;
 
 GsAppList      *gs_app_list_copy               (GsAppList      *list);
+GsAppList      *gs_app_list_copy_range         (GsAppList      *list,
+                                                guint           index_from,
+                                                guint           n_elements);
 guint           gs_app_list_get_size_peak      (GsAppList      *list);
 void            gs_app_list_filter_duplicates  (GsAppList      *list,
                                                 GsAppListFilterFlags flags);
diff --git a/lib/gs-app-list.c b/lib/gs-app-list.c
index 33a01ce56..4363f5cf6 100644
--- a/lib/gs-app-list.c
+++ b/lib/gs-app-list.c
@@ -882,6 +882,46 @@ gs_app_list_copy (GsAppList *list)
        return new;
 }
 
+/**
+ * gs_app_list_copy_range:
+ * @list: A #GsAppList
+ * @index_from: the range index to copy from, inclusive
+ * @n_elements: number of elements to copy
+ *
+ * Return a deep copy of the application list with applications
+ * in the given range. When the @n_elements is 0, the list is copied
+ * from the @index_from until the end.
+ *
+ * Returns: (transfer full): A newly allocated #GsAppList
+ *
+ * Since: 41
+ **/
+GsAppList *
+gs_app_list_copy_range (GsAppList *list,
+                       guint index_from,
+                       guint n_elements)
+{
+       GsAppList *copy;
+       guint ii, up_to;
+
+       g_return_val_if_fail (GS_IS_APP_LIST (list), NULL);
+
+       if (n_elements == 0)
+               up_to = gs_app_list_length (list);
+       else
+               up_to = index_from + n_elements;
+
+       if (up_to > gs_app_list_length (list))
+               up_to = gs_app_list_length (list);
+
+       copy = gs_app_list_new ();
+       for (ii = index_from; ii < up_to; ii++) {
+               GsApp *app = gs_app_list_index (list, ii);
+               gs_app_list_add_safe (copy, app, GS_APP_LIST_ADD_FLAG_NONE);
+       }
+       return copy;
+}
+
 static void
 gs_app_list_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
index 056171e40..90d523536 100644
--- a/lib/gs-plugin-loader.c
+++ b/lib/gs-plugin-loader.c
@@ -92,6 +92,7 @@ enum {
        SIGNAL_UPDATES_CHANGED,
        SIGNAL_RELOAD,
        SIGNAL_BASIC_AUTH_START,
+       SIGNAL_SEARCH_MATCH,
        SIGNAL_LAST
 };
 
@@ -1100,6 +1101,12 @@ gs_plugin_loader_job_sorted_truncation (GsPluginLoaderHelper *helper)
        gs_app_list_truncate (list, max_results);
 }
 
+static gboolean
+gs_plugin_loader_report_partial_search_results (GsPluginLoaderHelper *helper,
+                                               guint last_n_new,
+                                               GCancellable *cancellable,
+                                               GError **error);
+
 static gboolean
 gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
                              GCancellable *cancellable,
@@ -1110,6 +1117,7 @@ gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
 #ifdef HAVE_SYSPROF
        gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
 #endif
+       guint last_list_length = 0;
 
        /* Refining is done separately as it’s a special action */
        g_assert (action != GS_PLUGIN_ACTION_REFINE);
@@ -1138,6 +1146,18 @@ gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
                                                  cancellable, error)) {
                        return FALSE;
                }
+               if (gs_plugin_job_get_action (helper->plugin_job) == GS_PLUGIN_ACTION_SEARCH) {
+                       GsAppList *app_list = gs_plugin_job_get_list (helper->plugin_job);
+                       guint list_length = gs_app_list_length (app_list);
+
+                       /* Notify with the partial result only if the list changed */
+                       if (last_list_length < list_length) {
+                               if (!gs_plugin_loader_report_partial_search_results (helper, list_length - 
last_list_length, cancellable, error))
+                                       return FALSE;
+
+                               last_list_length = list_length;
+                       }
+               }
                gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
        }
 
@@ -2832,6 +2852,11 @@ gs_plugin_loader_class_init (GsPluginLoaderClass *klass)
                              G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
                              0, NULL, NULL, g_cclosure_marshal_generic,
                              G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER);
+       signals [SIGNAL_SEARCH_MATCH] =
+               g_signal_new ("search-match",
+                             G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+                             0, NULL, NULL, g_cclosure_marshal_generic,
+                             G_TYPE_NONE, 2, G_TYPE_STRING, GS_TYPE_APP_LIST);
 }
 
 static void
@@ -3523,6 +3548,108 @@ gs_plugin_loader_process_thread_cb (GTask *task,
        g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
 }
 
+typedef struct _SearchResultsData {
+       GWeakRef plugin_loader_weakref;
+       GsAppList *list;
+       gchar *search;
+} SearchResultsData;
+
+static void
+search_results_data_free (gpointer ptr)
+{
+       SearchResultsData *srd = ptr;
+
+       if (srd) {
+               g_weak_ref_clear (&srd->plugin_loader_weakref);
+               g_clear_object (&srd->list);
+               g_free (srd->search);
+               g_slice_free (SearchResultsData, srd);
+       }
+}
+
+static gboolean
+gs_plugin_loader_search_results_idle_cb (gpointer user_data)
+{
+       SearchResultsData *srd = user_data;
+       g_autoptr(GsPluginLoader) plugin_loader = NULL;
+
+       plugin_loader = g_weak_ref_get (&srd->plugin_loader_weakref);
+       if (plugin_loader)
+               g_signal_emit (plugin_loader, signals[SIGNAL_SEARCH_MATCH], 0, srd->search, srd->list);
+
+       return G_SOURCE_REMOVE;
+}
+
+static gboolean
+gs_plugin_loader_report_partial_search_results (GsPluginLoaderHelper *helper,
+                                               guint last_n_new,
+                                               GCancellable *cancellable,
+                                               GError **error)
+{
+       GsPluginLoader *plugin_loader = helper->plugin_loader;
+       GsAppListFilterFlags dedupe_flags;
+       GsPluginRefineFlags filter_flags;
+       guint max_results;
+       GsAppListSortFunc sort_func;
+       SearchResultsData *srd;
+       GsAppList *job_list;
+       g_autoptr(GsAppList) list = NULL;
+
+       job_list = gs_plugin_job_get_list (helper->plugin_job);
+       list = gs_app_list_copy_range (job_list, gs_app_list_length (job_list) - last_n_new, last_n_new);
+
+       /* refine with enough data so that the sort_func in
+        * gs_plugin_loader_job_sorted_truncation() can do what it needs */
+       filter_flags = gs_plugin_job_get_filter_flags (helper->plugin_job);
+       max_results = gs_plugin_job_get_max_results (helper->plugin_job);
+       sort_func = gs_plugin_job_get_sort_func (helper->plugin_job, NULL);
+       if (filter_flags > 0 && max_results > 0 && sort_func != NULL) {
+               g_autoptr(GsPluginLoaderHelper) helper2 = NULL;
+               g_autoptr(GsPluginJob) plugin_job = NULL;
+               plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+                                                "list", list,
+                                                "refine-flags", filter_flags,
+                                                NULL);
+               helper2 = gs_plugin_loader_helper_new (helper->plugin_loader, plugin_job);
+               helper2->function_name_parent = helper->function_name;
+               g_debug ("running filter flags with early refine");
+               if (!gs_plugin_loader_run_refine_filter (helper2, list,
+                                                        filter_flags,
+                                                        cancellable, error)) {
+                       gs_utils_error_convert_gio (error);
+                       return FALSE;
+               }
+       }
+
+       /* filter to reduce to a sane set */
+       gs_plugin_loader_job_sorted_truncation (helper);
+
+       gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+       gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+       gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+       if (gs_plugin_job_get_refine_flags (helper->plugin_job) != 0) {
+               if (!gs_plugin_loader_run_refine (helper, list, cancellable, error)) {
+                       return FALSE;
+               }
+       }
+       gs_app_list_filter (list, gs_plugin_loader_app_set_prio, plugin_loader);
+       dedupe_flags = gs_plugin_job_get_dedupe_flags (helper->plugin_job);
+       if (dedupe_flags != GS_APP_LIST_FILTER_FLAG_NONE)
+               gs_app_list_filter_duplicates (list, dedupe_flags);
+
+       /* sort these again as the refine may have added useful metadata */
+       gs_plugin_loader_job_sorted_truncation_again (helper);
+
+       srd = g_slice_new0 (SearchResultsData);
+       g_weak_ref_init (&srd->plugin_loader_weakref, plugin_loader);
+       srd->search = g_strdup (gs_plugin_job_get_search (helper->plugin_job));
+       srd->list = g_steal_pointer (&list);
+
+       g_idle_add_full (G_PRIORITY_DEFAULT, gs_plugin_loader_search_results_idle_cb, srd, 
search_results_data_free);
+
+       return TRUE;
+}
+
 static void
 gs_plugin_loader_process_in_thread_pool_cb (gpointer data,
                                            gpointer user_data)
diff --git a/src/gs-search-page.c b/src/gs-search-page.c
index b22e3bd3b..62f60a429 100644
--- a/src/gs-search-page.c
+++ b/src/gs-search-page.c
@@ -36,6 +36,7 @@ struct _GsSearchPage
        guint                    waiting_id;
        guint                    max_results;
        gboolean                 changed;
+       GHashTable              *known_results; /* GsApp * as pointer ~> nothing */
 
        GtkWidget               *list_box_search;
        GtkWidget               *scrolledwindow_search;
@@ -80,16 +81,58 @@ gs_search_page_waiting_cancel (GsSearchPage *self)
        self->waiting_id = 0;
 }
 
+static void
+gs_search_page_claim_results (GsSearchPage *self,
+                             GsAppList *list)
+{
+       guint i;
+
+       /* don't do the delayed spinner */
+       gs_search_page_waiting_cancel (self);
+
+       if (gs_app_list_length (list) == 0)
+               return;
+
+       if (!g_hash_table_size (self->known_results)) {
+               /* remove old entries */
+               gs_container_remove_all (GTK_CONTAINER (self->list_box_search));
+       }
+
+       gs_stop_spinner (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++) {
+               GtkWidget *app_row;
+               GsApp *app;
+
+               app = gs_app_list_index (list, i);
+               if (g_hash_table_contains (self->known_results, app))
+                       continue;
+               g_hash_table_add (self->known_results, app);
+               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_container_add (GTK_CONTAINER (self->list_box_search), app_row);
+               gs_app_row_set_size_groups (GS_APP_ROW (app_row),
+                                           self->sizegroup_image,
+                                           self->sizegroup_name,
+                                           self->sizegroup_desc,
+                                           self->sizegroup_button);
+               gtk_widget_show (app_row);
+
+               if (g_hash_table_size (self->known_results) >= self->max_results)
+                       break;
+       }
+}
+
 static void
 gs_search_page_get_search_cb (GObject *source_object,
                               GAsyncResult *res,
                               gpointer user_data)
 {
-       guint i;
-       GsApp *app;
        GsSearchPage *self = GS_SEARCH_PAGE (user_data);
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
-       GtkWidget *app_row;
        g_autoptr(GError) error = NULL;
        g_autoptr(GsAppList) list = NULL;
 
@@ -115,26 +158,9 @@ gs_search_page_get_search_cb (GObject *source_object,
                return;
        }
 
-       /* remove old entries */
-       gs_container_remove_all (GTK_CONTAINER (self->list_box_search));
-
-       gs_stop_spinner (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_container_add (GTK_CONTAINER (self->list_box_search), app_row);
-               gs_app_row_set_size_groups (GS_APP_ROW (app_row),
-                                           self->sizegroup_image,
-                                           self->sizegroup_name,
-                                           self->sizegroup_desc,
-                                           self->sizegroup_button);
-               gtk_widget_show (app_row);
-       }
+       /* This will show the precise data */
+       g_hash_table_remove_all (self->known_results);
+       gs_search_page_claim_results (self, list);
 
        /* too many results */
        if (gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)) {
@@ -143,7 +169,7 @@ gs_search_page_get_search_cb (GObject *source_object,
                g_autofree gchar *str = NULL;
 
                /* TRANSLATORS: this is when there are too many search results
-                * to show in in the search page */
+                * to show 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)),
@@ -252,6 +278,7 @@ gs_search_page_load (GsSearchPage *self)
        /* search for apps */
        gs_search_page_waiting_cancel (self);
        self->waiting_id = g_timeout_add (250, gs_search_page_waiting_show_cb, self);
+       g_hash_table_remove_all (self->known_results);
        plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
                                         "search", self->value,
                                         "max-results", self->max_results,
@@ -385,6 +412,26 @@ gs_search_page_app_removed (GsPage *page, GsApp *app)
        gs_search_page_reload (page);
 }
 
+static void
+gs_search_page_search_match_cb (GsPluginLoader *plugin_loader,
+                               const gchar *search_term,
+                               GsAppList *list,
+                               GsSearchPage *self)
+{
+       g_return_if_fail (GS_IS_SEARCH_PAGE (self));
+       g_return_if_fail (GS_IS_APP_LIST (list));
+
+       if (g_strcmp0 (search_term, self->value) != 0)
+               return;
+
+       gs_app_list_sort (list, gs_search_page_sort_cb, self);
+
+       if (gs_app_list_length (list) > self->max_results)
+               gs_app_list_truncate (list, self->max_results);
+
+       gs_search_page_claim_results (self, list);
+}
+
 static gboolean
 gs_search_page_setup (GsPage *page,
                       GsShell *shell,
@@ -408,6 +455,10 @@ gs_search_page_setup (GsPage *page,
        /* setup search */
        g_signal_connect (self->list_box_search, "row-activated",
                          G_CALLBACK (gs_search_page_app_row_activated_cb), self);
+
+       g_signal_connect_object (plugin_loader, "search-match",
+               G_CALLBACK (gs_search_page_search_match_cb), self, 0);
+
        return TRUE;
 }
 
@@ -468,6 +519,7 @@ gs_search_page_finalize (GObject *object)
 {
        GsSearchPage *self = GS_SEARCH_PAGE (object);
 
+       g_hash_table_destroy (self->known_results);
        g_free (self->appid_to_show);
        g_free (self->value);
 
@@ -512,6 +564,7 @@ gs_search_page_init (GsSearchPage *self)
        self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
        self->sizegroup_desc = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
        self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+       self->known_results = g_hash_table_new (g_direct_hash, g_direct_equal);
 
        self->max_results = GS_SEARCH_PAGE_MAX_RESULTS;
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]