[gnome-software: 3/5] gs-storage-context-dialog: Add a new dialog for storage information




commit 0c57328e1a637badd58a6d93f37cf90e12325497
Author: Philip Withnall <pwithnall endlessos org>
Date:   Mon Aug 9 17:17:29 2021 +0100

    gs-storage-context-dialog: Add a new dialog for storage information
    
    This will show more detailed information about the download size or
    installed storage requirements of an app.
    
    Signed-off-by: Philip Withnall <pwithnall endlessos org>
    
    Helps: #1353

 po/POTFILES.in                   |   2 +
 src/gnome-software.gresource.xml |   1 +
 src/gs-storage-context-dialog.c  | 382 +++++++++++++++++++++++++++++++++++++++
 src/gs-storage-context-dialog.h  |  31 ++++
 src/gs-storage-context-dialog.ui | 128 +++++++++++++
 src/meson.build                  |   1 +
 6 files changed, 545 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 68d98bc22..cd69cc311 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -78,6 +78,8 @@ src/gs-shell.c
 src/gs-shell.ui
 src/gs-shell-search-provider.c
 src/gs-star-widget.ui
+src/gs-storage-context-dialog.c
+src/gs-storage-context-dialog.ui
 src/gs-summary-tile.c
 src/gs-summary-tile.ui
 src/gs-third-party-repo-row.c
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index 080376cec..b09d28d3d 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -39,6 +39,7 @@
   <file preprocess="xml-stripblanks">gs-search-page.ui</file>
   <file preprocess="xml-stripblanks">gs-shell.ui</file>
   <file preprocess="xml-stripblanks">gs-star-widget.ui</file>
+  <file preprocess="xml-stripblanks">gs-storage-context-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-summary-tile.ui</file>
   <file preprocess="xml-stripblanks">gs-third-party-repo-row.ui</file>
   <file preprocess="xml-stripblanks">gs-update-dialog.ui</file>
