diff options
Diffstat (limited to '')
-rw-r--r-- | src/gs-os-update-page.c | 613 |
1 files changed, 613 insertions, 0 deletions
diff --git a/src/gs-os-update-page.c b/src/gs-os-update-page.c new file mode 100644 index 0000000..3b5b979 --- /dev/null +++ b/src/gs-os-update-page.c @@ -0,0 +1,613 @@ +/* -*- 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> + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-os-update-page + * @title: GsOsUpdatePage + * @include: gnome-software.h + * @stability: Stable + * @short_description: A small page showing OS updates + * + * This is a page from #GsUpdateDialog. + */ + +#include "config.h" + +#include <adwaita.h> +#include <glib/gi18n.h> + +#include "gs-os-update-page.h" +#include "gs-common.h" + +typedef enum { + GS_OS_UPDATE_PAGE_SECTION_ADDITIONS, + GS_OS_UPDATE_PAGE_SECTION_REMOVALS, + GS_OS_UPDATE_PAGE_SECTION_UPDATES, + GS_OS_UPDATE_PAGE_SECTION_DOWNGRADES, + GS_OS_UPDATE_PAGE_SECTION_LAST, +} GsOsUpdatePageSection; + +typedef enum { + PROP_APP = 1, + PROP_SHOW_BACK_BUTTON, + PROP_TITLE, +} GsOsUpdatePageProperty; + +enum { + SIGNAL_APP_ACTIVATED, + SIGNAL_BACK_CLICKED, + SIGNAL_LAST +}; + +static GParamSpec *obj_props[PROP_TITLE + 1] = { NULL, }; + +static guint signals[SIGNAL_LAST] = { 0 }; + +struct _GsOsUpdatePage +{ + GtkBox parent_instance; + + GtkWidget *back_button; + GtkWidget *box; + GtkWidget *group; + GtkWidget *header_bar; + AdwWindowTitle *window_title; + + GsApp *app; /* (owned) (nullable) */ + GtkWidget *list_boxes[GS_OS_UPDATE_PAGE_SECTION_LAST]; +}; + +G_DEFINE_TYPE (GsOsUpdatePage, gs_os_update_page, GTK_TYPE_BOX) + +static void +row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsOsUpdatePage *page) +{ + GsApp *app; + + app = GS_APP (g_object_get_data (G_OBJECT (gtk_list_box_row_get_child (row)), "app")); + g_assert (app != NULL); + + g_signal_emit (page, signals[SIGNAL_APP_ACTIVATED], 0, app); +} + +static gchar * +format_version_update (GsApp *app, GtkTextDirection direction) +{ + const gchar *tmp; + const gchar *version_current = NULL; + const gchar *version_update = NULL; + + /* current version */ + tmp = gs_app_get_version (app); + if (tmp != NULL && tmp[0] != '\0') + version_current = tmp; + + /* update version */ + tmp = gs_app_get_update_version (app); + if (tmp != NULL && tmp[0] != '\0') + version_update = tmp; + + /* have both */ + if (version_current != NULL && version_update != NULL && + g_strcmp0 (version_current, version_update) != 0) { + switch (direction) { + case GTK_TEXT_DIR_RTL: + /* ensure the arrow is the right way round for the text direction, + * as arrows are not bidi-mirrored automatically + * See section 2 of http://www.unicode.org/L2/L2017/17438-bidi-math-fdbk.html */ + return g_strdup_printf ("%s ← %s", + version_update, + version_current); + case GTK_TEXT_DIR_NONE: + case GTK_TEXT_DIR_LTR: + default: + return g_strdup_printf ("%s → %s", + version_current, + version_update); + } + } + + /* just update */ + if (version_update) + return g_strdup (version_update); + + /* we have nothing, nada, zilch */ + return NULL; +} + +static GtkWidget * +create_app_row (GsApp *app) +{ + GtkWidget *row, *label; + + row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + g_object_set_data_full (G_OBJECT (row), + "app", + g_object_ref (app), + g_object_unref); + label = gtk_label_new (gs_app_get_source_default (app)); + g_object_set (label, + "margin-start", 20, + "margin-end", 0, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 0.0, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_widget_set_hexpand (label, TRUE); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_box_append (GTK_BOX (row), label); + if (gs_app_get_state (app) == GS_APP_STATE_UPDATABLE || + gs_app_get_state (app) == GS_APP_STATE_UPDATABLE_LIVE) { + g_autofree gchar *verstr = format_version_update (app, gtk_widget_get_direction (row)); + label = gtk_label_new (verstr); + } else { + label = gtk_label_new (gs_app_get_version (app)); + } + g_object_set (label, + "margin-start", 0, + "margin-end", 20, + "margin-top", 6, + "margin-bottom", 6, + "xalign", 1.0, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + gtk_widget_set_halign (label, GTK_ALIGN_END); + gtk_widget_set_valign (label, GTK_ALIGN_CENTER); + gtk_box_append (GTK_BOX (row), label); + + return row; +} + +static gboolean +is_downgrade (const gchar *evr1, + const gchar *evr2) +{ + gint rc; + + if (evr1 == NULL || evr2 == NULL) + return FALSE; + + rc = as_vercmp (evr1, evr2, AS_VERCMP_FLAG_IGNORE_EPOCH); + if (rc != 0) + return rc > 0; + + return FALSE; +} + +static GsOsUpdatePageSection +get_app_section (GsApp *app) +{ + GsOsUpdatePageSection section; + + /* Sections: + * 1. additions + * 2. removals + * 3. updates + * 4. downgrades */ + switch (gs_app_get_state (app)) { + case GS_APP_STATE_AVAILABLE: + section = GS_OS_UPDATE_PAGE_SECTION_ADDITIONS; + break; + case GS_APP_STATE_UNAVAILABLE: + case GS_APP_STATE_INSTALLED: + section = GS_OS_UPDATE_PAGE_SECTION_REMOVALS; + break; + case GS_APP_STATE_UPDATABLE: + case GS_APP_STATE_UPDATABLE_LIVE: + if (is_downgrade (gs_app_get_version (app), + gs_app_get_update_version (app))) + section = GS_OS_UPDATE_PAGE_SECTION_DOWNGRADES; + else + section = GS_OS_UPDATE_PAGE_SECTION_UPDATES; + break; + default: + g_warning ("get_app_section: unhandled state %s for %s", + gs_app_state_to_string (gs_app_get_state (app)), + gs_app_get_unique_id (app)); + section = GS_OS_UPDATE_PAGE_SECTION_UPDATES; + break; + } + + return section; +} + +static gint +os_updates_sort_func (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + GObject *o1 = G_OBJECT (gtk_list_box_row_get_child (a)); + GObject *o2 = G_OBJECT (gtk_list_box_row_get_child (b)); + GsApp *a1 = g_object_get_data (o1, "app"); + GsApp *a2 = g_object_get_data (o2, "app"); + const gchar *key1 = gs_app_get_source_default (a1); + const gchar *key2 = gs_app_get_source_default (a2); + + return g_strcmp0 (key1, key2); +} + +static GtkWidget * +get_section_header (GsOsUpdatePage *page, GsOsUpdatePageSection section) +{ + GtkWidget *header; + GtkWidget *label; + + /* get labels and buttons for everything */ + if (section == GS_OS_UPDATE_PAGE_SECTION_ADDITIONS) { + /* TRANSLATORS: This is the header for package additions during + * a system update */ + label = gtk_label_new (_("Additions")); + } else if (section == GS_OS_UPDATE_PAGE_SECTION_REMOVALS) { + /* TRANSLATORS: This is the header for package removals during + * a system update */ + label = gtk_label_new (_("Removals")); + } else if (section == GS_OS_UPDATE_PAGE_SECTION_UPDATES) { + /* TRANSLATORS: This is the header for package updates during + * a system update */ + label = gtk_label_new (C_("Packages to be updated during a system upgrade", "Updates")); + } else if (section == GS_OS_UPDATE_PAGE_SECTION_DOWNGRADES) { + /* TRANSLATORS: This is the header for package downgrades during + * a system update */ + label = gtk_label_new (_("Downgrades")); + } else { + g_assert_not_reached (); + } + + /* create header */ + header = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3); + gtk_widget_add_css_class (header, "app-listbox-header"); + + /* put label into the header */ + gtk_widget_set_hexpand (label, TRUE); + gtk_box_append (GTK_BOX (header), label); + gtk_widget_set_margin_start (label, 16); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_widget_add_css_class (label, "heading"); + + /* success */ + return header; +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GsOsUpdatePage *page = (GsOsUpdatePage *) user_data; + GObject *o = G_OBJECT (gtk_list_box_row_get_child (row)); + GsApp *app = g_object_get_data (o, "app"); + GtkWidget *header = NULL; + + if (before == NULL) + header = get_section_header (page, get_app_section (app)); + else + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static void +create_section (GsOsUpdatePage *page, GsOsUpdatePageSection section) +{ + GtkWidget *previous = NULL; + + page->list_boxes[section] = gtk_list_box_new (); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (page->list_boxes[section]), + GTK_SELECTION_NONE); + gtk_list_box_set_sort_func (GTK_LIST_BOX (page->list_boxes[section]), + os_updates_sort_func, + page, NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (page->list_boxes[section]), + list_header_func, + page, NULL); + g_signal_connect (GTK_LIST_BOX (page->list_boxes[section]), "row-activated", + G_CALLBACK (row_activated_cb), page); + gtk_box_append (GTK_BOX (page->box), page->list_boxes[section]); + gtk_widget_set_margin_top (page->list_boxes[section], 24); + + /* reorder the children */ + for (guint i = 0; i < GS_OS_UPDATE_PAGE_SECTION_LAST; i++) { + if (page->list_boxes[i] == NULL) + continue; + gtk_box_reorder_child_after (GTK_BOX (page->box), + page->list_boxes[i], + previous); + previous = page->list_boxes[i]; + } + + /* make rounded edges */ + gtk_widget_set_overflow (page->list_boxes[section], GTK_OVERFLOW_HIDDEN); + gtk_widget_add_css_class (page->list_boxes[section], "card"); +} + +/** + * gs_os_update_page_get_app: + * @page: a #GsOsUpdatePage + * + * Get the value of #GsOsUpdatePage:app. + * + * Returns: (nullable) (transfer none): the app + * + * Since: 41 + */ +GsApp * +gs_os_update_page_get_app (GsOsUpdatePage *page) +{ + g_return_val_if_fail (GS_IS_OS_UPDATE_PAGE (page), NULL); + return page->app; +} + +/** + * gs_os_update_page_set_app: + * @page: a #GsOsUpdatePage + * @app: (transfer none) (nullable): new app + * + * Set the value of #GsOsUpdatePage:app. + * + * Since: 41 + */ +void +gs_os_update_page_set_app (GsOsUpdatePage *page, GsApp *app) +{ + GsAppList *related; + GsApp *app_related; + GsOsUpdatePageSection section; + GtkWidget *row; + + g_return_if_fail (GS_IS_OS_UPDATE_PAGE (page)); + g_return_if_fail (!app || GS_IS_APP (app)); + + if (page->app == app) + return; + + g_set_object (&page->app, app); + + /* clear existing data */ + for (guint i = 0; i < GS_OS_UPDATE_PAGE_SECTION_LAST; i++) { + if (page->list_boxes[i] == NULL) + continue; + gs_widget_remove_all (page->list_boxes[i], (GsRemoveFunc) gtk_list_box_remove); + } + + if (app) { + adw_window_title_set_title (page->window_title, gs_app_get_name (app)); + adw_preferences_group_set_description (ADW_PREFERENCES_GROUP (page->group), + gs_app_get_description (app)); + + /* add new apps */ + related = gs_app_get_related (app); + for (guint i = 0; i < gs_app_list_length (related); i++) { + app_related = gs_app_list_index (related, i); + + section = get_app_section (app_related); + if (page->list_boxes[section] == NULL) + create_section (page, section); + + row = create_app_row (app_related); + gtk_list_box_append (GTK_LIST_BOX (page->list_boxes[section]), row); + } + } else { + adw_window_title_set_title (page->window_title, NULL); + adw_preferences_group_set_description (ADW_PREFERENCES_GROUP (page->group), NULL); + } + + g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_APP]); + g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_TITLE]); +} + +/** + * gs_os_update_page_get_show_back_button: + * @page: a #GsOsUpdatePage + * + * Get the value of #GsOsUpdatePage:show-back-button. + * + * Returns: whether to show the back button + * + * Since: 42.1 + */ +gboolean +gs_os_update_page_get_show_back_button (GsOsUpdatePage *page) +{ + g_return_val_if_fail (GS_IS_OS_UPDATE_PAGE (page), FALSE); + return gtk_widget_get_visible (page->back_button); +} + +/** + * gs_os_update_page_set_show_back_button: + * @page: a #GsOsUpdatePage + * @show_back_button: whether to show the back button + * + * Set the value of #GsOsUpdatePage:show-back-button. + * + * Since: 42.1 + */ +void +gs_os_update_page_set_show_back_button (GsOsUpdatePage *page, + gboolean show_back_button) +{ + g_return_if_fail (GS_IS_OS_UPDATE_PAGE (page)); + + show_back_button = !!show_back_button; + + if (gtk_widget_get_visible (page->back_button) == show_back_button) + return; + + gtk_widget_set_visible (page->back_button, show_back_button); + + g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_SHOW_BACK_BUTTON]); +} + +static void +back_clicked_cb (GtkWidget *widget, + GsOsUpdatePage *page) +{ + g_signal_emit (page, signals[SIGNAL_BACK_CLICKED], 0); +} + +static void +gs_os_update_page_dispose (GObject *object) +{ + GsOsUpdatePage *page = GS_OS_UPDATE_PAGE (object); + + g_clear_object (&page->app); + + G_OBJECT_CLASS (gs_os_update_page_parent_class)->dispose (object); +} + +static void +gs_os_update_page_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsOsUpdatePage *page = GS_OS_UPDATE_PAGE (object); + + switch ((GsOsUpdatePageProperty) prop_id) { + case PROP_APP: + g_value_set_object (value, gs_os_update_page_get_app (page)); + break; + case PROP_SHOW_BACK_BUTTON: + g_value_set_boolean (value, gs_os_update_page_get_show_back_button (page)); + break; + case PROP_TITLE: + g_value_set_string (value, adw_window_title_get_title (page->window_title)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_os_update_page_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsOsUpdatePage *page = GS_OS_UPDATE_PAGE (object); + + switch ((GsOsUpdatePageProperty) prop_id) { + case PROP_APP: + gs_os_update_page_set_app (page, g_value_get_object (value)); + break; + case PROP_SHOW_BACK_BUTTON: + gs_os_update_page_set_show_back_button (page, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_os_update_page_init (GsOsUpdatePage *page) +{ + gtk_widget_init_template (GTK_WIDGET (page)); +} + +static void +gs_os_update_page_class_init (GsOsUpdatePageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gs_os_update_page_dispose; + object_class->get_property = gs_os_update_page_get_property; + object_class->set_property = gs_os_update_page_set_property; + + /** + * GsOsUpdatePage:app: (nullable) + * + * The app to present. + * + * Since: 41 + */ + obj_props[PROP_APP] = + g_param_spec_object ("app", NULL, NULL, + GS_TYPE_APP, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsOsUpdatePage:show-back-button + * + * Whether to show the back button. + * + * Since: 42.1 + */ + obj_props[PROP_SHOW_BACK_BUTTON] = + g_param_spec_boolean ("show-back-button", NULL, NULL, + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GsOsUpdatePage:title + * + * Read-only window title. + * + * Since: 42 + */ + obj_props[PROP_TITLE] = + g_param_spec_string ("title", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); + + /** + * GsOsUpdatePage:app-activated: + * @app: a #GsApp + * + * Emitted when an app listed in this page got activated and the + * #GsUpdateDialog containing this page is expected to present its + * details via a #GsAppDetailsPage. + * + * Since: 41 + */ + signals[SIGNAL_APP_ACTIVATED] = + g_signal_new ("app-activated", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 1, GS_TYPE_APP); + + /** + * GsOsUpdatePage::back-clicked: + * @self: a #GsOsUpdatePage + * + * Emitted when the back button got activated and the #GsUpdateDialog + * containing this page is expected to go back. + * + * Since: 42.1 + */ + signals[SIGNAL_BACK_CLICKED] = + g_signal_new ("back-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + 0, NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-os-update-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsOsUpdatePage, back_button); + gtk_widget_class_bind_template_child (widget_class, GsOsUpdatePage, box); + gtk_widget_class_bind_template_child (widget_class, GsOsUpdatePage, group); + gtk_widget_class_bind_template_child (widget_class, GsOsUpdatePage, header_bar); + gtk_widget_class_bind_template_child (widget_class, GsOsUpdatePage, window_title); + gtk_widget_class_bind_template_callback (widget_class, back_clicked_cb); +} + +/** + * gs_os_update_page_new: + * + * Create a new #GsOsUpdatePage. + * + * Returns: (transfer full): a new #GsOsUpdatePage + * Since: 41 + */ +GtkWidget * +gs_os_update_page_new (void) +{ + return GTK_WIDGET (g_object_new (GS_TYPE_OS_UPDATE_PAGE, NULL)); +} |