[gnome-software: 2/22] gs-featured-carousel: Add new carousel widget for featured apps
- From: Phaedrus Leeds <mwleeds src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software: 2/22] gs-featured-carousel: Add new carousel widget for featured apps
- Date: Thu, 18 Feb 2021 07:33:02 +0000 (UTC)
commit 94ff47c1bac6b99d17848a3e36b80f14b1f671f4
Author: Philip Withnall <pwithnall endlessos org>
Date: Thu Feb 11 23:09:50 2021 +0000
gs-featured-carousel: Add new carousel widget for featured apps
This is a new design for showing featured apps, using `HdyCarousel`.
Eventually, it will prioritise an app-specific background colour, the
app’s hi-res icon, and some text; instead of the current approach of
supporting custom per-app CSS. The CSS was hard to implement, the
background images took up a lot of memory and disk space, and it
required a lot of curation time which it turned out we just didn’t have.
Signed-off-by: Philip Withnall <pwithnall endlessos org>
Helps: #1131
contrib/gnome-software.spec.in | 2 +
data/icons/hicolor/meson.build | 4 +
.../scalable/carousel-arrow-next-symbolic.svg | 36 ++
.../scalable/carousel-arrow-previous-symbolic.svg | 36 ++
po/POTFILES.in | 1 +
src/gnome-software.gresource.xml | 1 +
src/gs-featured-carousel.c | 451 +++++++++++++++++++++
src/gs-featured-carousel.h | 31 ++
src/gs-featured-carousel.ui | 105 +++++
src/gtk-style.css | 30 +-
src/meson.build | 1 +
11 files changed, 697 insertions(+), 1 deletion(-)
---
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
index 0d0d6c74b..97d8e9282 100644
--- a/contrib/gnome-software.spec.in
+++ b/contrib/gnome-software.spec.in
@@ -141,6 +141,8 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_mandir}/man1/gnome-software.1.gz
%{_datadir}/icons/hicolor/*/apps/org.gnome.Software.svg
%{_datadir}/icons/hicolor/symbolic/apps/org.gnome.Software-symbolic.svg
+%{_datadir}/icons/hicolor/scalable/actions/carousel-arrow-next-symbolic.svg
+%{_datadir}/icons/hicolor/scalable/actions/carousel-arrow-previous-symbolic.svg
%{_datadir}/icons/hicolor/scalable/status/software-installed-symbolic.svg
%{_datadir}/gnome-software/featured-*.svg
%{_datadir}/gnome-software/featured-*.jpg
diff --git a/data/icons/hicolor/meson.build b/data/icons/hicolor/meson.build
index 8e232b8a9..933ae5ad8 100644
--- a/data/icons/hicolor/meson.build
+++ b/data/icons/hicolor/meson.build
@@ -1,5 +1,9 @@
install_data('scalable/org.gnome.Software.svg',
install_dir : 'share/icons/hicolor/scalable/apps')
+install_data('scalable/carousel-arrow-next-symbolic.svg',
+ install_dir : 'share/icons/hicolor/scalable/actions')
+install_data('scalable/carousel-arrow-previous-symbolic.svg',
+ install_dir : 'share/icons/hicolor/scalable/actions')
install_data('symbolic/org.gnome.Software-symbolic.svg',
install_dir : 'share/icons/hicolor/symbolic/apps')
install_data('scalable/software-installed-symbolic.svg',
diff --git a/data/icons/hicolor/scalable/carousel-arrow-next-symbolic.svg
b/data/icons/hicolor/scalable/carousel-arrow-next-symbolic.svg
new file mode 100644
index 000000000..7d6356f23
--- /dev/null
+++ b/data/icons/hicolor/scalable/carousel-arrow-next-symbolic.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24.000001"
+ version="1.1"
+ id="svg19258">
+ <defs
+ id="defs19252" />
+ <metadata
+ id="metadata19255">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="g872"
+ transform="matrix(0,-1,-1,0,742.51668,224.9988)"
+ style="fill:#241f31">
+ <path
+
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;
stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 213.0007,724.40348 -10.3711,7.3945 v 0 a 1.5,1.5 0 0 0 -0.6308,1.2187 v 1.5 h 1.5 a 1.5,1.5 0 0
0 0.8711,-0.2793 l 8.6289,-6.1523 8.6289,6.1523 a 1.5,1.5 0 0 0 0.8711,0.2793 h 1.5 v -1.5 a 1.5,1.5 0 0 0
-0.6309,-1.2187 v 0 z"
+ id="path870" />
+ </g>
+</svg>
diff --git a/data/icons/hicolor/scalable/carousel-arrow-previous-symbolic.svg
b/data/icons/hicolor/scalable/carousel-arrow-previous-symbolic.svg
new file mode 100644
index 000000000..984893092
--- /dev/null
+++ b/data/icons/hicolor/scalable/carousel-arrow-previous-symbolic.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24.000001"
+ version="1.1"
+ id="svg19258">
+ <defs
+ id="defs19252" />
+ <metadata
+ id="metadata19255">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="g834"
+ transform="rotate(-90,-246.75894,471.75774)"
+ style="fill:#241f31">
+ <path
+
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;
stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 213.0007,724.40348 -10.3711,7.3945 v 0 a 1.5,1.5 0 0 0 -0.6308,1.2187 v 1.5 h 1.5 a 1.5,1.5 0 0
0 0.8711,-0.2793 l 8.6289,-6.1523 8.6289,6.1523 a 1.5,1.5 0 0 0 0.8711,0.2793 h 1.5 v -1.5 a 1.5,1.5 0 0 0
-0.6309,-1.2187 v 0 z"
+ id="path832" />
+ </g>
+</svg>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 00028c0db..ae862e038 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -29,6 +29,7 @@ src/gs-details-page.ui
src/gs-extras-page.c
src/gs-extras-page.ui
src/gs-feature-tile.c
+src/gs-featured-carousel.c
src/gs-first-run-dialog.ui
src/gs-hiding-box.c
src/gs-history-dialog.c
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index d0f7ea95c..26f098d42 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -12,6 +12,7 @@
<file preprocess="xml-stripblanks">gs-details-page.ui</file>
<file preprocess="xml-stripblanks">gs-extras-page.ui</file>
<file preprocess="xml-stripblanks">gs-feature-tile.ui</file>
+ <file preprocess="xml-stripblanks">gs-featured-carousel.ui</file>
<file preprocess="xml-stripblanks">gs-first-run-dialog.ui</file>
<file preprocess="xml-stripblanks">gs-history-dialog.ui</file>
<file preprocess="xml-stripblanks">gs-info-bar.ui</file>
diff --git a/src/gs-featured-carousel.c b/src/gs-featured-carousel.c
new file mode 100644
index 000000000..42a399a29
--- /dev/null
+++ b/src/gs-featured-carousel.c
@@ -0,0 +1,451 @@
+/* -*- 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, Inc
+ *
+ * Author: Philip Withnall <pwithnall endlessos org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-featured-carousel
+ * @short_description: A carousel widget containing #GsFeatureTile instances
+ *
+ * #GsFeaturedCarousel is a carousel widget which rotates through a set of
+ * #GsFeatureTiles, displaying them to the user to advertise a given set of
+ * featured apps, set with gs_featured_carousel_set_apps().
+ *
+ * The widget has no special appearance if the app list is empty, so callers
+ * will typically want to hide the carousel in that case.
+ *
+ * Since: 40
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <handy.h>
+
+#include "gs-app-list.h"
+#include "gs-common.h"
+#include "gs-feature-tile.h"
+#include "gs-featured-carousel.h"
+
+struct _GsFeaturedCarousel
+{
+ GtkBox parent_instance;
+
+ GsAppList *apps; /* (nullable) (owned) */
+ guint rotation_timer_id;
+
+ HdyCarousel *carousel;
+ GtkButton *next_button;
+ GtkImage *next_button_image;
+ GtkButton *previous_button;
+ GtkImage *previous_button_image;
+};
+
+G_DEFINE_TYPE (GsFeaturedCarousel, gs_featured_carousel, GTK_TYPE_BOX)
+
+typedef enum {
+ PROP_APPS = 1,
+} GsFeaturedCarouselProperty;
+
+static GParamSpec *obj_props[PROP_APPS + 1] = { NULL, };
+
+typedef enum {
+ SIGNAL_APP_CLICKED,
+ SIGNAL_CLICKED,
+} GsFeaturedCarouselSignal;
+
+static guint obj_signals[SIGNAL_CLICKED + 1] = { 0, };
+
+static void
+show_relative_page (GsFeaturedCarousel *self,
+ gint delta)
+{
+ gdouble current_page = hdy_carousel_get_position (self->carousel);
+ guint n_pages = hdy_carousel_get_n_pages (self->carousel);
+ gdouble new_page;
+ g_autoptr(GList) children = gtk_container_get_children (GTK_CONTAINER (self->carousel));
+ GtkWidget *new_page_widget;
+ gint64 animation_duration_ms = hdy_carousel_get_animation_duration (self->carousel);
+
+ if (n_pages == 0)
+ return;
+
+ /* FIXME: This would be simpler if HdyCarousel had a way to scroll to
+ * a page by index, rather than by GtkWidget pointer.
+ * See https://gitlab.gnome.org/GNOME/libhandy/-/issues/413 */
+ new_page = ((guint) current_page + delta + n_pages) % n_pages;
+ new_page_widget = g_list_nth_data (children, new_page);
+ g_assert (new_page_widget != NULL);
+
+ /* Don’t animate if we’re wrapping from the last page back to the first,
+ * as it means rapidly spooling through all the pages, which looks
+ * confusing. */
+ if (new_page == 0.0)
+ animation_duration_ms = 0;
+
+ hdy_carousel_scroll_to_full (self->carousel, new_page_widget, animation_duration_ms);
+}
+
+static gboolean
+rotate_cb (gpointer user_data)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (user_data);
+
+ show_relative_page (self, +1);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+start_rotation_timer (GsFeaturedCarousel *self)
+{
+ const guint rotation_period_seconds = 15;
+
+ if (self->rotation_timer_id == 0) {
+ self->rotation_timer_id = g_timeout_add_seconds (rotation_period_seconds,
+ rotate_cb, self);
+ }
+}
+
+static void
+stop_rotation_timer (GsFeaturedCarousel *self)
+{
+ if (self->rotation_timer_id != 0) {
+ g_source_remove (self->rotation_timer_id);
+ self->rotation_timer_id = 0;
+ }
+}
+
+static void
+image_set_icon_for_direction (GtkImage *image,
+ const gchar *ltr_icon_name,
+ const gchar *rtl_icon_name)
+{
+ const gchar *icon_name = (gtk_widget_get_direction (GTK_WIDGET (image)) == GTK_TEXT_DIR_RTL) ?
rtl_icon_name : ltr_icon_name;
+ gtk_image_set_from_icon_name (image, icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR);
+}
+
+static void
+next_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (user_data);
+
+ show_relative_page (self, +1);
+
+ /* Reset the rotation timer in case it’s about to fire. */
+ stop_rotation_timer (self);
+ start_rotation_timer (self);
+}
+
+static void
+next_button_direction_changed_cb (GtkWidget *widget,
+ GtkTextDirection previous_direction,
+ gpointer user_data)
+{
+ image_set_icon_for_direction (GTK_IMAGE (widget), "carousel-arrow-next-symbolic",
"carousel-arrow-previous-symbolic");
+}
+
+static void
+previous_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (user_data);
+
+ show_relative_page (self, -1);
+
+ /* Reset the rotation timer in case it’s about to fire. */
+ stop_rotation_timer (self);
+ start_rotation_timer (self);
+}
+
+static void
+previous_button_direction_changed_cb (GtkWidget *widget,
+ GtkTextDirection previous_direction,
+ gpointer user_data)
+{
+ image_set_icon_for_direction (GTK_IMAGE (widget), "carousel-arrow-previous-symbolic",
"carousel-arrow-next-symbolic");
+}
+
+static void
+app_tile_clicked_cb (GsAppTile *app_tile,
+ gpointer user_data)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (user_data);
+ GsApp *app;
+
+ app = gs_app_tile_get_app (app_tile);
+ g_signal_emit (self, obj_signals[SIGNAL_APP_CLICKED], 0, app);
+}
+
+static void
+gs_featured_carousel_init (GsFeaturedCarousel *self)
+{
+ gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* Ensure the text directions are up to date */
+ next_button_direction_changed_cb (GTK_WIDGET (self->next_button_image), GTK_TEXT_DIR_NONE, self);
+ previous_button_direction_changed_cb (GTK_WIDGET (self->previous_button_image), GTK_TEXT_DIR_NONE,
self);
+}
+
+static void
+gs_featured_carousel_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (object);
+
+ switch ((GsFeaturedCarouselProperty) prop_id) {
+ case PROP_APPS:
+ g_value_set_object (value, gs_featured_carousel_get_apps (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_featured_carousel_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (object);
+
+ switch ((GsFeaturedCarouselProperty) prop_id) {
+ case PROP_APPS:
+ gs_featured_carousel_set_apps (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_featured_carousel_dispose (GObject *object)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (object);
+
+ stop_rotation_timer (self);
+ g_clear_object (&self->apps);
+
+ G_OBJECT_CLASS (gs_featured_carousel_parent_class)->dispose (object);
+}
+
+static gboolean
+gs_featured_carousel_key_press_event (GtkWidget *widget,
+ GdkEventKey *event)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (widget);
+
+ if (gtk_widget_is_visible (GTK_WIDGET (self->previous_button)) &&
+ gtk_widget_is_sensitive (GTK_WIDGET (self->previous_button)) &&
+ ((gtk_widget_get_direction (GTK_WIDGET (self->previous_button)) == GTK_TEXT_DIR_LTR &&
event->keyval == GDK_KEY_Left) ||
+ (gtk_widget_get_direction (GTK_WIDGET (self->previous_button)) == GTK_TEXT_DIR_RTL &&
event->keyval == GDK_KEY_Right))) {
+ gtk_widget_activate (GTK_WIDGET (self->previous_button));
+ return GDK_EVENT_STOP;
+ }
+
+ if (gtk_widget_is_visible (GTK_WIDGET (self->next_button)) &&
+ gtk_widget_is_sensitive (GTK_WIDGET (self->next_button)) &&
+ ((gtk_widget_get_direction (GTK_WIDGET (self->next_button)) == GTK_TEXT_DIR_LTR && event->keyval
== GDK_KEY_Right) ||
+ (gtk_widget_get_direction (GTK_WIDGET (self->next_button)) == GTK_TEXT_DIR_RTL && event->keyval
== GDK_KEY_Left))) {
+ gtk_widget_activate (GTK_WIDGET (self->next_button));
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+carousel_clicked_cb (GsFeaturedCarousel *carousel,
+ gpointer user_data)
+{
+ GsFeaturedCarousel *self = GS_FEATURED_CAROUSEL (user_data);
+ GsAppTile *current_tile;
+ GsApp *app;
+ gdouble current_page;
+ g_autoptr(GList) children = NULL;
+
+ /* Get the currently visible tile. */
+ current_page = hdy_carousel_get_position (self->carousel);
+ children = gtk_container_get_children (GTK_CONTAINER (self->carousel));
+
+ current_tile = g_list_nth_data (children, current_page);
+ if (current_tile == NULL)
+ return;
+
+ app = gs_app_tile_get_app (current_tile);
+ g_signal_emit (self, obj_signals[SIGNAL_APP_CLICKED], 0, app);
+}
+
+static void
+gs_featured_carousel_class_init (GsFeaturedCarouselClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gs_featured_carousel_get_property;
+ object_class->set_property = gs_featured_carousel_set_property;
+ object_class->dispose = gs_featured_carousel_dispose;
+
+ widget_class->key_press_event = gs_featured_carousel_key_press_event;
+
+ /**
+ * GsFeaturedCarousel:apps: (nullable)
+ *
+ * The list of featured apps to display in the carousel. This should
+ * typically be 4–8 apps. They will be displayed in the order listed,
+ * so the caller may want to randomise that order first, using
+ * gs_app_list_randomize().
+ *
+ * This may be %NULL if no apps have been set. This is equivalent to
+ * an empty #GsAppList.
+ *
+ * Since: 40
+ */
+ obj_props[PROP_APPS] =
+ g_param_spec_object ("apps", NULL, NULL,
+ GS_TYPE_APP_LIST,
+ 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);
+
+ /**
+ * GsFeaturedCarousel::app-clicked:
+ * @app: the #GsApp which was clicked on
+ *
+ * Emitted when one of the app tiles is clicked. Typically the caller
+ * should display the details of the given app in the callback.
+ *
+ * Since: 40
+ */
+ obj_signals[SIGNAL_APP_CLICKED] =
+ g_signal_new ("app-clicked",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE, 1, GS_TYPE_APP);
+
+ /**
+ * GsFeaturedCarousel::clicked:
+ *
+ * Emitted when the carousel is clicked, and typically emitted shortly
+ * before #GsFeaturedCarousel::app-clicked is emitted. Most callers will
+ * want to connect to #GsFeaturedCarousel::app-clicked instead.
+ *
+ * Since: 40
+ */
+ obj_signals[SIGNAL_CLICKED] =
+ g_signal_new ("clicked",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ widget_class->activate_signal = obj_signals[SIGNAL_CLICKED];
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/Software/gs-featured-carousel.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsFeaturedCarousel, carousel);
+ gtk_widget_class_bind_template_child (widget_class, GsFeaturedCarousel, next_button);
+ gtk_widget_class_bind_template_child (widget_class, GsFeaturedCarousel, next_button_image);
+ gtk_widget_class_bind_template_child (widget_class, GsFeaturedCarousel, previous_button);
+ gtk_widget_class_bind_template_child (widget_class, GsFeaturedCarousel, previous_button_image);
+ gtk_widget_class_bind_template_callback (widget_class, next_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, next_button_direction_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, previous_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, previous_button_direction_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, carousel_clicked_cb);
+}
+
+/**
+ * gs_featured_carousel_new:
+ * @apps: (nullable): a list of apps to display in the carousel, or %NULL
+ *
+ * Create a new #GsFeaturedCarousel and set its initial app list to @apps.
+ *
+ * Returns: (transfer full): a new #GsFeaturedCarousel
+ * Since: 40
+ */
+GtkWidget *
+gs_featured_carousel_new (GsAppList *apps)
+{
+ g_return_val_if_fail (apps == NULL || GS_IS_APP_LIST (apps), NULL);
+
+ return g_object_new (GS_TYPE_FEATURED_CAROUSEL,
+ "apps", apps,
+ NULL);
+}
+
+/**
+ * gs_featured_carousel_get_apps:
+ * @self: a #GsFeaturedCarousel
+ *
+ * Gets the value of #GsFeaturedCarousel:apps.
+ *
+ * Returns: (nullable) (transfer none): list of apps in the carousel, or %NULL
+ * if none are set
+ * Since: 40
+ */
+GsAppList *
+gs_featured_carousel_get_apps (GsFeaturedCarousel *self)
+{
+ g_return_val_if_fail (GS_IS_FEATURED_CAROUSEL (self), NULL);
+
+ return self->apps;
+}
+
+/**
+ * gs_featured_carousel_set_apps:
+ * @self: a #GsFeaturedCarousel
+ * @apps: (nullable) (transfer none): list of apps to display in the carousel,
+ * or %NULL for none
+ *
+ * Set the value of #GsFeaturedCarousel:apps.
+ *
+ * Since: 40
+ */
+void
+gs_featured_carousel_set_apps (GsFeaturedCarousel *self,
+ GsAppList *apps)
+{
+ g_return_if_fail (GS_IS_FEATURED_CAROUSEL (self));
+ g_return_if_fail (apps == NULL || GS_IS_APP_LIST (apps));
+
+ if (apps == self->apps)
+ return;
+
+ stop_rotation_timer (self);
+ gs_container_remove_all (GTK_CONTAINER (self->carousel));
+
+ g_set_object (&self->apps, apps);
+
+ for (guint i = 0; i < gs_app_list_length (apps); i++) {
+ GsApp *app = gs_app_list_index (apps, i);
+ GtkWidget *tile = gs_feature_tile_new (app);
+ gtk_widget_set_hexpand (tile, TRUE);
+ gtk_widget_set_vexpand (tile, TRUE);
+ gtk_widget_set_can_focus (tile, FALSE);
+ g_signal_connect (tile, "clicked",
+ G_CALLBACK (app_tile_clicked_cb), self);
+ gtk_container_add (GTK_CONTAINER (self->carousel), tile);
+ }
+
+ gtk_widget_set_visible (GTK_WIDGET (self->next_button), self->apps != NULL && gs_app_list_length
(self->apps) > 1);
+ gtk_widget_set_visible (GTK_WIDGET (self->previous_button), self->apps != NULL && gs_app_list_length
(self->apps) > 1);
+
+ if (self->apps != NULL && gs_app_list_length (self->apps) > 0)
+ start_rotation_timer (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APPS]);
+}
diff --git a/src/gs-featured-carousel.h b/src/gs-featured-carousel.h
new file mode 100644
index 000000000..aa031e8f3
--- /dev/null
+++ b/src/gs-featured-carousel.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, Inc
+ *
+ * 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-list.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_FEATURED_CAROUSEL (gs_featured_carousel_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsFeaturedCarousel, gs_featured_carousel, GS, FEATURED_CAROUSEL, GtkBox)
+
+GtkWidget *gs_featured_carousel_new (GsAppList *apps);
+
+GsAppList *gs_featured_carousel_get_apps (GsFeaturedCarousel *self);
+void gs_featured_carousel_set_apps (GsFeaturedCarousel *self,
+ GsAppList *apps);
+
+G_END_DECLS
diff --git a/src/gs-featured-carousel.ui b/src/gs-featured-carousel.ui
new file mode 100644
index 000000000..1c2f8b790
--- /dev/null
+++ b/src/gs-featured-carousel.ui
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <requires lib="handy" version="1.0"/>
+ <template class="GsFeaturedCarousel" parent="GtkBox">
+ <property name="halign">fill</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <signal name="clicked" handler="carousel_clicked_cb"/>
+ <style>
+ <class name="featured-carousel"/>
+ </style>
+ <child>
+ <object class="GtkOverlay" id="overlay">
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GsRoundedBin">
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyCarousel" id="carousel">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkButton" id="previous_button">
+ <property name="visible">True</property>
+ <property name="use-underline">True</property>
+ <property name="can-focus">True</property>
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="width-request">56</property>
+ <property name="height-request">56</property>
+ <property name="margin">9</property>
+ <signal name="clicked" handler="previous_button_clicked_cb"/>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">Previous</property>
+ </object>
+ </child>
+ <style>
+ <class name="osd"/>
+ <class name="featured-button-left"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="previous_button_image">
+ <property name="visible">True</property>
+ <property name="icon-name">carousel-arrow-previous-symbolic</property>
+ <property name="icon-size">3</property><!-- GTK_ICON_SIZE_LARGE_TOOLBAR -->
+ <signal name="direction-changed" handler="previous_button_direction_changed_cb"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkButton" id="next_button">
+ <property name="visible">True</property>
+ <property name="use-underline">True</property>
+ <property name="can-focus">True</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="width-request">56</property>
+ <property name="height-request">56</property>
+ <property name="margin">9</property>
+ <signal name="clicked" handler="next_button_clicked_cb"/>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">Next</property>
+ </object>
+ </child>
+ <style>
+ <class name="osd"/>
+ <class name="featured-button-right"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="next_button_image">
+ <property name="visible">True</property>
+ <property name="icon_name">carousel-arrow-next-symbolic</property>
+ <property name="icon_size">3</property><!-- GTK_ICON_SIZE_LARGE_TOOLBAR -->
+ <signal name="direction-changed" handler="next_button_direction_changed_cb"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyCarouselIndicatorDots" id="dots">
+ <property name="carousel">carousel</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">Featured Apps List</property>
+ <property name="accessible-role">grouping</property>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gtk-style.css b/src/gtk-style.css
index 557c435f7..413acef95 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -547,10 +547,38 @@ flowboxchild {
opacity: 0.5;
}
+.featured-carousel rounded-bin {
+ border-radius: 8px;
+}
+
.featured-button-left,
.featured-button-right {
padding: 2px 5px;
- margin: 6px;
+ border-radius: 50%;
+ -gtk-outline-radius: 50%;
+ color: @theme_fg_color;
+}
+
+.featured-carousel button.osd {
+ -gtk-icon-shadow: none;
+}
+
+.featured-carousel button.osd:focus {
+ /* this is @theme_fg_color at 10% opacity, but we can’t use the variable
+ * directly as rgba() requires 4 components */
+ background: rgba(46, 52, 54, 0.1);
+}
+
+.featured-carousel button.osd:hover {
+ /* this is @theme_fg_color at 20% opacity, but we can’t use the variable
+ * directly as rgba() requires 4 components */
+ background: rgba(46, 52, 54, 0.2);
+}
+
+.featured-carousel button.osd:active {
+ /* this is @theme_fg_color at 35% opacity, but we can’t use the variable
+ * directly as rgba() requires 4 components */
+ background: rgba(46, 52, 54, 0.35);
}
.featured-button-left:not(:hover),
diff --git a/src/meson.build b/src/meson.build
index 14c902f58..3a91c8667 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -31,6 +31,7 @@ gnome_software_sources = [
'gs-details-page.c',
'gs-extras-page.c',
'gs-feature-tile.c',
+ 'gs-featured-carousel.c',
'gs-first-run-dialog.c',
'gs-fixed-size-bin.c',
'gs-folders.c',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]