[gnome-software: 3/4] details-page: Use a carousel for the screenshots




commit 97f609863d8a478623a6346eea52996e26357f58
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Wed Dec 2 14:48:41 2020 +0100

    details-page: Use a carousel for the screenshots

 src/gnome-software.gresource.xml |   1 +
 src/gs-details-page.c            | 208 +--------------------
 src/gs-details-page.ui           |  86 +--------
 src/gs-screenshot-carousel.c     | 385 +++++++++++++++++++++++++++++++++++++++
 src/gs-screenshot-carousel.h     |  27 +++
 src/gs-screenshot-carousel.ui    | 152 ++++++++++++++++
 src/gtk-style-hc.css             |  19 +-
 src/gtk-style.css                |  19 +-
 src/meson.build                  |   1 +
 9 files changed, 606 insertions(+), 292 deletions(-)
---
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index b509b0334..8da93bec8 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -28,6 +28,7 @@
   <file preprocess="xml-stripblanks">gs-review-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-review-histogram.ui</file>
   <file preprocess="xml-stripblanks">gs-review-row.ui</file>
+  <file preprocess="xml-stripblanks">gs-screenshot-carousel.ui</file>
   <file preprocess="xml-stripblanks">gs-screenshot-image.ui</file>
   <file preprocess="xml-stripblanks">gs-search-page.ui</file>
   <file preprocess="xml-stripblanks">gs-shell.ui</file>
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
index 565608e71..55984df5b 100644
--- a/src/gs-details-page.c
+++ b/src/gs-details-page.c
@@ -24,7 +24,7 @@
 #include "gs-description-box.h"
 #include "gs-history-dialog.h"
 #include "gs-origin-popover-row.h"
-#include "gs-screenshot-image.h"
+#include "gs-screenshot-carousel.h"
 #include "gs-star-widget.h"
 #include "gs-review-histogram.h"
 #include "gs-review-dialog.h"
@@ -90,10 +90,7 @@ struct _GsDetailsPage
        GtkWidget               *box_progress2;
        GtkWidget               *star;
        GtkWidget               *label_review_count;
-       GtkWidget               *box_details_screenshot;
-       GtkWidget               *box_details_screenshot_main;
-       GtkWidget               *box_details_screenshot_scrolledwindow;
-       GtkWidget               *box_details_screenshot_thumbnails;
+       GtkWidget               *screenshot_carousel;
        GtkWidget               *box_details_license_list;
        GtkWidget               *button_details_launch;
        GtkWidget               *button_details_add_shortcut;
@@ -143,7 +140,6 @@ struct _GsDetailsPage
        GtkWidget               *row_latest_version;
        GtkWidget               *version_history_button;
        GtkWidget               *box_reviews;
-       GtkWidget               *box_details_screenshot_fallback;
        GtkWidget               *histogram;
        GtkWidget               *button_review;
        GtkWidget               *list_box_reviews;
@@ -580,192 +576,6 @@ gs_details_page_notify_state_changed_cb (GsApp *app,
        g_idle_add (gs_details_page_switch_to_idle, g_object_ref (self));
 }
 
