summaryrefslogtreecommitdiffstats
path: root/src/gs-updates-section.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-updates-section.c')
-rw-r--r--src/gs-updates-section.c733
1 files changed, 733 insertions, 0 deletions
diff --git a/src/gs-updates-section.c b/src/gs-updates-section.c
new file mode 100644
index 0000000..68d818a
--- /dev/null
+++ b/src/gs-updates-section.c
@@ -0,0 +1,733 @@
+/* -*- 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) 2014-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include "gs-app-list-private.h"
+#include "gs-app-row.h"
+#include "gs-page.h"
+#include "gs-common.h"
+#include "gs-progress-button.h"
+#include "gs-update-dialog.h"
+#include "gs-updates-section.h"
+#include "gs-utils.h"
+
+struct _GsUpdatesSection
+{
+ GtkBox parent_instance;
+
+ GtkWidget *button_cancel;
+ GtkWidget *button_download;
+ GtkWidget *button_stack;
+ GtkWidget *button_update;
+ GtkWidget *description;
+ GtkWidget *listbox;
+ GtkWidget *listbox_box;
+ GtkWidget *section_header;
+ GtkWidget *title;
+
+ GsAppList *list;
+ GsUpdatesSectionKind kind;
+ GCancellable *cancellable;
+ GsPage *page; /* (transfer none) */
+ GsPluginLoader *plugin_loader;
+ GtkSizeGroup *sizegroup_name;
+ GtkSizeGroup *sizegroup_button_label;
+ GtkSizeGroup *sizegroup_button_image;
+ GtkSizeGroup *sizegroup_header;
+ gboolean is_narrow;
+};
+
+G_DEFINE_TYPE (GsUpdatesSection, gs_updates_section, GTK_TYPE_BOX)
+
+typedef enum {
+ PROP_IS_NARROW = 1,
+} GsUpdatesSectionProperty;
+
+static GParamSpec *obj_props[PROP_IS_NARROW + 1] = { NULL, };
+
+GsAppList *
+gs_updates_section_get_list (GsUpdatesSection *self)
+{
+ return self->list;
+}
+
+static gboolean
+_listbox_keynav_failed_cb (GsUpdatesSection *self, GtkDirectionType direction, GtkListBox *listbox)
+{
+ GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (listbox));
+
+ if (!root)
+ return FALSE;
+
+ if (direction != GTK_DIR_UP && direction != GTK_DIR_DOWN)
+ return FALSE;
+
+ return gtk_widget_child_focus (GTK_WIDGET (root), direction == GTK_DIR_UP ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD);
+}
+
+static void
+_app_row_button_clicked_cb (GsAppRow *app_row, GsUpdatesSection *self)
+{
+ GsApp *app = gs_app_row_get_app (app_row);
+ if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE)
+ return;
+ gs_page_update_app (GS_PAGE (self->page), app, gs_app_get_cancellable (app));
+}
+
+static void
+_row_unrevealed_cb (GObject *row, GParamSpec *pspec, gpointer data)
+{
+ GtkWidget *widget;
+ GsUpdatesSection *self;
+
+ widget = gtk_widget_get_parent (GTK_WIDGET (row));
+ if (widget == NULL)
+ return;
+
+ widget = gtk_widget_get_ancestor (GTK_WIDGET (row), GS_TYPE_UPDATES_SECTION);
+ g_return_if_fail (GS_IS_UPDATES_SECTION (widget));
+ self = GS_UPDATES_SECTION (widget);
+
+ gs_app_list_remove (self->list, gs_app_row_get_app (GS_APP_ROW (row)));
+
+ gtk_list_box_remove (GTK_LIST_BOX (self->listbox), GTK_WIDGET (row));
+
+ if (!gs_app_list_length (self->list))
+ gtk_widget_hide (widget);
+}
+
+static void
+_unreveal_row (GsAppRow *app_row)
+{
+ g_signal_connect (app_row, "unrevealed",
+ G_CALLBACK (_row_unrevealed_cb), NULL);
+ gs_app_row_unreveal (app_row);
+}
+
+static void
+_app_state_notify_cb (GsApp *app, GParamSpec *pspec, gpointer user_data)
+{
+ if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED) {
+ GsAppRow *app_row = GS_APP_ROW (user_data);
+ _unreveal_row (app_row);
+ }
+}
+
+void
+gs_updates_section_add_app (GsUpdatesSection *self, GsApp *app)
+{
+ GtkWidget *app_row;
+ app_row = gs_app_row_new (app);
+ gs_app_row_set_show_description (GS_APP_ROW (app_row), FALSE);
+ gs_app_row_set_show_update (GS_APP_ROW (app_row), TRUE);
+ gs_app_row_set_show_buttons (GS_APP_ROW (app_row), TRUE);
+ g_signal_connect (app_row, "button-clicked",
+ G_CALLBACK (_app_row_button_clicked_cb),
+ self);
+ gtk_list_box_append (GTK_LIST_BOX (self->listbox), app_row);
+ gs_app_list_add (self->list, app);
+
+ gs_app_row_set_size_groups (GS_APP_ROW (app_row),
+ self->sizegroup_name,
+ self->sizegroup_button_label,
+ self->sizegroup_button_image);
+ g_signal_connect_object (app, "notify::state",
+ G_CALLBACK (_app_state_notify_cb),
+ app_row, 0);
+ g_object_bind_property (G_OBJECT (self), "is-narrow",
+ app_row, "is-narrow",
+ G_BINDING_SYNC_CREATE);
+ gtk_widget_show (GTK_WIDGET (self));
+}
+
+void
+gs_updates_section_remove_all (GsUpdatesSection *self)
+{
+ GtkWidget *child;
+ while ((child = gtk_widget_get_first_child (self->listbox)) != NULL)
+ gtk_list_box_remove (GTK_LIST_BOX (self->listbox), child);
+ gs_app_list_remove_all (self->list);
+ gtk_widget_hide (GTK_WIDGET (self));
+}
+
+typedef struct {
+ GsUpdatesSection *self;
+ gboolean do_reboot;
+ gboolean do_reboot_notification;
+} GsUpdatesSectionUpdateHelper;
+
+static gchar *
+_get_app_sort_key (GsApp *app)
+{
+ GString *key;
+ g_autofree gchar *sort_name = NULL;
+
+ key = g_string_sized_new (64);
+
+ /* 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;
+ }
+
+ /* 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
+_list_sort_func (GtkListBoxRow *a, GtkListBoxRow *b, gpointer user_data)
+{
+ GsApp *a1 = gs_app_row_get_app (GS_APP_ROW (a));
+ GsApp *a2 = gs_app_row_get_app (GS_APP_ROW (b));
+ g_autofree gchar *key1 = _get_app_sort_key (a1);
+ g_autofree gchar *key2 = _get_app_sort_key (a2);
+
+ /* compare the keys according to the algorithm above */
+ return g_strcmp0 (key1, key2);
+}
+
+static void
+_update_helper_free (GsUpdatesSectionUpdateHelper *helper)
+{
+ g_object_unref (helper->self);
+ g_free (helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsUpdatesSectionUpdateHelper, _update_helper_free);
+
+static void
+_cancel_trigger_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ GsUpdatesSection *self = GS_UPDATES_SECTION (user_data);
+ g_autoptr(GError) error = NULL;
+ if (!gs_plugin_loader_job_action_finish (self->plugin_loader, res, &error)) {
+ g_warning ("failed to cancel trigger: %s", error->message);
+ return;
+ }
+}
+
+static void
+_reboot_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ GsUpdatesSection *self = GS_UPDATES_SECTION (user_data);
+ g_autoptr(GError) error = NULL;
+ GsApp *app = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* get result */
+ if (gs_utils_invoke_reboot_finish (source, res, &error))
+ return;
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_debug ("Calling reboot had been cancelled");
+ else if (error != NULL)
+ g_warning ("Calling reboot failed: %s", error->message);
+
+ /* cancel trigger */
+ app = gs_app_list_index (self->list, 0);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE_CANCEL,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ gs_app_get_cancellable (app),
+ _cancel_trigger_failed_cb,
+ self);
+}
+
+static gboolean
+_all_offline_updates_downloaded (GsUpdatesSection *self)
+{
+ /* use the download size to figure out what is downloaded and what not */
+ for (guint i = 0; i < gs_app_list_length (self->list); i++) {
+ GsApp *app = gs_app_list_index (self->list, i);
+ if (!gs_app_is_downloaded (app))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+_update_buttons (GsUpdatesSection *self)
+{
+ /* operation in progress */
+ if (self->cancellable != NULL) {
+ gtk_widget_set_sensitive (self->button_cancel,
+ !g_cancellable_is_cancelled (self->cancellable));
+ gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), "cancel");
+ gtk_widget_show (GTK_WIDGET (self->button_stack));
+ return;
+ }
+
+ if (self->kind == GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE ||
+ self->kind == GS_UPDATES_SECTION_KIND_OFFLINE) {
+ if (_all_offline_updates_downloaded (self))
+ gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), "update");
+ else
+ gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), "download");
+
+ gtk_widget_show (GTK_WIDGET (self->button_stack));
+ /* TRANSLATORS: This is the button for installing all
+ * offline updates */
+ gs_progress_button_set_label (GS_PROGRESS_BUTTON (self->button_update), _("Restart & Update"));
+ } else if (self->kind == GS_UPDATES_SECTION_KIND_ONLINE) {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->button_stack), "update");
+ gtk_widget_show (GTK_WIDGET (self->button_stack));
+ /* TRANSLATORS: This is the button for upgrading all
+ * online-updatable applications */
+ gs_progress_button_set_label (GS_PROGRESS_BUTTON (self->button_update), _("Update All"));
+ } else {
+ gtk_widget_hide (GTK_WIDGET (self->button_stack));
+ }
+
+}
+
+static void
+_perform_update_cb (GsPluginLoader *plugin_loader, GAsyncResult *res, gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsUpdatesSectionUpdateHelper) helper = (GsUpdatesSectionUpdateHelper *) user_data;
+ GsUpdatesSection *self = helper->self;
+
+ /* get the results */
+ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ GsApp *app = NULL;
+
+ if (gs_app_list_length (self->list) == 1)
+ app = gs_app_list_index (self->list, 0);
+
+ gs_plugin_loader_claim_error (plugin_loader,
+ NULL,
+ GS_PLUGIN_ACTION_UPDATE,
+ app,
+ TRUE,
+ error);
+ }
+ goto out;
+ }
+
+ /* trigger reboot if any application was not updatable live */
+ if (helper->do_reboot) {
+ gs_utils_invoke_reboot_async (NULL, _reboot_failed_cb, self);
+
+ /* when we are not doing an offline update, show a notification
+ * if any application requires a reboot */
+ } else if (helper->do_reboot_notification) {
+ gs_utils_reboot_notify (self->list, TRUE);
+ }
+
+out:
+ g_clear_object (&self->cancellable);
+ _update_buttons (self);
+}
+
+static void
+_button_cancel_clicked_cb (GsUpdatesSection *self)
+{
+ g_cancellable_cancel (self->cancellable);
+ _update_buttons (self);
+}
+
+static void
+_download_finished_cb (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ g_autoptr(GsUpdatesSection) self = (GsUpdatesSection *) user_data;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) list = NULL;
+
+ /* get result */
+ list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), 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 download updates: %s", error->message);
+ }
+
+ g_clear_object (&self->cancellable);
+ _update_buttons (self);
+}
+
+static void
+_button_download_clicked_cb (GsUpdatesSection *self)
+{
+ g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ g_set_object (&self->cancellable, cancellable);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DOWNLOAD,
+ "list", self->list,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE,
+ "interactive", TRUE,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ (GAsyncReadyCallback) _download_finished_cb,
+ g_object_ref (self));
+ _update_buttons (self);
+}
+
+static void
+_button_update_all_clicked_cb (GsUpdatesSection *self)
+{
+ g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsUpdatesSectionUpdateHelper *helper = g_new0 (GsUpdatesSectionUpdateHelper, 1);
+
+ helper->self = g_object_ref (self);
+
+ /* look at each app in turn */
+ for (guint i = 0; i < gs_app_list_length (self->list); i++) {
+ GsApp *app = gs_app_list_index (self->list, i);
+ if (gs_app_get_state (app) == GS_APP_STATE_UPDATABLE)
+ helper->do_reboot = TRUE;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT))
+ helper->do_reboot_notification = TRUE;
+ }
+
+ g_set_object (&self->cancellable, cancellable);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE,
+ "list", self->list,
+ "interactive", TRUE,
+ "propagate-error", TRUE,
+ NULL);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ (GAsyncReadyCallback) _perform_update_cb,
+ helper);
+ _update_buttons (self);
+}
+
+static void
+_setup_section_header (GsUpdatesSection *self)
+{
+ /* get labels and buttons for everything */
+ switch (self->kind) {
+ case GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE:
+ /* TRANSLATORS: This is the header for system firmware that
+ * requires a reboot to apply */
+ gtk_label_set_label (GTK_LABEL (self->title), _("Integrated Firmware"));
+ break;
+ case GS_UPDATES_SECTION_KIND_OFFLINE:
+ /* TRANSLATORS: This is the header for offline OS and offline
+ * app updates that require a reboot to apply */
+ gtk_label_set_label (GTK_LABEL (self->title), _("Requires Restart"));
+ break;
+ case GS_UPDATES_SECTION_KIND_ONLINE:
+ /* TRANSLATORS: This is the header for online runtime and
+ * app updates, typically flatpaks or snaps */
+ gtk_label_set_label (GTK_LABEL (self->title), _("Application Updates"));
+ break;
+ case GS_UPDATES_SECTION_KIND_ONLINE_FIRMWARE:
+ /* TRANSLATORS: This is the header for device firmware that can
+ * be installed online */
+ gtk_label_set_label (GTK_LABEL (self->title), _("Device Firmware"));
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+_app_row_activated_cb (GsUpdatesSection *self, GtkListBoxRow *row)
+{
+ GsApp *app = gs_app_row_get_app (GS_APP_ROW (row));
+ GtkWidget *dialog;
+ g_autofree gchar *str = NULL;
+
+ /* debug */
+ str = gs_app_to_string (app);
+ g_debug ("%s", str);
+
+ dialog = gs_update_dialog_new_for_app (self->plugin_loader, app);
+ gs_shell_modal_dialog_present (gs_page_get_shell (self->page), GTK_WINDOW (dialog));
+}
+
+static void
+gs_updates_section_show (GtkWidget *widget)
+{
+ _update_buttons (GS_UPDATES_SECTION (widget));
+
+ GTK_WIDGET_CLASS (gs_updates_section_parent_class)->show (widget);
+}
+
+static void
+gs_updates_section_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsUpdatesSection *self = GS_UPDATES_SECTION (object);
+
+ switch ((GsUpdatesSectionProperty) prop_id) {
+ case PROP_IS_NARROW:
+ g_value_set_boolean (value, gs_updates_section_get_is_narrow (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_updates_section_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsUpdatesSection *self = GS_UPDATES_SECTION (object);
+
+ switch ((GsUpdatesSectionProperty) prop_id) {
+ case PROP_IS_NARROW:
+ gs_updates_section_set_is_narrow (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_updates_section_dispose (GObject *object)
+{
+ GsUpdatesSection *self = GS_UPDATES_SECTION (object);
+
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->list);
+ g_clear_object (&self->plugin_loader);
+ g_clear_object (&self->sizegroup_name);
+ g_clear_object (&self->sizegroup_button_label);
+ g_clear_object (&self->sizegroup_button_image);
+ g_clear_object (&self->sizegroup_header);
+ self->button_download = NULL;
+ self->button_update = NULL;
+ self->button_cancel = NULL;
+ self->button_stack = NULL;
+ self->page = NULL;
+
+ G_OBJECT_CLASS (gs_updates_section_parent_class)->dispose (object);
+}
+
+static void
+gs_updates_section_class_init (GsUpdatesSectionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gs_updates_section_get_property;
+ object_class->set_property = gs_updates_section_set_property;
+ object_class->dispose = gs_updates_section_dispose;
+ widget_class->show = gs_updates_section_show;
+
+ /**
+ * GsUpdatesSection:is-narrow:
+ *
+ * Whether the section is in narrow mode.
+ *
+ * In narrow mode, the section 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);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-updates-section.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, button_cancel);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, button_download);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, button_stack);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, button_update);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, description);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, listbox);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, listbox_box);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, section_header);
+ gtk_widget_class_bind_template_child (widget_class, GsUpdatesSection, title);
+ gtk_widget_class_bind_template_callback (widget_class, _app_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, _button_cancel_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, _button_download_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, _button_update_all_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, _listbox_keynav_failed_cb);
+}
+
+void
+gs_updates_section_set_size_groups (GsUpdatesSection *self,
+ GtkSizeGroup *name,
+ GtkSizeGroup *button_label,
+ GtkSizeGroup *button_image,
+ GtkSizeGroup *header)
+{
+ g_return_if_fail (GS_IS_UPDATES_SECTION (self));
+
+ g_set_object (&self->sizegroup_name, name);
+ g_set_object (&self->sizegroup_button_label, button_label);
+ g_set_object (&self->sizegroup_button_image, button_image);
+ g_set_object (&self->sizegroup_header, header);
+
+ gs_progress_button_set_size_groups (GS_PROGRESS_BUTTON (self->button_cancel), button_label, button_image);
+ gs_progress_button_set_size_groups (GS_PROGRESS_BUTTON (self->button_download), button_label, button_image);
+ gs_progress_button_set_size_groups (GS_PROGRESS_BUTTON (self->button_update), button_label, button_image);
+ gtk_size_group_add_widget (self->sizegroup_header, self->section_header);
+}
+
+static void
+gs_updates_section_progress_notify_cb (GsAppList *list,
+ GParamSpec *pspec,
+ GsUpdatesSection *self)
+{
+ if (self->button_cancel == NULL)
+ return;
+
+ gs_progress_button_set_progress (GS_PROGRESS_BUTTON (self->button_cancel),
+ gs_app_list_get_progress (list));
+}
+
+static void
+gs_updates_section_app_state_changed_cb (GsAppList *list,
+ GsApp *in_app,
+ GsUpdatesSection *self)
+{
+ guint ii, len, busy = 0;
+
+ len = gs_app_list_length (list);
+
+ for (ii = 0; ii < len; ii++) {
+ GsApp *app = gs_app_list_index (list, ii);
+ GsAppState state = gs_app_get_state (app);
+
+ if (state == GS_APP_STATE_INSTALLING ||
+ state == GS_APP_STATE_REMOVING) {
+ busy++;
+ }
+ }
+
+ gtk_widget_set_sensitive (self->button_update, busy < len);
+}
+
+static void
+gs_updates_section_init (GsUpdatesSection *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->list = gs_app_list_new ();
+ gs_app_list_add_flag (self->list,
+ GS_APP_LIST_FLAG_WATCH_APPS |
+ GS_APP_LIST_FLAG_WATCH_APPS_ADDONS |
+ GS_APP_LIST_FLAG_WATCH_APPS_RELATED);
+ g_signal_connect_object (self->list, "notify::progress",
+ G_CALLBACK (gs_updates_section_progress_notify_cb),
+ self, 0);
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (self->listbox),
+ GTK_SELECTION_NONE);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->listbox),
+ _list_sort_func,
+ self, NULL);
+}
+
+/**
+ * gs_updates_section_get_is_narrow:
+ * @self: a #GsUpdatesSection
+ *
+ * Get the value of #GsUpdatesSection:is-narrow.
+ *
+ * Returns: %TRUE if the section is in narrow mode, %FALSE otherwise
+ *
+ * Since: 41
+ */
+gboolean
+gs_updates_section_get_is_narrow (GsUpdatesSection *self)
+{
+ g_return_val_if_fail (GS_IS_UPDATES_SECTION (self), FALSE);
+
+ return self->is_narrow;
+}
+
+/**
+ * gs_updates_section_set_is_narrow:
+ * @self: a #GsUpdatesSection
+ * @is_narrow: %TRUE to set the section in narrow mode, %FALSE otherwise
+ *
+ * Set the value of #GsUpdatesSection:is-narrow.
+ *
+ * Since: 41
+ */
+void
+gs_updates_section_set_is_narrow (GsUpdatesSection *self, gboolean is_narrow)
+{
+ g_return_if_fail (GS_IS_UPDATES_SECTION (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]);
+}
+
+GsUpdatesSection *
+gs_updates_section_new (GsUpdatesSectionKind kind,
+ GsPluginLoader *plugin_loader,
+ GsPage *page)
+{
+ GsUpdatesSection *self;
+ self = g_object_new (GS_TYPE_UPDATES_SECTION, NULL);
+ self->kind = kind;
+ self->plugin_loader = g_object_ref (plugin_loader);
+ self->page = page;
+ _setup_section_header (self);
+
+ if (self->kind == GS_UPDATES_SECTION_KIND_ONLINE) {
+ g_signal_connect_object (self->list, "app-state-changed",
+ G_CALLBACK (gs_updates_section_app_state_changed_cb),
+ self, 0);
+ }
+
+ return self;
+}