summaryrefslogtreecommitdiffstats
path: root/src/gs-screenshot-carousel.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-screenshot-carousel.c')
-rw-r--r--src/gs-screenshot-carousel.c365
1 files changed, 365 insertions, 0 deletions
diff --git a/src/gs-screenshot-carousel.c b/src/gs-screenshot-carousel.c
new file mode 100644
index 0000000..d3111af
--- /dev/null
+++ b/src/gs-screenshot-carousel.c
@@ -0,0 +1,365 @@
+/* -*- 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 <adwaita.h>
+#include <glib/gi18n.h>
+#include <locale.h>
+#include <math.h>
+#include <string.h>
+
+#include "gs-common.h"
+#include "gs-download-utils.h"
+#include "gs-utils.h"
+
+#include "gs-screenshot-carousel.h"
+#include "gs-screenshot-image.h"
+
+struct _GsScreenshotCarousel
+{
+ GtkWidget 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;
+ GtkStack *stack;
+};
+
+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_WIDGET)
+
+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 (self->stack, 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]);
+ }
+}
+
+static void
+gs_screenshot_carousel_img_clicked_cb (GtkWidget *ssimg,
+ gpointer user_data)
+{
+ GsScreenshotCarousel *self = user_data;
+ adw_carousel_scroll_to (ADW_CAROUSEL (self->carousel), ssimg, TRUE);
+}
+
+/**
+ * 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));
+
+ /* 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_widget_remove_all (self->carousel, (GsRemoveFunc) adw_carousel_remove);
+
+ 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_widget_get_first_child (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_ref_sink (ssimg);
+ g_object_unref (ssimg);
+ continue;
+ }
+
+ g_signal_connect_object (ssimg, "clicked",
+ G_CALLBACK (gs_screenshot_carousel_img_clicked_cb), self, 0);
+
+ adw_carousel_append (ADW_CAROUSEL (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 (AdwCarousel *carousel, AdwNavigationDirection direction)
+{
+ g_autoptr (GList) children = NULL;
+ GtkWidget *child;
+ gdouble position;
+ guint n_children;
+
+ n_children = 0;
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (carousel));
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child)) {
+ children = g_list_prepend (children, child);
+ n_children++;
+ }
+ children = g_list_reverse (children);
+
+ position = adw_carousel_get_position (carousel);
+ position += (direction == ADW_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)
+ adw_carousel_scroll_to (carousel, child, TRUE);
+}
+
+static void
+gs_screenshot_carousel_update_buttons (GsScreenshotCarousel *self)
+{
+ gdouble position = adw_carousel_get_position (ADW_CAROUSEL (self->carousel));
+ guint n_pages = adw_carousel_get_n_pages (ADW_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 (ADW_CAROUSEL (self->carousel),
+ ADW_NAVIGATION_DIRECTION_BACK);
+}
+
+static void
+gs_screenshot_carousel_button_next_clicked_cb (GsScreenshotCarousel *self)
+{
+ _carousel_navigate (ADW_CAROUSEL (self->carousel),
+ ADW_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);
+
+ gs_widget_remove_all (GTK_WIDGET (self), NULL);
+
+ 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_child (widget_class, GsScreenshotCarousel, stack);
+
+ 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_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ 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));
+
+ /* Disable scrolling through the carousel, as it’s typically used
+ * in application pages which are themselves scrollable. */
+ adw_carousel_set_allow_scroll_wheel (ADW_CAROUSEL (self->carousel), FALSE);
+
+ /* setup networking */
+ self->session = gs_build_soup_session ();
+}
+
+/**
+ * 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));
+}