-static void
-gs_details_page_load_main_screenshot (GsDetailsPage *self,
-                                     AsScreenshot *screenshot)
-{
-       GsScreenshotImage *ssmain;
-       g_autoptr(GList) children = NULL;
-
-       children = gtk_container_get_children (GTK_CONTAINER (self->box_details_screenshot_main));
-       ssmain = GS_SCREENSHOT_IMAGE (children->data);
-
-       gs_screenshot_image_set_screenshot (ssmain, screenshot);
-       gs_screenshot_image_load_async (ssmain, NULL);
-}
-
-static void
-gs_details_page_screenshot_selected_cb (GtkListBox *list,
-                                        GtkListBoxRow *row,
-                                        GsDetailsPage *self)
-{
-       GsScreenshotImage *ssthumb;
-       AsScreenshot *ss;
-
-       if (row == NULL)
-               return;
-
-       ssthumb = GS_SCREENSHOT_IMAGE (gtk_bin_get_child (GTK_BIN (row)));
-       ss = gs_screenshot_image_get_screenshot (ssthumb);
-
-       gs_details_page_load_main_screenshot (self, ss);
-}
-
-static void
-gs_details_page_refresh_screenshots (GsDetailsPage *self)
-{
-       GPtrArray *screenshots;
-       AsScreenshot *ss;
-       GtkWidget *label;
-       GtkWidget *list;
-       GtkWidget *ssimg;
-       GtkWidget *main_screenshot = NULL;
-       guint i;
-       gboolean is_offline = !gs_plugin_loader_get_network_available (self->plugin_loader);
-       guint num_screenshots_loaded = 0;
-
-       /* reset the visibility of screenshots */
-       gtk_widget_show (self->box_details_screenshot);
-
-       /* treat screenshots differently */
-       if (gs_app_get_kind (self->app) == AS_COMPONENT_KIND_FONT) {
-               gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_thumbnails));
-               gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_main));
-               screenshots = gs_app_get_screenshots (self->app);
-               for (i = 0; i < screenshots->len; i++) {
-                       ss = g_ptr_array_index (screenshots, i);
-
-                       /* set caption */
-                       label = gtk_label_new (as_screenshot_get_caption (ss));
-                       g_object_set (label,
-                                     "xalign", 0.0,
-                                     "max-width-chars", 10,
-                                     "wrap", TRUE,
-                                     NULL);
-                       gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_main), label);
-                       gtk_widget_set_visible (label, TRUE);
-
-                       /* set images */
-                       ssimg = gs_screenshot_image_new (self->session);
-                       gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss);
-                       gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg),
-                                                     640,
-                                                     48);
-                       gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), NULL);
-                       gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_main), ssimg);
-                       gtk_widget_set_visible (ssimg, TRUE);
-               }
-               gtk_widget_set_visible (self->box_details_screenshot,
-                                       screenshots->len > 0);
-               gtk_widget_set_visible (self->box_details_screenshot_fallback,
-                                       screenshots->len == 0 && !is_offline);
-               return;
-       }
-
-       /* fallback warning */
-       screenshots = gs_app_get_screenshots (self->app);
-       switch (gs_app_get_kind (self->app)) {
-       case AS_COMPONENT_KIND_GENERIC:
-       case AS_COMPONENT_KIND_CODEC:
-       case AS_COMPONENT_KIND_ADDON:
-       case AS_COMPONENT_KIND_REPOSITORY:
-       case AS_COMPONENT_KIND_FIRMWARE:
-       case AS_COMPONENT_KIND_DRIVER:
-       case AS_COMPONENT_KIND_INPUT_METHOD:
-       case AS_COMPONENT_KIND_LOCALIZATION:
-       case AS_COMPONENT_KIND_RUNTIME:
-               gtk_widget_set_visible (self->box_details_screenshot_fallback, FALSE);
-               break;
-       default:
-               gtk_widget_set_visible (self->box_details_screenshot_fallback,
-                                       screenshots->len == 0 && !is_offline);
-               break;
-       }
-
-       /* reset screenshots */
-       gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_main));
-       gs_container_remove_all (GTK_CONTAINER (self->box_details_screenshot_thumbnails));
-
-       list = gtk_list_box_new ();
-       gtk_style_context_add_class (gtk_widget_get_style_context (list), "image-list");
-       gtk_widget_show (list);
-       gtk_widget_show (self->box_details_screenshot_scrolledwindow);
-       gtk_container_add (GTK_CONTAINER (self->box_details_screenshot_thumbnails), list);
-
-       for (i = 0; i < screenshots->len; i++) {
-               ss = g_ptr_array_index (screenshots, i);
-
-               /* we need to load the main screenshot only once if we're online
-                * but all times if we're offline (to check which are cached and
-                * hide those who aren't) */
-               if (is_offline || main_screenshot == NULL) {
-                       GtkWidget *ssmain = gs_screenshot_image_new (self->session);
-                       gtk_widget_set_can_focus (gtk_bin_get_child (GTK_BIN (ssmain)), FALSE);
-                       gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssmain), ss);
-                       gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssmain),
-                                                     AS_IMAGE_NORMAL_WIDTH,
-                                                     AS_IMAGE_NORMAL_HEIGHT);
-                       gtk_style_context_add_class (gtk_widget_get_style_context (ssmain),
-                                                    "screenshot-image-main");
-                       gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssmain), NULL);
-
-                       /* when we're offline, the load will be immediate, so we
-                        * can check if it succeeded, and just skip it and its
-                        * thumbnails otherwise */
-                       if (is_offline &&
-                           !gs_screenshot_image_is_showing (GS_SCREENSHOT_IMAGE (ssmain)))
-                               continue;
-
-                       /* only set the main_screenshot once */
-                       if (main_screenshot == NULL) {
-                               main_screenshot = ssmain;
-                               gtk_box_pack_start (GTK_BOX (self->box_details_screenshot_main),
-                                                   main_screenshot, FALSE, FALSE, 0);
-                               gtk_widget_show (main_screenshot);
-                       }
-               }
-
-               ssimg = gs_screenshot_image_new (self->session);
-               gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss);
-               gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg),
-                                             AS_IMAGE_THUMBNAIL_WIDTH,
-                                             AS_IMAGE_THUMBNAIL_HEIGHT);
-               gtk_style_context_add_class (gtk_widget_get_style_context (ssimg),
-                                            "screenshot-image-thumb");
-               gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), NULL);
-               gtk_list_box_insert (GTK_LIST_BOX (list), ssimg, -1);
-               gtk_widget_set_visible (ssimg, TRUE);
-               ++num_screenshots_loaded;
-       }
-
-       if (main_screenshot == NULL) {
-               gtk_widget_hide (self->box_details_screenshot);
-               return;
-       }
-
-       /* reload the main screenshot with a larger size if it's the only screenshot
-        * available */
-       if (num_screenshots_loaded == 1) {
-               gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (main_screenshot),
-                                             AS_IMAGE_LARGE_WIDTH,
-                                             AS_IMAGE_LARGE_HEIGHT);
-               gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (main_screenshot), NULL);
-       }
-
-       if (num_screenshots_loaded <= 1) {
-               gtk_widget_hide (self->box_details_screenshot_thumbnails);
-               return;
-       }
-
-       gtk_widget_show (self->box_details_screenshot_thumbnails);
-       gtk_list_box_set_selection_mode (GTK_LIST_BOX (list), GTK_SELECTION_BROWSE);
-       g_signal_connect (list, "row-selected",
-                         G_CALLBACK (gs_details_page_screenshot_selected_cb),
-                         self);
-       gtk_list_box_select_row (GTK_LIST_BOX (list),
-                                gtk_list_box_get_row_at_index (GTK_LIST_BOX (list), 0));
-}
-
 static void
 gs_details_page_website_cb (GtkWidget *widget, GsDetailsPage *self)
 {
@@ -2032,6 +1842,8 @@ gs_details_page_load_stage2 (GsDetailsPage *self)
        g_autofree gchar *tmp = NULL;
        g_autoptr(GsPluginJob) plugin_job1 = NULL;
        g_autoptr(GsPluginJob) plugin_job2 = NULL;
+       gboolean is_online = gs_plugin_loader_get_network_available (self->plugin_loader);
+       gboolean has_screenshots;
 
        /* print what we've got */
        tmp = gs_app_to_string (self->app);
@@ -2039,7 +1851,9 @@ gs_details_page_load_stage2 (GsDetailsPage *self)
 
        /* update UI */
        gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_READY);
