summaryrefslogtreecommitdiffstats
path: root/src/gs-storage-context-dialog.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/gs-storage-context-dialog.c412
1 files changed, 412 insertions, 0 deletions
diff --git a/src/gs-storage-context-dialog.c b/src/gs-storage-context-dialog.c
new file mode 100644
index 0000000..e0ca377
--- /dev/null
+++ b/src/gs-storage-context-dialog.c
@@ -0,0 +1,412 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-storage-context-dialog
+ * @short_description: A dialog showing storage information about an app
+ *
+ * #GsStorageContextDialog is a dialog which shows detailed information
+ * about the download size of an uninstalled app, or the storage usage of
+ * an installed one. It shows how those sizes are broken down into components
+ * such as user data, cached data, or dependencies, where possible.
+ *
+ * It is designed to show a more detailed view of the information which the
+ * app’s storage tile in #GsAppContextBar is derived from.
+ *
+ * The widget has no special appearance if the app is unset, so callers will
+ * typically want to hide the dialog in that case.
+ *
+ * Since: 41
+ */
+
+#include "config.h"
+
+#include <adwaita.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <locale.h>
+
+#include "gs-app.h"
+#include "gs-common.h"
+#include "gs-context-dialog-row.h"
+#include "gs-lozenge.h"
+#include "gs-storage-context-dialog.h"
+
+struct _GsStorageContextDialog
+{
+ GsInfoWindow parent_instance;
+
+ GsApp *app; /* (nullable) (owned) */
+ gulong app_notify_handler;
+
+ GtkSizeGroup *lozenge_size_group;
+ GtkWidget *lozenge;
+ GtkLabel *title;
+ GtkListBox *sizes_list;
+ GtkLabel *manage_storage_label;
+};
+
+G_DEFINE_TYPE (GsStorageContextDialog, gs_storage_context_dialog, GS_TYPE_INFO_WINDOW)
+
+typedef enum {
+ PROP_APP = 1,
+} GsStorageContextDialogProperty;
+
+static GParamSpec *obj_props[PROP_APP + 1] = { NULL, };
+
+typedef enum {
+ MATCH_STATE_NO_MATCH = 0,
+ MATCH_STATE_MATCH = 1,
+ MATCH_STATE_UNKNOWN,
+} MatchState;
+
+/* The arguments are all non-nullable. */
+static void
+add_size_row (GtkListBox *list_box,
+ GtkSizeGroup *lozenge_size_group,
+ GsSizeType size_type,
+ guint64 size_bytes,
+ const gchar *title,
+ const gchar *description)
+{
+ GtkListBoxRow *row;
+ g_autofree gchar *size_bytes_str = NULL;
+ gboolean is_markup = FALSE;
+
+ if (size_type != GS_SIZE_TYPE_VALID)
+ /* Translators: This is shown in a bubble if the storage
+ * size of an application is not known. The bubble is small,
+ * so the string should be as short as possible. */
+ size_bytes_str = g_strdup (_("?"));
+ else if (size_bytes == 0)
+ /* Translators: This is shown in a bubble to represent a 0 byte
+ * storage size, so its context is “storage size: none”. The
+ * bubble is small, so the string should be as short as
+ * possible. */
+ size_bytes_str = g_strdup (_("None"));
+ else
+ size_bytes_str = gs_utils_format_size (size_bytes, &is_markup);
+
+ row = gs_context_dialog_row_new_text (size_bytes_str, GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL,
+ title, description);
+ if (is_markup)
+ gs_context_dialog_row_set_content_markup (GS_CONTEXT_DIALOG_ROW (row), size_bytes_str);
+ gs_context_dialog_row_set_size_groups (GS_CONTEXT_DIALOG_ROW (row), lozenge_size_group, NULL, NULL);
+ gtk_list_box_append (list_box, GTK_WIDGET (row));
+}
+
+static void
+update_sizes_list (GsStorageContextDialog *self)
+{
+ GsSizeType title_size_type;
+ guint64 title_size_bytes;
+ g_autofree gchar *title_size_bytes_str = NULL;
+ const gchar *title;
+ gboolean cache_row_added = FALSE;
+ gboolean is_markup = FALSE;
+
+ gs_widget_remove_all (GTK_WIDGET (self->sizes_list), (GsRemoveFunc) gtk_list_box_remove);
+
+ /* UI state is undefined if app is not set. */
+ if (self->app == NULL)
+ return;
+
+ if (gs_app_is_installed (self->app)) {
+ guint64 size_installed_bytes, size_user_data_bytes, size_cache_data_bytes;
+ GsSizeType size_installed_type, size_user_data_type, size_cache_data_type;
+
+ /* Don’t list the size of the dependencies as that space likely
+ * won’t be reclaimed unless many other apps are removed. */
+ size_installed_type = gs_app_get_size_installed (self->app, &size_installed_bytes);
+ size_user_data_type = gs_app_get_size_user_data (self->app, &size_user_data_bytes);
+ size_cache_data_type = gs_app_get_size_cache_data (self->app, &size_cache_data_bytes);
+
+ title = _("Installed Size");
+ title_size_bytes = size_installed_bytes;
+ title_size_type = size_installed_type;
+
+ add_size_row (self->sizes_list, self->lozenge_size_group,
+ size_installed_type, size_installed_bytes,
+ _("Application Data"),
+ _("Data needed for the application to run"));
+
+ if (size_user_data_type == GS_SIZE_TYPE_VALID) {
+ add_size_row (self->sizes_list, self->lozenge_size_group,
+ size_user_data_type, size_user_data_bytes,
+ _("User Data"),
+ _("Data created by you in the application"));
+ title_size_bytes += size_user_data_bytes;
+ }
+
+ if (size_cache_data_type == GS_SIZE_TYPE_VALID) {
+ add_size_row (self->sizes_list, self->lozenge_size_group,
+ size_cache_data_type, size_cache_data_bytes,
+ _("Cache Data"),
+ _("Temporary cached data"));
+ title_size_bytes += size_cache_data_bytes;
+ cache_row_added = TRUE;
+ }
+ } else {
+ guint64 size_download_bytes, size_download_dependencies_bytes;
+ GsSizeType size_download_type, size_download_dependencies_type;
+
+ size_download_type = gs_app_get_size_download (self->app, &size_download_bytes);
+ size_download_dependencies_type = gs_app_get_size_download_dependencies (self->app, &size_download_dependencies_bytes);
+
+ title = _("Download Size");
+ title_size_bytes = size_download_bytes;
+ title_size_type = size_download_type;
+
+ add_size_row (self->sizes_list, self->lozenge_size_group,
+ size_download_type, size_download_bytes,
+ gs_app_get_name (self->app),
+ _("The application itself"));
+
+ if (size_download_dependencies_type == GS_SIZE_TYPE_VALID) {
+ add_size_row (self->sizes_list, self->lozenge_size_group,
+ size_download_dependencies_type, size_download_dependencies_bytes,
+ _("Required Dependencies"),
+ _("Shared system components required by this application"));
+ title_size_bytes += size_download_dependencies_bytes;
+ }
+
+ /* FIXME: Addons, Potential Additional Downloads */
+ }
+
+ if (title_size_type == GS_SIZE_TYPE_VALID)
+ title_size_bytes_str = gs_utils_format_size (title_size_bytes, &is_markup);
+ else
+ title_size_bytes_str = g_strdup (C_("Download size", "Unknown"));
+
+ if (is_markup)
+ gs_lozenge_set_markup (GS_LOZENGE (self->lozenge), title_size_bytes_str);
+ else
+ gs_lozenge_set_text (GS_LOZENGE (self->lozenge), title_size_bytes_str);
+
+ gtk_label_set_text (self->title, title);
+
+ /* Update the Manage Storage label. */
+ gtk_widget_set_visible (GTK_WIDGET (self->manage_storage_label), cache_row_added);
+}
+
+static void
+app_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsStorageContextDialog *self = GS_STORAGE_CONTEXT_DIALOG (user_data);
+ GQuark pspec_name_quark = g_param_spec_get_name_quark (pspec);
+
+ if (pspec_name_quark == g_quark_from_static_string ("state") ||
+ pspec_name_quark == g_quark_from_static_string ("size-installed") ||
+ pspec_name_quark == g_quark_from_static_string ("size-installed-dependencies") ||
+ pspec_name_quark == g_quark_from_static_string ("size-download") ||
+ pspec_name_quark == g_quark_from_static_string ("size-download-dependencies") ||
+ pspec_name_quark == g_quark_from_static_string ("size-cache-data") ||
+ pspec_name_quark == g_quark_from_static_string ("size-user-data"))
+ update_sizes_list (self);
+}
+
+static gboolean
+manage_storage_activate_link_cb (GtkLabel *label,
+ const gchar *uri,
+ gpointer user_data)
+{
+ GsStorageContextDialog *self = GS_STORAGE_CONTEXT_DIALOG (user_data);
+ g_autoptr(GError) local_error = NULL;
+ const gchar *desktop_id;
+ const gchar *argv[] = {
+ "gnome-control-center",
+ "applications",
+ "", /* application ID */
+ NULL
+ };
+
+ /* Button shouldn’t have been sensitive if the launchable ID isn’t available. */
+ desktop_id = gs_app_get_launchable (self->app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
+ g_assert (desktop_id != NULL);
+
+ argv[2] = desktop_id;
+
+ if (!g_spawn_async (NULL, (gchar **) argv, NULL,
+ G_SPAWN_SEARCH_PATH |
+ G_SPAWN_STDOUT_TO_DEV_NULL |
+ G_SPAWN_STDERR_TO_DEV_NULL |
+ G_SPAWN_CLOEXEC_PIPES,
+ NULL, NULL, NULL, &local_error)) {
+ g_warning ("Error opening GNOME Control Center: %s",
+ local_error->message);
+ return TRUE;
+ }
+
+ return TRUE;
+}
+
+static void
+gs_storage_context_dialog_init (GsStorageContextDialog *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+gs_storage_context_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsStorageContextDialog *self = GS_STORAGE_CONTEXT_DIALOG (object);
+
+ switch ((GsStorageContextDialogProperty) prop_id) {
+ case PROP_APP:
+ g_value_set_object (value, gs_storage_context_dialog_get_app (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_storage_context_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsStorageContextDialog *self = GS_STORAGE_CONTEXT_DIALOG (object);
+
+ switch ((GsStorageContextDialogProperty) prop_id) {
+ case PROP_APP:
+ gs_storage_context_dialog_set_app (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_storage_context_dialog_dispose (GObject *object)
+{
+ GsStorageContextDialog *self = GS_STORAGE_CONTEXT_DIALOG (object);
+
+ gs_storage_context_dialog_set_app (self, NULL);
+
+ G_OBJECT_CLASS (gs_storage_context_dialog_parent_class)->dispose (object);
+}
+
+static void
+gs_storage_context_dialog_class_init (GsStorageContextDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gs_storage_context_dialog_get_property;
+ object_class->set_property = gs_storage_context_dialog_set_property;
+ object_class->dispose = gs_storage_context_dialog_dispose;
+
+ /**
+ * GsStorageContextDialog:app: (nullable)
+ *
+ * The app to display the storage context details for.
+ *
+ * This may be %NULL; if so, the content of the widget will be
+ * undefined.
+ *
+ * Since: 41
+ */
+ obj_props[PROP_APP] =
+ g_param_spec_object ("app", NULL, NULL,
+ GS_TYPE_APP,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ 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-storage-context-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsStorageContextDialog, lozenge_size_group);
+ gtk_widget_class_bind_template_child (widget_class, GsStorageContextDialog, lozenge);
+ gtk_widget_class_bind_template_child (widget_class, GsStorageContextDialog, title);
+ gtk_widget_class_bind_template_child (widget_class, GsStorageContextDialog, sizes_list);
+ gtk_widget_class_bind_template_child (widget_class, GsStorageContextDialog, manage_storage_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, manage_storage_activate_link_cb);
+}
+
+/**
+ * gs_storage_context_dialog_new:
+ * @app: (nullable): the app to display storage context information for, or %NULL
+ *
+ * Create a new #GsStorageContextDialog and set its initial app to @app.
+ *
+ * Returns: (transfer full): a new #GsStorageContextDialog
+ * Since: 41
+ */
+GsStorageContextDialog *
+gs_storage_context_dialog_new (GsApp *app)
+{
+ g_return_val_if_fail (app == NULL || GS_IS_APP (app), NULL);
+
+ return g_object_new (GS_TYPE_STORAGE_CONTEXT_DIALOG,
+ "app", app,
+ NULL);
+}
+
+/**
+ * gs_storage_context_dialog_get_app:
+ * @self: a #GsStorageContextDialog
+ *
+ * Gets the value of #GsStorageContextDialog:app.
+ *
+ * Returns: (nullable) (transfer none): app whose storage context information is
+ * being displayed, or %NULL if none is set
+ * Since: 41
+ */
+GsApp *
+gs_storage_context_dialog_get_app (GsStorageContextDialog *self)
+{
+ g_return_val_if_fail (GS_IS_STORAGE_CONTEXT_DIALOG (self), NULL);
+
+ return self->app;
+}
+
+/**
+ * gs_storage_context_dialog_set_app:
+ * @self: a #GsStorageContextDialog
+ * @app: (nullable) (transfer none): the app to display storage context
+ * information for, or %NULL for none
+ *
+ * Set the value of #GsStorageContextDialog:app.
+ *
+ * Since: 41
+ */
+void
+gs_storage_context_dialog_set_app (GsStorageContextDialog *self,
+ GsApp *app)
+{
+ g_return_if_fail (GS_IS_STORAGE_CONTEXT_DIALOG (self));
+ g_return_if_fail (app == NULL || GS_IS_APP (app));
+
+ if (app == self->app)
+ return;
+
+ g_clear_signal_handler (&self->app_notify_handler, self->app);
+
+ g_set_object (&self->app, app);
+
+ if (self->app != NULL)
+ self->app_notify_handler = g_signal_connect (self->app, "notify", G_CALLBACK (app_notify_cb), self);
+
+ /* Update the UI. */
+ update_sizes_list (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APP]);
+}