diff --git a/src/gs-storage-context-dialog.c b/src/gs-storage-context-dialog.c
new file mode 100644
index 000000000..42e887f8a
--- /dev/null
+++ b/src/gs-storage-context-dialog.c
@@ -0,0 +1,382 @@
+/* -*- 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 <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <handy.h>
+#include <locale.h>
+
+#include "gs-app.h"
+#include "gs-common.h"
+#include "gs-context-dialog-row.h"
+#include "gs-storage-context-dialog.h"
+
+struct _GsStorageContextDialog
+{
+       HdyWindow                parent_instance;
+
+       GsApp                   *app;  /* (nullable) (owned) */
+       gulong                   app_notify_handler;
+
+       GtkSizeGroup            *lozenge_size_group;
+       GtkLabel                *lozenge_content;
+       GtkWidget               *lozenge;
+       GtkLabel                *title;
+       GtkListBox              *sizes_list;
+};
+
+G_DEFINE_TYPE (GsStorageContextDialog, gs_storage_context_dialog, HDY_TYPE_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,
+              guint64       size_bytes,
+              const gchar  *title,
+              const gchar  *description)
+{
+       GtkListBoxRow *row;
+       g_autofree gchar *size_bytes_str = NULL;
+
+       if (size_bytes == GS_APP_SIZE_UNKNOWABLE)
+               /* 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 = g_format_size (size_bytes);
+
+       row = gs_context_dialog_row_new_text (size_bytes_str, GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL,
+                                             title, description);
+       gs_context_dialog_row_set_size_groups (GS_CONTEXT_DIALOG_ROW (row), lozenge_size_group, NULL, NULL);
+       gtk_list_box_insert (list_box, GTK_WIDGET (row), -1);
+}
+
+static void
+update_sizes_list (GsStorageContextDialog *self)
+{
+       guint64 title_size_bytes;
+       g_autofree gchar *title_size_bytes_str = NULL;
+       const gchar *title;
+
+       gs_container_remove_all (GTK_CONTAINER (self->sizes_list));
+
+       /* UI state is undefined if app is not set. */
+       if (self->app == NULL)
+               return;
+
+       if (gs_app_is_installed (self->app)) {
+               guint64 size_installed;
+               guint64 size_user_data;
+               guint64 size_cache_data;
+
+               /* Don’t list the size of the dependencies as that space likely
+                * won’t be reclaimed unless many other apps are removed. */
+               size_installed = gs_app_get_size_installed (self->app);
+               size_user_data = gs_app_get_size_user_data (self->app);
+               size_cache_data = gs_app_get_size_cache_data (self->app);
+
+               title = _("Installed Size");
+               title_size_bytes = size_installed;
+
+               add_size_row (self->sizes_list, self->lozenge_size_group, size_installed,
+                             _("Application Data"),
+                             _("Data needed for the application to run"));
+
+               if (size_user_data != GS_APP_SIZE_UNKNOWABLE) {
+                       add_size_row (self->sizes_list, self->lozenge_size_group, size_user_data,
+                                     _("User Data"),
+                                     _("Data created by you in the application"));
+                       title_size_bytes += size_user_data;
+               }
+
+               if (size_cache_data != GS_APP_SIZE_UNKNOWABLE) {
+                       add_size_row (self->sizes_list, self->lozenge_size_group, size_cache_data,
+                                     _("Cache Data"),
+                                     _("Temporary cached data"));
+                       title_size_bytes += size_cache_data;
+               }
+
+               /* FIXME: ‘Manage Storage’ button */
+       } else {
+               guint64 size_download;
+               guint64 size_download_dependencies;
+
+               size_download = gs_app_get_size_download (self->app);
+               size_download_dependencies = gs_app_get_size_download_dependencies (self->app);
+
+               title = _("Download Size");
+               title_size_bytes = size_download;
+
+               add_size_row (self->sizes_list, self->lozenge_size_group, size_download,
+                             gs_app_get_name (self->app),
+                             _("The application itself"));
+
+               if (size_download_dependencies != GS_APP_SIZE_UNKNOWABLE) {
+                       add_size_row (self->sizes_list, self->lozenge_size_group, size_download_dependencies,
+                                     _("Required Dependencies"),
+                                     _("Shared system components required by this application"));
+                       title_size_bytes += size_download_dependencies;
+               }
+
+               /* FIXME: Addons, Potential Additional Downloads */
+       }
+
+       title_size_bytes_str = g_format_size (title_size_bytes);
+       gtk_label_set_text (self->lozenge_content, title_size_bytes_str);
+       gtk_label_set_text (self->title, title);
+}
+
+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
+key_press_event_cb (GtkWidget            *sender,
+                    GdkEvent             *event,
+                    HdyPreferencesWindow *self)
+{
+       guint keyval;
+       GdkModifierType state;
+       GdkKeymap *keymap;
+       GdkEventKey *key_event = (GdkEventKey *) event;
+
+       gdk_event_get_state (event, &state);
+
+       keymap = gdk_keymap_get_for_display (gtk_widget_get_display (sender));
+
+       gdk_keymap_translate_keyboard_state (keymap,
+                                            key_event->hardware_keycode,
+                                            state,
+                                            key_event->group,
+                                            &keyval, NULL, NULL, NULL);
+
+       if (keyval == GDK_KEY_Escape) {
+               gtk_window_close (GTK_WINDOW (self));
+
+               return GDK_EVENT_STOP;
+       }
+
+       return GDK_EVENT_PROPAGATE;
+}
+
+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_content);
+       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_callback (widget_class, key_press_event_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]);
+}
diff --git a/src/gs-storage-context-dialog.h b/src/gs-storage-context-dialog.h
new file mode 100644
index 000000000..900ab6924
--- /dev/null
+++ b/src/gs-storage-context-dialog.h
@@ -0,0 +1,31 @@
+/* -*- 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+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_STORAGE_CONTEXT_DIALOG (gs_storage_context_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsStorageContextDialog, gs_storage_context_dialog, GS, STORAGE_CONTEXT_DIALOG, 
HdyWindow)
+
+GsStorageContextDialog *gs_storage_context_dialog_new          (GsApp                  *app);
+
+GsApp                  *gs_storage_context_dialog_get_app      (GsStorageContextDialog *self);
+void                    gs_storage_context_dialog_set_app      (GsStorageContextDialog *self,
+                                                                GsApp                  *app);
+
+G_END_DECLS
diff --git a/src/gs-storage-context-dialog.ui b/src/gs-storage-context-dialog.ui
new file mode 100644
index 000000000..6ffaa66b0
--- /dev/null
+++ b/src/gs-storage-context-dialog.ui
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="GsStorageContextDialog" parent="HdyWindow">
+    <property name="modal">True</property>
+    <property name="window_position">center</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="icon_name">dialog-information</property>
+    <property name="title" translatable="yes" comments="Translators: This is the title of the dialog which 
contains information about the storage or download size needed for an app">Storage</property>
+    <property name="type_hint">dialog</property>
+    <property name="default-width">640</property>
+    <property name="default-height">576</property>
+    <signal name="key-press-event" handler="key_press_event_cb" after="yes" swapped="no"/>
+    <style>
+      <class name="toolbox"/>
+    </style>
+
+    <child>
+      <object class="GtkOverlay">
+        <property name="visible">True</property>
+        <child type="overlay">
+          <object class="HdyHeaderBar">
+            <property name="show_close_button">True</property>
+            <property name="visible">True</property>
+            <property name="valign">start</property>
+          </object>
+        </child>
+        <child>
+          <object class="HdyPreferencesPage">
+            <property name="visible">True</property>
+            <child>
+              <object class="HdyPreferencesGroup">
+                <property name="visible">True</property>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">8</property>
+                    <property name="visible">True</property>
+
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin">20</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">12</property>
+                        <property name="visible">True</property>
+
+                        <child>
+                          <object class="GtkBox" id="lozenge">
+                            <property name="halign">center</property>
+                            <property name="visible">True</property>
+                            <style>
+                              <class name="context-tile-lozenge"/>
+                              <class name="large"/>
+                              <class name="grey"/>
+                            </style>
+                            <child>
+                              <object class="GtkLabel" id="lozenge_content">
+                                <property name="halign">center</property>
+                                <!-- this is a placeholder: the text is actually set in code -->
+                                <property name="label">20 MB</property>
+                                <property name="visible">True</property>
+                                <property name="xalign">0.5</property>
+                                <accessibility>
+                                  <relation target="title" type="labelled-by"/>
+                                </accessibility>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="fill">False</property>
+                          </packing>
+                        </child>
+
+                        <child>
+                          <object class="GtkLabel" id="title">
+                            <property name="justify">center</property>
+                            <!-- this is a placeholder: the text is actually set in code -->
+                            <property name="label">Shortwave works on this device</property>
+                            <property name="visible">True</property>
+                            <property name="wrap">True</property>
+                            <property name="xalign">0.5</property>
+                            <style>
+                              <class name="heading"/>
+                              <class name="title-1"/>
+                            </style>
+                            <accessibility>
+                              <relation target="lozenge" type="label-for"/>
+                            </accessibility>
+                            <style>
+                              <class name="context-tile-title"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkListBox" id="sizes_list">
+                        <property name="visible">True</property>
+                        <property name="selection_mode">none</property>
+                        <property name="halign">fill</property>
+                        <property name="valign">start</property>
+                        <style>
+                          <class name="content"/>
+                        </style>
+                        <!-- Rows are added in code -->
+                        <placeholder/>
+                      </object>
+                    </child>
+
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+
+  <object class="GtkSizeGroup" id="lozenge_size_group">
+    <property name="mode">horizontal</property>
+  </object>
+</interface>
diff --git a/src/meson.build b/src/meson.build
index a15cb41f2..10705abfe 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -76,6 +76,7 @@ gnome_software_sources = [
   'gs-shell-search-provider.c',
   'gs-star-image.c',
   'gs-star-widget.c',
+  'gs-storage-context-dialog.c',
   'gs-summary-tile.c',
   'gs-third-party-repo-row.c',
   'gs-update-dialog.c',


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