-       gs_details_page_refresh_screenshots (self);
+       gs_screenshot_carousel_load_screenshots (GS_SCREENSHOT_CAROUSEL (self->screenshot_carousel), 
self->app, is_online, NULL);
+       has_screenshots = gs_screenshot_carousel_get_has_screenshots (GS_SCREENSHOT_CAROUSEL 
(self->screenshot_carousel));
+       gtk_widget_set_visible (self->screenshot_carousel, has_screenshots);
        gs_details_page_refresh_addons (self);
        gs_details_page_refresh_reviews (self);
        gs_details_page_refresh_all (self);
@@ -3021,10 +2835,7 @@ gs_details_page_class_init (GsDetailsPageClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress2);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, star);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_review_count);
-       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot);
-       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_main);
-       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, 
box_details_screenshot_scrolledwindow);
-       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_thumbnails);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, screenshot_carousel);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_license_list);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_launch);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_add_shortcut);
@@ -3072,7 +2883,6 @@ gs_details_page_class_init (GsDetailsPageClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, row_latest_version);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, version_history_button);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_reviews);
-       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_fallback);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, histogram);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_review);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_reviews);
@@ -3115,6 +2925,8 @@ gs_details_page_class_init (GsDetailsPageClass *klass)
 static void
 gs_details_page_init (GsDetailsPage *self)
 {
+       g_type_ensure (GS_TYPE_SCREENSHOT_CAROUSEL);
+
        gtk_widget_init_template (GTK_WIDGET (self));
 
        /* setup networking */
diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui
index 612418afe..bafb11d15 100644
--- a/src/gs-details-page.ui
+++ b/src/gs-details-page.ui
@@ -65,11 +65,11 @@
                     <property name="visible">True</property>
                     <property name="halign">fill</property>
                     <property name="valign">start</property>
-                    <property name="margin_top">4</property>
-                    <property name="margin_bottom">4</property>
-                    <property name="border_width">24</property>
                     <property name="spacing">18</property>
                     <property name="hexpand">False</property>
+                    <style>
+                      <class name="details-page"/>
+                    </style>
                     <child>
                       <object class="GsFixedSizeBin">
                         <property name="visible">True</property>
@@ -317,86 +317,8 @@
                     </child>
 
                     <child>
-                      <object class="GsFixedSizeBin">
+                      <object class="GsScreenshotCarousel" id="screenshot_carousel">
                         <property name="visible">True</property>
-                        <property name="preferred-width">860</property>
-                        <child>
-                          <object class="GtkBox" id="box_details_screenshot">
-                            <property name="visible">True</property>
-                            <property name="margin_bottom">14</property>
-                            <property name="spacing">9</property>
-                            <child>
-                              <object class="GtkBox" id="box_details_screenshot_main">
-                                <property name="visible">True</property>
-                                <property name="orientation">vertical</property>
-                                <property name="spacing">6</property>
-                                <property name="hexpand">True</property>
-                                <child>
-                                  <placeholder/>
-                                </child>
-                              </object>
-                            </child>
-                            <child>
-                              <object class="GtkScrolledWindow" id="box_details_screenshot_scrolledwindow">
-                                <property name="visible">True</property>
-                                <property name="shadow_type">none</property>
-                                <property name="can_focus">True</property>
-                                <property name="hscrollbar_policy">never</property>
-                                <property name="vscrollbar_policy">automatic</property>
-                                <child>
-                                  <object class="GtkBox" id="box_details_screenshot_thumbnails">
-                                    <property name="visible">True</property>
-                                    <property name="orientation">vertical</property>
-                                    <property name="spacing">7</property>
-                                    <child>
-                                      <placeholder/>
-                                    </child>
-                                  </object>
-                                </child>
-                              </object>
-                            </child>
-                          </object>
-                        </child>
-                      </object>
-                    </child>
-
-                    <child>
-                      <object class="GsFixedSizeBin">
-                        <property name="visible">True</property>
-                        <property name="preferred-width">860</property>
-                        <child>
-                          <object class="GtkBox" id="box_details_screenshot_fallback">
-                            <property name="visible">True</property>
-                            <property name="orientation">vertical</property>
-                            <property name="spacing">6</property>
-                            <property name="width_request">752</property>
-                            <property name="height_request">423</property>
-                            <property name="hexpand">True</property>
-                            <property name="halign">fill</property>
-                            <style>
-                              <class name="screenshot-image"/>
-                            </style>
-                            <child>
-                              <object class="GtkImage">
-                                <property name="visible">True</property>
-                                <property name="pixel_size">64</property>
-                                <property name="icon_name">camera-photo-symbolic</property>
-                                <property name="icon_size">6</property>
-                                <property name="valign">end</property>
-                                <property name="vexpand">True</property>
-                              </object>
-                            </child>
-                            <child>
-                              <object class="GtkLabel">
-                                <property name="visible">True</property>
-                                <property name="halign">center</property>
-                                <property name="valign">start</property>
-                                <property name="vexpand">True</property>
-                                <property name="label" translatable="yes">No screenshot provided</property>
-                              </object>
-                            </child>
-                          </object>
-                        </child>
                       </object>
                     </child>
 
diff --git a/src/gs-screenshot-carousel.c b/src/gs-screenshot-carousel.c
new file mode 100644
index 000000000..7a3ca5292
--- /dev/null
+++ b/src/gs-screenshot-carousel.c
@@ -0,0 +1,385 @@
+/* -*- 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) 2013 Matthias Clasen <mclasen redhat com>
+ * Copyright (C) 2015-2019 Kalev Lember <klember redhat com>
+ * Copyright (C) 2019 Joaquim Rocha <jrocha endlessm com>
+ * Copyright (C) 2021 Adrien Plazas <adrien plazas puri sm>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-screenshot-carousel
+ * @short_description: A carousel presenting the screenshots of a #GsApp
+ *
+ * #GsScreenshotCarousel loads screenshots from a #GsApp and present them to the
+ * users.
+ *
+ * If the carousel doesn't have any screenshot to display, an empty state
+ * fallback will be presented, and it will be considered to have screenshots as
+ * long as it is trying to load some.
+ *
+ * Since: 41
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <handy.h>
+#include <locale.h>
+#include <math.h>
+#include <string.h>
+
+#include "gs-common.h"
+#include "gs-utils.h"
+
+#include "gs-screenshot-carousel.h"
+#include "gs-app-version-history-row.h"
+#include "gs-app-version-history-dialog.h"
+#include "gs-description-box.h"
+#include "gs-history-dialog.h"
+#include "gs-origin-popover-row.h"
+#include "gs-screenshot-image.h"
+
+struct _GsScreenshotCarousel
+{
+       GtkStack                 parent_instance;
+
+       SoupSession             *session;  /* (owned) (not nullable) */
+       gboolean                 has_screenshots;
+
+       GtkWidget               *button_next;
+       GtkWidget               *button_next_revealer;
+       GtkWidget               *button_previous;
+       GtkWidget               *button_previous_revealer;
+       GtkWidget               *carousel;
+       GtkWidget               *carousel_indicator;
+};
+
+typedef enum {
+       PROP_HAS_SCREENSHOTS = 1,
+} GsScreenshotCarouselProperty;
+
+static GParamSpec *obj_props[PROP_HAS_SCREENSHOTS + 1] = { NULL, };
+
+G_DEFINE_TYPE (GsScreenshotCarousel, gs_screenshot_carousel, GTK_TYPE_STACK)
+
+static void
+_set_state (GsScreenshotCarousel *self, guint length, gboolean allow_fallback, gboolean is_online)
+{
+       gboolean has_screenshots;
+
+       gtk_widget_set_visible (self->carousel_indicator, length > 1);
+       gtk_stack_set_visible_child_name (GTK_STACK (self), length > 0 ? "carousel" : "fallback");
+
+       has_screenshots = length > 0 || (allow_fallback && is_online);
+       if (self->has_screenshots != has_screenshots) {
+               self->has_screenshots = has_screenshots;
+               g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_HAS_SCREENSHOTS]);
+       }
+}
+
+/**
+ * gs_screenshot_carousel_load_screenshots:
+ * @self: a #GsScreenshotCarousel
+ * @app: app to load the screenshots for
+ * @is_online: %TRUE if the network is expected to work to load screenshots, %FALSE otherwise
+ *
+ * Clear the existing set of screenshot images, and load the
+ * screenshots for @app instead. Display them, or display a
+ * fallback if no screenshots could be loaded (and the fallback
+ * is enabled).
+ *
+ * This will start some asynchronous network requests to download
+ * screenshots. Those requests may continue after this function
+ * call returns.
+ *
+ * Since: 41
+ */
+void
+gs_screenshot_carousel_load_screenshots (GsScreenshotCarousel *self, GsApp *app, gboolean is_online, 
GCancellable *cancellable)
+{
+       GPtrArray *screenshots;
+       gboolean allow_fallback;
+       guint num_screenshots_loaded = 0;
+
+       g_return_if_fail (GS_IS_SCREENSHOT_CAROUSEL (self));
+       g_return_if_fail (GS_IS_APP (app));
+
+       /* treat screenshots differently */
+       if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FONT) {
+               gs_container_remove_all (GTK_CONTAINER (self->carousel));
+               screenshots = gs_app_get_screenshots (app);
+               for (guint i = 0; i < screenshots->len && !g_cancellable_is_cancelled (cancellable); i++) {
+                       AsScreenshot *ss = g_ptr_array_index (screenshots, i);
+                       GtkWidget *ssimg;
+                       GtkWidget *label;
+
+                       /* set caption */
+                       label = gtk_label_new (as_screenshot_get_caption (ss));
+                       g_object_set (label,
+                                     "xalign", 0.0,
+                                     "max-width-chars", 10,
+                                     "wrap", TRUE,
+                                     NULL);
+                       gtk_container_add (GTK_CONTAINER (self->carousel), label);
+                       gtk_widget_set_visible (label, TRUE);
+
+                       /* set images */
+                       ssimg = gs_screenshot_image_new (self->session);
+                       gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss);
+                       gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg),
+                                                     640,
+                                                     48);
+                       gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), cancellable);
+                       gtk_container_add (GTK_CONTAINER (self->carousel), ssimg);
+                       gtk_widget_set_visible (ssimg, TRUE);
+                       gs_screenshot_image_set_description (GS_SCREENSHOT_IMAGE (ssimg),
+                                                            as_screenshot_get_caption (ss));
+               }
+               _set_state (self, screenshots->len, TRUE, is_online);
+               return;
+       }
+
+       /* fallback warning */
+       screenshots = gs_app_get_screenshots (app);
+       switch (gs_app_get_kind (app)) {
+       case AS_COMPONENT_KIND_GENERIC:
+       case AS_COMPONENT_KIND_CODEC:
+       case AS_COMPONENT_KIND_ADDON:
+       case AS_COMPONENT_KIND_REPOSITORY:
+       case AS_COMPONENT_KIND_FIRMWARE:
+       case AS_COMPONENT_KIND_DRIVER:
+       case AS_COMPONENT_KIND_INPUT_METHOD:
+       case AS_COMPONENT_KIND_LOCALIZATION:
+       case AS_COMPONENT_KIND_RUNTIME:
+               allow_fallback = FALSE;
+               break;
+       default:
+               allow_fallback = TRUE;
+               break;
+       }
+
+       /* reset screenshots */
+       gs_container_remove_all (GTK_CONTAINER (self->carousel));
+
+       for (guint i = 0; i < screenshots->len && !g_cancellable_is_cancelled (cancellable); i++) {
+               AsScreenshot *ss = g_ptr_array_index (screenshots, i);
+               GtkWidget *ssimg = gs_screenshot_image_new (self->session);
+               gtk_widget_set_can_focus (gtk_bin_get_child (GTK_BIN (ssimg)), FALSE);
+               gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss);
+               gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg),
+                                             AS_IMAGE_NORMAL_WIDTH,
+                                             AS_IMAGE_NORMAL_HEIGHT);
+               gtk_style_context_add_class (gtk_widget_get_style_context (ssimg),
+                                            "screenshot-image-main");
+               gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), cancellable);
+
+               /* when we're offline, the load will be immediate, so we
+                * can check if it succeeded, and just skip it and its
+                * thumbnails otherwise */
+               if (!is_online &&
+                   !gs_screenshot_image_is_showing (GS_SCREENSHOT_IMAGE (ssimg))) {
+                       g_object_unref (ssimg);
+                       continue;
+               }
+
+               gtk_container_add (GTK_CONTAINER (self->carousel),
+                                  ssimg);
+               gtk_widget_show (ssimg);
+               gs_screenshot_image_set_description (GS_SCREENSHOT_IMAGE (ssimg),
+                                                    as_screenshot_get_caption (ss));
+               ++num_screenshots_loaded;
+       }
+
+       _set_state (self, num_screenshots_loaded, allow_fallback, is_online);
+}
+
+/**
+ * gs_screenshot_carousel_get_has_screenshots:
+ * @self: a #GsScreenshotCarousel
+ *
+ * Get whether the carousel contains any screenshots.
+ *
+ * Returns: %TRUE if there are screenshots, %FALSE otherwise
+ *
+ * Since: 41
+ */
+gboolean
+gs_screenshot_carousel_get_has_screenshots (GsScreenshotCarousel *self)
+{
+       g_return_val_if_fail (GS_IS_SCREENSHOT_CAROUSEL (self), FALSE);
+
+       return self->has_screenshots;
+}
+
+static void
+_carousel_navigate (HdyCarousel *carousel, HdyNavigationDirection direction)
+{
+       g_autoptr (GList) children = NULL;
+       GtkWidget *child;
+       gdouble position;
+       guint n_children;
+
+       children = gtk_container_get_children (GTK_CONTAINER (carousel));
+       n_children = g_list_length (children);
+
+       position = hdy_carousel_get_position (carousel);
+       position += (direction == HDY_NAVIGATION_DIRECTION_BACK) ? -1 : 1;
+       /* Round the position to the closest integer in the valid range. */
+       position = round (position);
+       position = MIN (position, n_children - 1);
+       position = MAX (0, position);
+
+       child = g_list_nth_data (children, position);
+       if (child)
+               hdy_carousel_scroll_to (carousel, child);
+}
+
+static void
+gs_screenshot_carousel_update_buttons (GsScreenshotCarousel *self)
+{
+       gdouble position = hdy_carousel_get_position (HDY_CAROUSEL (self->carousel));
+       guint n_pages = hdy_carousel_get_n_pages (HDY_CAROUSEL (self->carousel));
+       gtk_revealer_set_reveal_child (GTK_REVEALER (self->button_previous_revealer), position >= 0.5);
+       gtk_revealer_set_reveal_child (GTK_REVEALER (self->button_next_revealer), position < n_pages - 1.5);
+}
+
+static void
+gs_screenshot_carousel_notify_n_pages_cb (GsScreenshotCarousel *self)
+{
+       gs_screenshot_carousel_update_buttons (self);
+}
+
+static void
+gs_screenshot_carousel_notify_position_cb (GsScreenshotCarousel *self)
+{
+       gs_screenshot_carousel_update_buttons (self);
+}
+
+static void
+gs_screenshot_carousel_button_previous_clicked_cb (GsScreenshotCarousel *self)
+{
+       _carousel_navigate (HDY_CAROUSEL (self->carousel),
+                           HDY_NAVIGATION_DIRECTION_BACK);
+}
+
+static void
+gs_screenshot_carousel_button_next_clicked_cb (GsScreenshotCarousel *self)
+{
+       _carousel_navigate (HDY_CAROUSEL (self->carousel),
+                           HDY_NAVIGATION_DIRECTION_FORWARD);
+}
+
+static void
+gs_screenshot_carousel_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+       GsScreenshotCarousel *self = GS_SCREENSHOT_CAROUSEL (object);
+
+       switch ((GsScreenshotCarouselProperty) prop_id) {
+       case PROP_HAS_SCREENSHOTS:
+               g_value_set_boolean (value, self->has_screenshots);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_screenshot_carousel_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+       switch ((GsScreenshotCarouselProperty) prop_id) {
+       case PROP_HAS_SCREENSHOTS:
+               /* Read only */
+               g_assert_not_reached ();
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_screenshot_carousel_dispose (GObject *object)
+{
+       GsScreenshotCarousel *self = GS_SCREENSHOT_CAROUSEL (object);
+
+       g_clear_object (&self->session);
+
+       G_OBJECT_CLASS (gs_screenshot_carousel_parent_class)->dispose (object);
+}
+
+static void
+gs_screenshot_carousel_class_init (GsScreenshotCarouselClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       object_class->dispose = gs_screenshot_carousel_dispose;
+       object_class->get_property = gs_screenshot_carousel_get_property;
+       object_class->set_property = gs_screenshot_carousel_set_property;
+
+       /**
+        * GsScreenshotCarousel:has-screenshots:
+        *
+        * Whether the carousel contains any screenshots.
+        *
+        * Since: 41
+        */
+       obj_props[PROP_HAS_SCREENSHOTS] =
+               g_param_spec_boolean ("has-screenshots", NULL, NULL,
+                                     FALSE,
+                                     G_PARAM_READABLE | 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-screenshot-carousel.ui");
+
+       gtk_widget_class_bind_template_child (widget_class, GsScreenshotCarousel, button_next);
+       gtk_widget_class_bind_template_child (widget_class, GsScreenshotCarousel, button_next_revealer);
+       gtk_widget_class_bind_template_child (widget_class, GsScreenshotCarousel, button_previous);
+       gtk_widget_class_bind_template_child (widget_class, GsScreenshotCarousel, button_previous_revealer);
+       gtk_widget_class_bind_template_child (widget_class, GsScreenshotCarousel, carousel);
+       gtk_widget_class_bind_template_child (widget_class, GsScreenshotCarousel, carousel_indicator);
+
+       gtk_widget_class_bind_template_callback (widget_class, gs_screenshot_carousel_notify_n_pages_cb);
+       gtk_widget_class_bind_template_callback (widget_class, gs_screenshot_carousel_notify_position_cb);
+       gtk_widget_class_bind_template_callback (widget_class, 
gs_screenshot_carousel_button_previous_clicked_cb);
+       gtk_widget_class_bind_template_callback (widget_class, gs_screenshot_carousel_button_next_clicked_cb);
+
+       gtk_widget_class_set_css_name (widget_class, "screenshot-carousel");
+}
+
+static void
+gs_screenshot_carousel_init (GsScreenshotCarousel *self)
+{
+       gtk_widget_init_template (GTK_WIDGET (self));
+
+#if HDY_CHECK_VERSION(1, 3, 0)
+       /* Disable scrolling through the carousel, as it’s typically used
+        * in application pages which are themselves scrollable. */
+       hdy_carousel_set_allow_scroll_wheel (HDY_CAROUSEL (self->carousel), FALSE);
+#endif
+
+       /* setup networking */
+       self->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (),
+                                                      NULL);
+}
+
+/**
+ * gs_screenshot_carousel_new:
+ *
+ * Create a new #GsScreenshotCarousel.
+ *
+ * Returns: (transfer full): a new #GsScreenshotCarousel
+ *
+ * Since: 41
+ */
+GsScreenshotCarousel *
+gs_screenshot_carousel_new (void)
+{
+       return GS_SCREENSHOT_CAROUSEL (g_object_new (GS_TYPE_SCREENSHOT_CAROUSEL, NULL));
+}
diff --git a/src/gs-screenshot-carousel.h b/src/gs-screenshot-carousel.h
new file mode 100644
index 000000000..ea208280a
--- /dev/null
+++ b/src/gs-screenshot-carousel.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Adrien Plazas <adrien plazas puri sm>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_SCREENSHOT_CAROUSEL (gs_screenshot_carousel_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsScreenshotCarousel, gs_screenshot_carousel, GS, SCREENSHOT_CAROUSEL, GtkStack)
+
+GsScreenshotCarousel   *gs_screenshot_carousel_new     (void);
+void                    gs_screenshot_carousel_load_screenshots        (GsScreenshotCarousel *self,
+                                                                        GsApp                *app,
+                                                                        gboolean              is_online,
+                                                                        GCancellable         *cancellable);
+gboolean                gs_screenshot_carousel_get_has_screenshots     (GsScreenshotCarousel *self);
+
+G_END_DECLS
diff --git a/src/gs-screenshot-carousel.ui b/src/gs-screenshot-carousel.ui
new file mode 100644
index 000000000..af766c3e4
--- /dev/null
+++ b/src/gs-screenshot-carousel.ui
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <requires lib="handy" version="1.0"/>
+  <template class="GsScreenshotCarousel" parent="GtkStack">
+    <property name="no-show-all">True</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <style>
+          <class name="frame"/>
+          <class name="view"/>
+        </style>
+        <child>
+          <object class="GtkOverlay">
+            <property name="visible">True</property>
+            <child>
+              <object class="HdyCarousel" id="carousel">
+                <property name="visible">True</property>
+                <signal name="notify::n-pages" handler="gs_screenshot_carousel_notify_n_pages_cb" 
swapped="yes"/>
+                <signal name="notify::position" handler="gs_screenshot_carousel_notify_position_cb" 
swapped="yes"/>
+              </object>
+            </child>
+            <child type="overlay">
+              <object class="GtkRevealer" id="button_previous_revealer">
+                <property name="halign">start</property>
+                <property name="transition-type">crossfade</property>
+                <property name="valign">center</property>
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkButton" id="button_previous">
+                    <property name="visible">True</property>
+                    <signal name="clicked" handler="gs_screenshot_carousel_button_previous_clicked_cb" 
swapped="yes"/>
+                    <child internal-child="accessible">
+                      <object class="AtkObject">
+                        <!-- Translators: This is the accessible description for a button to go to the 
previous screenshot in the screenshot carousel. -->
+                        <property name="accessible-name" translatable="yes">Previous Screenshot</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="circular"/>
+                      <class name="image-button"/>
+                      <class name="osd"/>
+                    </style>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="halign">center</property>
+                        <property name="icon-name">go-previous-symbolic</property>
+                        <property name="icon-size">1</property>
+                        <property name="valign">center</property>
+                        <property name="visible">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pass-through">True</property>
+              </packing>
+            </child>
+            <child type="overlay">
+              <object class="GtkRevealer" id="button_next_revealer">
+                <property name="halign">end</property>
+                <property name="transition-type">crossfade</property>
+                <property name="valign">center</property>
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkButton" id="button_next">
+                    <property name="visible">True</property>
+                    <signal name="clicked" handler="gs_screenshot_carousel_button_next_clicked_cb" 
swapped="yes"/>
+                    <child internal-child="accessible">
+                      <object class="AtkObject">
+                        <!-- Translators: This is the accessible description for a button to go to the next 
screenshot in the screenshot carousel. -->
+                        <property name="accessible-name" translatable="yes">Next Screenshot</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="circular"/>
+                      <class name="image-button"/>
+                      <class name="osd"/>
+                    </style>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="halign">center</property>
+                        <property name="icon-name">go-next-symbolic</property>
+                        <property name="icon-size">1</property>
+                        <property name="valign">center</property>
+                        <property name="visible">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pass-through">True</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="HdyCarouselIndicatorDots" id="carousel_indicator">
+            <property name="carousel">carousel</property>
+            <property name="visible">True</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="name">carousel</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <!-- Arbitrary sizes chosen to tile together at 16:9;
+             see https://blogs.gnome.org/hughsie/2014/07/02/blurry-screenshots-in-gnome-software/ -->
+        <property name="width_request">752</property>
+        <property name="height_request">423</property>
+        <property name="hexpand">True</property>
+        <property name="halign">fill</property>
+        <style>
+          <class name="screenshot-image"/>
+          <class name="frame"/>
+          <class name="view"/>
+        </style>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="pixel_size">64</property>
+            <property name="icon_name">camera-photo-symbolic</property>
+            <property name="valign">end</property>
+            <property name="vexpand">True</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <property name="vexpand">True</property>
+            <property name="label" translatable="yes">No screenshot provided</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="name">fallback</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css
index a0f2938f3..95ad283dd 100644
--- a/src/gtk-style-hc.css
+++ b/src/gtk-style-hc.css
@@ -53,13 +53,20 @@ sidebar row.needs-attention:selected > box > label {
        background-image: none;
 }
 
-.screenshot-image, .screenshot-image-thumb {
-       background-image: none;
-       background-color: shade(@theme_bg_color, 0.9);
+screenshot-carousel > box {
+       border-width: 1px 0;
+}
+
+screenshot-carousel button {
+       margin: 12px;
+}
+
+.screenshot-image-main .image1, .screenshot-image-main .image2 {
+       margin-top: 6px;
+       margin-bottom: 12px;
+       margin-left: 6px;
+       margin-right: 6px;
 }
-.screenshot-image { border-radius: 5px; }
-.screenshot-image-thumb { border-radius: 3px; }
-.screenshot-image-main .image1, .screenshot-image-main .image2 { margin: 12px; }
 
 .app-tile-label {
        font-size: 105%;
diff --git a/src/gtk-style.css b/src/gtk-style.css
index e49f91335..4b701bd0f 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -214,13 +214,20 @@ sidebar row.needs-attention:selected > box > label {
        opacity: 0.25;
 }
 
-.screenshot-image, .screenshot-image-thumb {
-       background-image: none;
-       background-color: shade(@theme_bg_color, 0.9);
+screenshot-carousel > box {
+       border-width: 1px 0;
+}
+
+screenshot-carousel button {
+       margin: 12px;
+}
+
+.screenshot-image-main .image1, .screenshot-image-main .image2 {
+       margin-top: 6px;
+       margin-bottom: 12px;
+       margin-left: 6px;
+       margin-right: 6px;
 }
-.screenshot-image { border-radius: 5px; }
-.screenshot-image-thumb { border-radius: 3px; }
-.screenshot-image-main .image1, .screenshot-image-main .image2 { margin: 12px; }
 
 .app-tile-label {
        font-size: 105%;
diff --git a/src/meson.build b/src/meson.build
index 7925a4b2e..430eb0c4b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -57,6 +57,7 @@ gnome_software_sources = [
   'gs-review-histogram.c',
   'gs-review-row.c',
   'gs-rounded-bin.c',
+  'gs-screenshot-carousel.c',
   'gs-screenshot-image.c',
   'gs-search-page.c',
   'gs-shell.c',


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