summaryrefslogtreecommitdiffstats
path: root/src/gs-installed-page.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/gs-installed-page.c974
1 files changed, 974 insertions, 0 deletions
diff --git a/src/gs-installed-page.c b/src/gs-installed-page.c
new file mode 100644
index 0000000..4b2997d
--- /dev/null
+++ b/src/gs-installed-page.c
@@ -0,0 +1,974 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.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-shell.h"
+#include "gs-installed-page.h"
+#include "gs-common.h"
+#include "gs-app-row.h"
+#include "gs-utils.h"
+
+struct _GsInstalledPage
+{
+ GsPage parent_instance;
+
+ GsPluginLoader *plugin_loader;
+ GCancellable *cancellable;
+ GtkSizeGroup *sizegroup_name;
+ GtkSizeGroup *sizegroup_button_label;
+ GtkSizeGroup *sizegroup_button_image;
+ gboolean cache_valid;
+ gboolean waiting;
+ GsShell *shell;
+ GSettings *settings;
+ guint pending_apps_counter;
+ gboolean is_narrow;
+
+ GtkWidget *group_install_in_progress;
+ GtkWidget *group_install_apps;
+ GtkWidget *group_install_system_apps;
+ GtkWidget *group_install_addons;
+ GtkWidget *group_install_web_apps;
+
+ GtkWidget *list_box_install_in_progress;
+ GtkWidget *list_box_install_apps;
+ GtkWidget *list_box_install_system_apps;
+ GtkWidget *list_box_install_addons;
+ GtkWidget *list_box_install_web_apps;
+ GtkWidget *scrolledwindow_install;
+ GtkWidget *spinner_install;
+ GtkWidget *stack_install;
+};
+
+G_DEFINE_TYPE (GsInstalledPage, gs_installed_page, GS_TYPE_PAGE)
+
+typedef enum {
+ PROP_IS_NARROW = 1,
+ /* Overrides: */
+ PROP_VADJUSTMENT,
+ PROP_TITLE,
+} GsInstalledPageProperty;
+
+static GParamSpec *obj_props[PROP_IS_NARROW + 1] = { NULL, };
+
+static void gs_installed_page_pending_apps_refined_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data);
+static GsPluginRefineFlags gs_installed_page_get_refine_flags (GsInstalledPage *self);
+static void gs_installed_page_notify_state_changed_cb (GsApp *app,
+ GParamSpec *pspec,
+ GsInstalledPage *self);
+
+typedef enum {
+ GS_UPDATE_LIST_SECTION_INSTALLING_AND_REMOVING,
+ GS_UPDATE_LIST_SECTION_REMOVABLE_APPS,
+ GS_UPDATE_LIST_SECTION_SYSTEM_APPS,
+ GS_UPDATE_LIST_SECTION_ADDONS,
+ GS_UPDATE_LIST_SECTION_WEB_APPS,
+ GS_UPDATE_LIST_SECTION_LAST
+} GsInstalledPageSection;
+
+/* This must mostly mirror gs_installed_page_get_app_sort_key() otherwise apps
+ * will end up sorted into a section they don’t belong in. */
+static GsInstalledPageSection
+gs_installed_page_get_app_section (GsApp *app)
+{
+ GsAppState state = gs_app_get_state (app);
+ AsComponentKind kind = gs_app_get_kind (app);
+
+ if (state == GS_APP_STATE_INSTALLING ||
+ state == GS_APP_STATE_QUEUED_FOR_INSTALL ||
+ state == GS_APP_STATE_REMOVING)
+ return GS_UPDATE_LIST_SECTION_INSTALLING_AND_REMOVING;
+
+ if (kind == AS_COMPONENT_KIND_DESKTOP_APP) {
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY))
+ return GS_UPDATE_LIST_SECTION_SYSTEM_APPS;
+ return GS_UPDATE_LIST_SECTION_REMOVABLE_APPS;
+ }
+
+ if (kind == AS_COMPONENT_KIND_WEB_APP)
+ return GS_UPDATE_LIST_SECTION_WEB_APPS;
+
+ return GS_UPDATE_LIST_SECTION_ADDONS;
+}
+
+static void
+update_groups (GsInstalledPage *self)
+{
+ gtk_widget_set_visible (self->group_install_in_progress,
+ gtk_widget_get_first_child (self->list_box_install_in_progress) != NULL);
+ gtk_widget_set_visible (self->group_install_apps,
+ gtk_widget_get_first_child (self->list_box_install_apps) != NULL);
+ gtk_widget_set_visible (self->group_install_system_apps,
+ gtk_widget_get_first_child (self->list_box_install_system_apps) != NULL);
+ gtk_widget_set_visible (self->group_install_addons,
+ gtk_widget_get_first_child (self->list_box_install_addons) != NULL);
+ gtk_widget_set_visible (self->group_install_web_apps,
+ gtk_widget_get_first_child (self->list_box_install_web_apps) != NULL);
+}
+
+static GsInstalledPageSection
+gs_installed_page_get_row_section (GsInstalledPage *self,
+ GsAppRow *app_row)
+{
+ GtkWidget *parent;
+
+ g_return_val_if_fail (GS_IS_INSTALLED_PAGE (self), GS_UPDATE_LIST_SECTION_LAST);
+ g_return_val_if_fail (GS_IS_APP_ROW (app_row), GS_UPDATE_LIST_SECTION_LAST);
+
+ parent = gtk_widget_get_parent (GTK_WIDGET (app_row));
+ if (parent == self->list_box_install_in_progress)
+ return GS_UPDATE_LIST_SECTION_INSTALLING_AND_REMOVING;
+ if (parent == self->list_box_install_apps)
+ return GS_UPDATE_LIST_SECTION_REMOVABLE_APPS;
+ if (parent == self->list_box_install_system_apps)
+ return GS_UPDATE_LIST_SECTION_SYSTEM_APPS;
+ if (parent == self->list_box_install_addons)
+ return GS_UPDATE_LIST_SECTION_ADDONS;
+ if (parent == self->list_box_install_web_apps)
+ return GS_UPDATE_LIST_SECTION_WEB_APPS;
+
+ g_warn_if_reached ();
+
+ return GS_UPDATE_LIST_SECTION_LAST;
+}
+
+static void
+gs_installed_page_invalidate (GsInstalledPage *self)
+{
+ self->cache_valid = FALSE;
+}
+
+static void
+gs_installed_page_app_row_activated_cb (GtkListBox *list_box,
+ GtkListBoxRow *row,
+ GsInstalledPage *self)
+{
+ GsApp *app;
+ app = gs_app_row_get_app (GS_APP_ROW (row));
+ gs_shell_show_app (self->shell, app);
+}
+
+static void
+row_unrevealed (GObject *row, GParamSpec *pspec, gpointer data)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (gtk_widget_get_ancestor (GTK_WIDGET (row),
+ GS_TYPE_INSTALLED_PAGE));
+ GtkWidget *list;
+
+ list = gtk_widget_get_parent (GTK_WIDGET (row));
+ if (list == NULL)
+ return;
+ gtk_list_box_remove (GTK_LIST_BOX (list), GTK_WIDGET (row));
+ update_groups (self);
+}
+
+static void
+gs_installed_page_unreveal_row (GsAppRow *app_row)
+{
+ GsApp *app = gs_app_row_get_app (app_row);
+ if (app != NULL) {
+ g_signal_handlers_disconnect_matched (app, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+ G_CALLBACK (gs_installed_page_notify_state_changed_cb), NULL);
+ }
+
+ g_signal_connect (app_row, "unrevealed",
+ G_CALLBACK (row_unrevealed), NULL);
+ gs_app_row_unreveal (app_row);
+}
+
+static GsAppRow * /* (transfer none) */
+gs_installed_page_find_app_row (GsInstalledPage *self,
+ GsApp *app)
+{
+ GtkWidget *lists[] = {
+ self->list_box_install_in_progress,
+ self->list_box_install_apps,
+ self->list_box_install_system_apps,
+ self->list_box_install_addons,
+ self->list_box_install_web_apps,
+ NULL
+ };
+
+ for (gsize i = 0; lists[i]; i++) {
+ for (GtkWidget *child = gtk_widget_get_first_child (lists[i]);
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child)) {
+ GsAppRow *app_row = GS_APP_ROW (child);
+ if (gs_app_row_get_app (app_row) == app) {
+ return app_row;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+
+static void
+gs_installed_page_app_removed (GsPage *page, GsApp *app)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (page);
+ GsAppRow *app_row = gs_installed_page_find_app_row (self, app);
+ if (app_row != NULL)
+ gs_installed_page_unreveal_row (app_row);
+}
+
+static void
+gs_installed_page_app_remove_cb (GsAppRow *app_row,
+ GsInstalledPage *self)
+{
+ GsApp *app;
+
+ app = gs_app_row_get_app (app_row);
+ gs_page_remove_app (GS_PAGE (self), app, self->cancellable);
+}
+
+static void
+gs_installed_page_maybe_move_app_row (GsInstalledPage *self,
+ GsAppRow *app_row)
+{
+ GsInstalledPageSection current_section, expected_section;
+
+ current_section = gs_installed_page_get_row_section (self, app_row);
+ g_return_if_fail (current_section != GS_UPDATE_LIST_SECTION_LAST);
+
+ expected_section = gs_installed_page_get_app_section (gs_app_row_get_app (app_row));
+ if (expected_section != current_section) {
+ GtkWidget *widget = GTK_WIDGET (app_row);
+
+ g_object_ref (app_row);
+ gtk_list_box_remove (GTK_LIST_BOX (gtk_widget_get_parent (widget)), widget);
+ switch (expected_section) {
+ case GS_UPDATE_LIST_SECTION_INSTALLING_AND_REMOVING:
+ widget = self->list_box_install_in_progress;
+ break;
+ case GS_UPDATE_LIST_SECTION_REMOVABLE_APPS:
+ widget = self->list_box_install_apps;
+ break;
+ case GS_UPDATE_LIST_SECTION_SYSTEM_APPS:
+ widget = self->list_box_install_system_apps;
+ break;
+ case GS_UPDATE_LIST_SECTION_ADDONS:
+ widget = self->list_box_install_addons;
+ break;
+ case GS_UPDATE_LIST_SECTION_WEB_APPS:
+ widget = self->list_box_install_web_apps;
+ break;
+ default:
+ g_warn_if_reached ();
+ widget = NULL;
+ break;
+ }
+
+ if (widget != NULL)
+ gtk_list_box_append (GTK_LIST_BOX (widget), GTK_WIDGET (app_row));
+
+ g_object_unref (app_row);
+ update_groups (self);
+ }
+}
+
+static void
+gs_installed_page_notify_state_changed_cb (GsApp *app,
+ GParamSpec *pspec,
+ GsInstalledPage *self)
+{
+ GsAppState state = gs_app_get_state (app);
+ GsAppRow *app_row = gs_installed_page_find_app_row (self, app);
+
+ g_assert (app_row != NULL);
+
+ gtk_list_box_row_changed (GTK_LIST_BOX_ROW (app_row));
+
+ /* Filter which applications can be shown in the installed page */
+ if (state != GS_APP_STATE_INSTALLING &&
+ state != GS_APP_STATE_INSTALLED &&
+ state != GS_APP_STATE_REMOVING &&
+ state != GS_APP_STATE_UPDATABLE &&
+ state != GS_APP_STATE_UPDATABLE_LIVE)
+ gs_installed_page_unreveal_row (app_row);
+ else
+ gs_installed_page_maybe_move_app_row (self, app_row);
+}
+
+static gboolean
+should_show_installed_size (GsInstalledPage *self)
+{
+ return g_settings_get_boolean (self->settings,
+ "installed-page-show-size");
+}
+
+static gboolean
+gs_installed_page_is_actual_app (GsApp *app)
+{
+ if (gs_app_get_description (app) != NULL)
+ return TRUE;
+
+ /* special snowflake */
+ if (g_strcmp0 (gs_app_get_id (app), "google-chrome.desktop") == 0)
+ return TRUE;
+
+ /* web apps sometimes don't have descriptions */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_WEB_APP)
+ return TRUE;
+
+ g_debug ("%s is not an actual app", gs_app_get_unique_id (app));
+ return FALSE;
+}
+
+static void
+gs_installed_page_add_app (GsInstalledPage *self, GsAppList *list, GsApp *app)
+{
+ GtkWidget *app_row;
+
+ /* only show if is an actual application */
+ if (!gs_installed_page_is_actual_app (app))
+ return;
+
+ app_row = g_object_new (GS_TYPE_APP_ROW,
+ "app", app,
+ "show-buttons", TRUE,
+ "show-source", gs_utils_list_has_component_fuzzy (list, app),
+ "show-installed-size", !gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY) && should_show_installed_size (self),
+ NULL);
+
+ g_signal_connect (app_row, "button-clicked",
+ G_CALLBACK (gs_installed_page_app_remove_cb), self);
+ g_signal_connect_object (app, "notify::state",
+ G_CALLBACK (gs_installed_page_notify_state_changed_cb),
+ self, 0);
+
+ switch (gs_installed_page_get_app_section (app)) {
+ case GS_UPDATE_LIST_SECTION_INSTALLING_AND_REMOVING:
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box_install_in_progress), app_row);
+ break;
+ case GS_UPDATE_LIST_SECTION_REMOVABLE_APPS:
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box_install_apps), app_row);
+ break;
+ case GS_UPDATE_LIST_SECTION_SYSTEM_APPS:
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box_install_system_apps), app_row);
+ break;
+ case GS_UPDATE_LIST_SECTION_ADDONS:
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box_install_addons), app_row);
+ break;
+ case GS_UPDATE_LIST_SECTION_WEB_APPS:
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box_install_web_apps), app_row);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ update_groups (self);
+
+ gs_app_row_set_size_groups (GS_APP_ROW (app_row),
+ self->sizegroup_name,
+ self->sizegroup_button_label,
+ self->sizegroup_button_image);
+
+ gs_app_row_set_show_description (GS_APP_ROW (app_row), FALSE);
+ gs_app_row_set_show_source (GS_APP_ROW (app_row), FALSE);
+ g_object_bind_property (self, "is-narrow", app_row, "is-narrow", G_BINDING_SYNC_CREATE);
+}
+
+static void
+gs_installed_page_get_installed_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ guint i;
+ GsApp *app;
+ GsInstalledPage *self = GS_INSTALLED_PAGE (user_data);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) pending = gs_plugin_loader_get_pending (plugin_loader);
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ gtk_spinner_stop (GTK_SPINNER (self->spinner_install));
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), "view");
+
+ self->waiting = FALSE;
+ self->cache_valid = TRUE;
+
+ 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_warning ("failed to get installed apps: %s", error->message);
+ goto out;
+ }
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ app = gs_app_list_index (list, i);
+ gs_installed_page_add_app (self, list, app);
+ }
+out:
+ if (gs_app_list_length (pending) > 0) {
+ plugin_job = gs_plugin_job_refine_new (pending,
+ gs_installed_page_get_refine_flags (self));
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ gs_installed_page_pending_apps_refined_cb,
+ self);
+
+ }
+}
+
+static void
+gs_installed_page_remove_all_cb (GtkWidget *container,
+ GtkWidget *child)
+{
+ if (GS_IS_APP_ROW (child)) {
+ GsApp *app = gs_app_row_get_app (GS_APP_ROW (child));
+ if (app != NULL) {
+ g_signal_handlers_disconnect_matched (app, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
+ G_CALLBACK (gs_installed_page_notify_state_changed_cb), NULL);
+ }
+ } else {
+ g_warn_if_reached ();
+ }
+
+ gtk_list_box_remove (GTK_LIST_BOX (container), child);
+}
+
+static gboolean
+filter_app_kinds_cb (GsApp *app,
+ gpointer user_data)
+{
+ /* Remove invalid apps. */
+ if (!gs_plugin_loader_app_is_valid (app, GS_PLUGIN_REFINE_FLAGS_NONE))
+ return FALSE;
+
+ switch (gs_app_get_kind (app)) {
+ case AS_COMPONENT_KIND_OPERATING_SYSTEM:
+ case AS_COMPONENT_KIND_CODEC:
+ case AS_COMPONENT_KIND_FONT:
+ g_debug ("app invalid as %s: %s",
+ as_component_kind_to_string (gs_app_get_kind (app)),
+ gs_app_get_unique_id (app));
+ return FALSE;
+ default:
+ return TRUE;
+ }
+}
+
+static GsPluginRefineFlags
+gs_installed_page_get_refine_flags (GsInstalledPage *self)
+{
+ GsPluginRefineFlags flags;
+
+ flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING;
+
+ if (should_show_installed_size (self))
+ flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE;
+
+ return flags;
+}
+
+static void
+gs_installed_page_load (GsInstalledPage *self)
+{
+ g_autoptr(GsAppQuery) query = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ if (self->waiting)
+ return;
+ self->waiting = TRUE;
+
+ /* remove old entries */
+ gs_widget_remove_all (self->list_box_install_in_progress, gs_installed_page_remove_all_cb);
+ gs_widget_remove_all (self->list_box_install_apps, gs_installed_page_remove_all_cb);
+ gs_widget_remove_all (self->list_box_install_system_apps, gs_installed_page_remove_all_cb);
+ gs_widget_remove_all (self->list_box_install_addons, gs_installed_page_remove_all_cb);
+ gs_widget_remove_all (self->list_box_install_web_apps, gs_installed_page_remove_all_cb);
+ update_groups (self);
+
+ /* get installed apps */
+ query = gs_app_query_new ("is-installed", GS_APP_QUERY_TRISTATE_TRUE,
+ "refine-flags", gs_installed_page_get_refine_flags (self),
+ "filter-func", filter_app_kinds_cb,
+ NULL);
+ plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
+ gs_plugin_loader_job_process_async (self->plugin_loader,
+ plugin_job,
+ self->cancellable,
+ gs_installed_page_get_installed_cb,
+ self);
+ gtk_spinner_start (GTK_SPINNER (self->spinner_install));
+ gtk_stack_set_visible_child_name (GTK_STACK (self->stack_install), "spinner");
+}
+
+static void
+gs_installed_page_reload (GsPage *page)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (page);
+ gs_installed_page_invalidate (self);
+ gs_installed_page_load (self);
+}
+
+static void
+gs_installed_page_switch_to (GsPage *page)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (page);
+
+ if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_INSTALLED) {
+ g_warning ("Called switch_to(installed) when in mode %s",
+ gs_shell_get_mode_string (self->shell));
+ return;
+ }
+
+ if (gs_shell_get_mode (self->shell) == GS_SHELL_MODE_INSTALLED) {
+ gs_grab_focus_when_mapped (self->scrolledwindow_install);
+ }
+
+ /* no need to refresh */
+ if (self->cache_valid)
+ return;
+
+ gs_installed_page_load (self);
+}
+
+/**
+ * gs_installed_page_get_app_sort_key:
+ *
+ * Get a sort key to achive this:
+ *
+ * 1. state:installing applications
+ * 2. state: applications queued for installing
+ * 3. state:removing applications
+ * 4. kind:normal applications
+ * 5. kind:system applications
+ *
+ * Within each of these groups, they are sorted by the install date and then
+ * by name.
+ **/
+static gchar *
+gs_installed_page_get_app_sort_key (GsApp *app)
+{
+ GString *key;
+ g_autofree gchar *sort_name = NULL;
+
+ key = g_string_sized_new (64);
+
+ /* sort installed, removing, other */
+ switch (gs_app_get_state (app)) {
+ case GS_APP_STATE_INSTALLING:
+ g_string_append (key, "1:");
+ break;
+ case GS_APP_STATE_QUEUED_FOR_INSTALL:
+ g_string_append (key, "2:");
+ break;
+ case GS_APP_STATE_REMOVING:
+ g_string_append (key, "3:");
+ break;
+ default:
+ g_string_append (key, "4:");
+ break;
+ }
+
+ /* sort apps by kind */
+ switch (gs_app_get_kind (app)) {
+ case AS_COMPONENT_KIND_DESKTOP_APP:
+ g_string_append (key, "2:");
+ break;
+ case AS_COMPONENT_KIND_WEB_APP:
+ g_string_append (key, "3:");
+ break;
+ case AS_COMPONENT_KIND_RUNTIME:
+ g_string_append (key, "4:");
+ break;
+ case AS_COMPONENT_KIND_ADDON:
+ g_string_append (key, "5:");
+ break;
+ case AS_COMPONENT_KIND_CODEC:
+ g_string_append (key, "6:");
+ break;
+ case AS_COMPONENT_KIND_FONT:
+ g_string_append (key, "6:");
+ break;
+ case AS_COMPONENT_KIND_INPUT_METHOD:
+ g_string_append (key, "7:");
+ break;
+ default:
+ if (gs_app_get_special_kind (app) == GS_APP_SPECIAL_KIND_OS_UPDATE)
+ g_string_append (key, "1:");
+ else
+ g_string_append (key, "8:");
+ break;
+ }
+
+ /* sort normal, compulsory */
+ if (!gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY))
+ g_string_append (key, "1:");
+ else
+ g_string_append (key, "2:");
+
+ /* finally, sort by short name */
+ if (gs_app_get_name (app) != NULL) {
+ sort_name = gs_utils_sort_key (gs_app_get_name (app));
+ g_string_append (key, sort_name);
+ }
+
+ return g_string_free (key, FALSE);
+}
+
+static gint
+gs_installed_page_sort_func (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ GsApp *a1, *a2;
+ g_autofree gchar *key1 = NULL;
+ g_autofree gchar *key2 = NULL;
+
+ a1 = gs_app_row_get_app (GS_APP_ROW (a));
+ a2 = gs_app_row_get_app (GS_APP_ROW (b));
+ key1 = gs_installed_page_get_app_sort_key (a1);
+ key2 = gs_installed_page_get_app_sort_key (a2);
+
+ /* compare the keys according to the algorithm above */
+ return g_strcmp0 (key1, key2);
+}
+
+static gboolean
+gs_installed_page_has_app (GsInstalledPage *self,
+ GsApp *app)
+{
+ GtkWidget *lists[] = {
+ self->list_box_install_in_progress,
+ self->list_box_install_apps,
+ self->list_box_install_system_apps,
+ self->list_box_install_addons,
+ self->list_box_install_web_apps,
+ NULL
+ };
+
+ for (gsize i = 0; lists[i]; i++) {
+ for (GtkWidget *child = gtk_widget_get_first_child (lists[i]);
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child)) {
+ GsAppRow *app_row = GS_APP_ROW (child);
+ if (gs_app_row_get_app (app_row) == app)
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+gs_installed_page_add_pending_apps (GsInstalledPage *self,
+ GsAppList *list,
+ gboolean should_install)
+{
+ guint pending_apps_count = 0;
+
+ for (guint i = 0; i < gs_app_list_length (list); ++i) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (gs_app_is_installed (app)) {
+ continue;
+ }
+
+ /* never show OS upgrades, we handle the scheduling and
+ * cancellation in GsUpgradeBanner */
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_OPERATING_SYSTEM)
+ continue;
+
+ if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE)
+ gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL);
+
+ if (should_install &&
+ gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL &&
+ gs_plugin_loader_get_network_available (self->plugin_loader) &&
+ !gs_plugin_loader_get_network_metered (self->plugin_loader))
+ gs_page_install_app (GS_PAGE (self), app,
+ GS_SHELL_INTERACTION_FULL,
+ gs_app_get_cancellable (app));
+
+ ++pending_apps_count;
+ if (!gs_installed_page_has_app (self, app))
+ gs_installed_page_add_app (self, list, app);
+ }
+
+ /* update the number of on-going operations */
+ if (pending_apps_count != self->pending_apps_counter) {
+ self->pending_apps_counter = pending_apps_count;
+ g_object_notify (G_OBJECT (self), "counter");
+ }
+}
+
+static void
+gs_installed_page_pending_apps_refined_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ GsInstalledPage *self = GS_INSTALLED_PAGE (user_data);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) 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_warning ("failed to refine pending apps: %s", error->message);
+ return;
+ }
+
+ /* we add the pending apps and install them because this is called after we
+ * populate the page, and there may be pending apps coming from the saved list
+ * (i.e. after loading the saved pending apps from the disk) */
+ gs_installed_page_add_pending_apps (self, list, TRUE);
+}
+
+static void
+gs_installed_page_pending_apps_changed_cb (GsPluginLoader *plugin_loader,
+ GsInstalledPage *self)
+{
+ g_autoptr(GsAppList) pending = gs_plugin_loader_get_pending (plugin_loader);
+ /* we don't call install every time the pending apps list changes because
+ * it may be queued in the plugin loader */
+ gs_installed_page_add_pending_apps (self, pending, FALSE);
+}
+
+static gboolean
+gs_installed_page_setup (GsPage *page,
+ GsShell *shell,
+ GsPluginLoader *plugin_loader,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (page);
+
+ g_return_val_if_fail (GS_IS_INSTALLED_PAGE (self), TRUE);
+
+ self->shell = shell;
+ self->plugin_loader = g_object_ref (plugin_loader);
+ g_signal_connect (self->plugin_loader, "pending-apps-changed",
+ G_CALLBACK (gs_installed_page_pending_apps_changed_cb),
+ self);
+
+ self->cancellable = g_object_ref (cancellable);
+
+ /* setup installed */
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_install_in_progress),
+ gs_installed_page_sort_func,
+ self, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_install_apps),
+ gs_installed_page_sort_func,
+ self, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_install_system_apps),
+ gs_installed_page_sort_func,
+ self, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_install_addons),
+ gs_installed_page_sort_func,
+ self, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box_install_web_apps),
+ gs_installed_page_sort_func,
+ self, NULL);
+ return TRUE;
+}
+
+static void
+gs_installed_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (object);
+
+ switch ((GsInstalledPageProperty) prop_id) {
+ case PROP_IS_NARROW:
+ g_value_set_boolean (value, gs_installed_page_get_is_narrow (self));
+ break;
+ case PROP_VADJUSTMENT:
+ g_value_set_object (value, gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_install)));
+ break;
+ case PROP_TITLE:
+ /* Translators: This is in the context of a list of apps which are installed on the system. */
+ g_value_set_string (value, C_("List of installed apps", "Installed"));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_installed_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsInstalledPage *self = GS_INSTALLED_PAGE (object);
+
+ switch ((GsInstalledPageProperty) prop_id) {
+ case PROP_IS_NARROW:
+ gs_installed_page_set_is_narrow (self, g_value_get_boolean (value));
+ break;
+ case PROP_VADJUSTMENT:
+ case PROP_TITLE:
+ /* Read only. */
+ g_assert_not_reached ();
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_installed_page_dispose (GObject *object)
+{
+ GsInstalledPage *self = GS_INSTALLED_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->settings);
+
+ G_OBJECT_CLASS (gs_installed_page_parent_class)->dispose (object);
+}
+
+static void
+gs_installed_page_class_init (GsInstalledPageClass *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_installed_page_get_property;
+ object_class->set_property = gs_installed_page_set_property;
+ object_class->dispose = gs_installed_page_dispose;
+
+ page_class->app_removed = gs_installed_page_app_removed;
+ page_class->switch_to = gs_installed_page_switch_to;
+ page_class->reload = gs_installed_page_reload;
+ page_class->setup = gs_installed_page_setup;
+
+ /**
+ * GsInstalledPage:is-narrow:
+ *
+ * Whether the page is in narrow mode.
+ *
+ * In narrow mode, the page will take up less horizontal space, doing so
+ * by e.g. using icons rather than labels in buttons. This is needed to
+ * keep the UI useable on small form-factors like smartphones.
+ *
+ * Since: 41
+ */
+ obj_props[PROP_IS_NARROW] =
+ g_param_spec_boolean ("is-narrow", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+
+ g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
+ g_object_class_override_property (object_class, PROP_TITLE, "title");
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-installed-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, group_install_in_progress);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, group_install_apps);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, group_install_system_apps);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, group_install_addons);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, group_install_web_apps);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, list_box_install_in_progress);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, list_box_install_apps);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, list_box_install_system_apps);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, list_box_install_addons);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, list_box_install_web_apps);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, scrolledwindow_install);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, spinner_install);
+ gtk_widget_class_bind_template_child (widget_class, GsInstalledPage, stack_install);
+
+ gtk_widget_class_bind_template_callback (widget_class, gs_installed_page_app_row_activated_cb);
+}
+
+static void
+gs_installed_page_init (GsInstalledPage *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->settings = g_settings_new ("org.gnome.software");
+}
+
+/**
+ * gs_installed_page_get_is_narrow:
+ * @self: a #GsInstalledPage
+ *
+ * Get the value of #GsInstalledPage:is-narrow.
+ *
+ * Returns: %TRUE if the page is in narrow mode, %FALSE otherwise
+ *
+ * Since: 41
+ */
+gboolean
+gs_installed_page_get_is_narrow (GsInstalledPage *self)
+{
+ g_return_val_if_fail (GS_IS_INSTALLED_PAGE (self), FALSE);
+
+ return self->is_narrow;
+}
+
+/**
+ * gs_installed_page_set_is_narrow:
+ * @self: a #GsInstalledPage
+ * @is_narrow: %TRUE to set the page in narrow mode, %FALSE otherwise
+ *
+ * Set the value of #GsInstalledPage:is-narrow.
+ *
+ * Since: 41
+ */
+void
+gs_installed_page_set_is_narrow (GsInstalledPage *self, gboolean is_narrow)
+{
+ g_return_if_fail (GS_IS_INSTALLED_PAGE (self));
+
+ is_narrow = !!is_narrow;
+
+ if (self->is_narrow == is_narrow)
+ return;
+
+ self->is_narrow = is_narrow;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_IS_NARROW]);
+}
+
+GsInstalledPage *
+gs_installed_page_new (void)
+{
+ GsInstalledPage *self;
+ self = g_object_new (GS_TYPE_INSTALLED_PAGE, NULL);
+ return GS_INSTALLED_PAGE (self);
+}