*
* Copyright 2025 Alexander Vanhee
*
* Adapted from gs-screenshot-carousel.c
*
* 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>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <adwaita.h>
#include <bge.h>
#include <bzvala.h>
#include <glib/gi18n.h>
#include "bz-screenshot-page.h"
#include "bz-screenshots-carousel.h"
#include "bz-window.h"
#include "template-callbacks.h"
#define LIGHT_CLASS "screenshot-carousel-light"
#define DARK_CLASS "screenshot-carousel-dark"
#define LIGHT_MIX_PERCENTAGE 15
#define DARK_MIX_PERCENTAGE 4
struct _BzScreenshotsCarousel
{
GtkWidget parent_instance;
BgeCarousel *carousel;
GtkButton *prev_button;
GtkWidget *prev_button_revealer;
GtkButton *next_button;
GtkWidget *next_button_revealer;
GtkSingleSelection *selection;
GListModel *model;
GListModel *captions;
gboolean compact;
char *light_accent_color;
char *dark_accent_color;
char *widget_css_class;
GtkCssProvider *css;
gulong items_changed_id;
};
G_DEFINE_FINAL_TYPE (BzScreenshotsCarousel, bz_screenshots_carousel, GTK_TYPE_WIDGET)
enum
{
PROP_0,
PROP_MODEL,
PROP_CAPTIONS,
PROP_COMPACT,
PROP_LIGHT_ACCENT_COLOR,
PROP_DARK_ACCENT_COLOR,
N_PROPS
};
static GParamSpec *properties[N_PROPS];
static void refresh_css (BzScreenshotsCarousel *self);
static void clear_css (BzScreenshotsCarousel *self);
static void
update_button_visibility (BzScreenshotsCarousel *self)
{
gdouble position;
guint n_pages;
if (!self->carousel)
return;
position = gtk_single_selection_get_selected (self->selection);
n_pages = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
if (n_pages == 0)
{
gtk_revealer_set_reveal_child (GTK_REVEALER (self->prev_button_revealer), FALSE);
gtk_revealer_set_reveal_child (GTK_REVEALER (self->next_button_revealer), FALSE);
return;
}
gtk_revealer_set_reveal_child (GTK_REVEALER (self->prev_button_revealer), position >= 0.5);
gtk_revealer_set_reveal_child (GTK_REVEALER (self->next_button_revealer), position < n_pages - 1.5);
}
static void
carousel_navigate (BzScreenshotsCarousel *self,
AdwNavigationDirection direction)
{
guint n_children = 0;
gdouble position = 0.0;
n_children = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
position = gtk_single_selection_get_selected (self->selection);
position += (direction == ADW_NAVIGATION_DIRECTION_BACK) ? -1 : 1;
position = round (position);
position = MIN (position, n_children - 1);
position = MAX (0, position);
gtk_single_selection_set_selected (self->selection, position);
}
static void
on_prev_clicked (BzScreenshotsCarousel *self)
{
if (!self->carousel)
return;
carousel_navigate (self, ADW_NAVIGATION_DIRECTION_BACK);
}
static void
on_next_clicked (BzScreenshotsCarousel *self)
{
if (!self->carousel)
return;
carousel_navigate (self, ADW_NAVIGATION_DIRECTION_FORWARD);
}
static void
on_notify_selected (BzScreenshotsCarousel *self)
{
update_button_visibility (self);
}
static void
on_notify_model (BzScreenshotsCarousel *self)
{
guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
if (!self->compact && n_items >= 3)
gtk_single_selection_set_selected (self->selection, 1);
update_button_visibility (self);
}
static void
open_screenshot_at_index (BzScreenshotsCarousel *self, guint index)
{
BzScreenshotPage *page = NULL;
GtkWidget *picture = NULL;
GtkWidget *child = NULL;
GtkWidget *root = NULL;
guint n_items;
if (!self->model)
return;
n_items = g_list_model_get_n_items (self->model);
if (index >= n_items)
return;
child = bge_carousel_get_nth_page (self->carousel, index);
if (child != NULL)
picture = gtk_widget_get_first_child (child);
root = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
if (!BZ_IS_WINDOW (root))
return;
page = BZ_SCREENSHOT_PAGE (bz_screenshot_page_new (self->model, self->captions, index, picture));
bz_window_open_screenshot_page (BZ_WINDOW (root), page);
}
static void
on_screenshot_clicked (BzDecoratedScreenshot *screenshot,
BzScreenshotsCarousel *self)
{
BzAsyncTexture *async_texture = NULL;
guint index = 0;
guint n_items = 0;
if (!self->model)
return;
async_texture = bz_decorated_screenshot_get_async_texture (screenshot);
if (async_texture == NULL)
return;
n_items = g_list_model_get_n_items (self->model);
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzAsyncTexture) item = NULL;
item = g_list_model_get_item (self->model, i);
if (item == async_texture)
{
index = i;
break;
}
}
open_screenshot_at_index (self, index);
}
static void
on_expand_clicked (BzScreenshotsCarousel *self)
{
gdouble position;
guint index;
guint n_pages;
if (!self->carousel || !self->model)
return;
n_pages = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
if (n_pages == 0)
return;
position = gtk_single_selection_get_selected (self->selection);
index = (guint) round (position);
index = MIN (index, n_pages - 1);
open_screenshot_at_index (self, index);
}
static int
get_carousel_height (BzScreenshotsCarousel *self)
{
return self->compact ? 250 : 375;
}
static void
on_screenshot_focus_changed (BzDecoratedScreenshot *screenshot,
GParamSpec *pspec,
BzScreenshotsCarousel *self)
{
if (!self->carousel)
return;
if (gtk_widget_has_focus (GTK_WIDGET (screenshot)))
{
BzAsyncTexture *texture = NULL;
guint n_items = 0;
texture = bz_decorated_screenshot_get_async_texture (screenshot);
n_items = g_list_model_get_n_items (self->model);
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzAsyncTexture) item = NULL;
item = g_list_model_get_item (self->model, i);
if (item == texture)
{
gtk_single_selection_set_selected (self->selection, i);
break;
}
}
}
}
static GtkWidget *
on_create_widget (BzScreenshotsCarousel *self,
BzAsyncTexture *item,
BgeCarousel *carousel)
{
GtkWidget *screenshot = NULL;
g_autofree char *caption = NULL;
const char *shot_caption = NULL;
guint n_items = 0;
guint index = 0;
screenshot = g_object_new (BZ_TYPE_DECORATED_SCREENSHOT,
"async-texture", item,
NULL);
n_items = g_list_model_get_n_items (self->model);
for (index = 0; index < n_items; index++)
{
g_autoptr (BzAsyncTexture) it = NULL;
it = g_list_model_get_item (self->model, index);
if (it == item)
break;
}
if (self->captions != NULL && index < g_list_model_get_n_items (self->captions))
{
g_autoptr (GtkStringObject) caption_obj = NULL;
caption_obj = g_list_model_get_item (self->captions, index);
if (caption_obj != NULL)
shot_caption = gtk_string_object_get_string (caption_obj);
}
if (shot_caption != NULL && *shot_caption != '\0')
caption = g_strdup_printf (_ ("Screenshot %u %s"), index + 1, shot_caption);
else
caption = g_strdup_printf (_ ("Screenshot %u"), index + 1);
gtk_accessible_update_property (GTK_ACCESSIBLE (screenshot),
GTK_ACCESSIBLE_PROPERTY_LABEL, caption,
-1);
g_signal_connect (screenshot, "clicked",
G_CALLBACK (on_screenshot_clicked), self);
g_signal_connect (screenshot, "notify::has-focus",
G_CALLBACK (on_screenshot_focus_changed), self);
return screenshot;
}
static void
on_remove_widget (BzScreenshotsCarousel *self,
BzDecoratedScreenshot *screenshot,
BzAsyncTexture *item,
BgeCarousel *carousel)
{
}
static void
dark_changed (BzScreenshotsCarousel *self,
GParamSpec *pspec,
AdwStyleManager *mgr)
{
gboolean is_dark;
if (self->css == NULL)
return;
is_dark = adw_style_manager_get_dark (adw_style_manager_get_default ());
gtk_widget_remove_css_class (GTK_WIDGET (self), LIGHT_CLASS);
gtk_widget_remove_css_class (GTK_WIDGET (self), DARK_CLASS);
gtk_widget_add_css_class (GTK_WIDGET (self), is_dark ? DARK_CLASS : LIGHT_CLASS);
}
static void
bz_screenshots_carousel_dispose (GObject *object)
{
BzScreenshotsCarousel *self = BZ_SCREENSHOTS_CAROUSEL (object);
GtkWidget *root_child = gtk_widget_get_first_child (GTK_WIDGET (self));
if (self->model && self->items_changed_id)
{
g_signal_handler_disconnect (self->model, self->items_changed_id);
self->items_changed_id = 0;
}
g_clear_object (&self->model);
g_clear_object (&self->captions);
clear_css (self);
g_clear_pointer (&self->light_accent_color, g_free);
g_clear_pointer (&self->dark_accent_color, g_free);
g_clear_pointer (&self->widget_css_class, g_free);
if (root_child != NULL)
gtk_widget_unparent (root_child);
gtk_widget_dispose_template (GTK_WIDGET (self), BZ_TYPE_SCREENSHOTS_CAROUSEL);
G_OBJECT_CLASS (bz_screenshots_carousel_parent_class)->dispose (object);
}
static void
bz_screenshots_carousel_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzScreenshotsCarousel *self = BZ_SCREENSHOTS_CAROUSEL (object);
switch (prop_id)
{
case PROP_MODEL:
g_value_set_object (value, self->model);
break;
case PROP_CAPTIONS:
g_value_set_object (value, self->captions);
break;
case PROP_COMPACT:
g_value_set_boolean (value, self->compact);
break;
case PROP_LIGHT_ACCENT_COLOR:
g_value_set_string (value, self->light_accent_color);
break;
case PROP_DARK_ACCENT_COLOR:
g_value_set_string (value, self->dark_accent_color);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_screenshots_carousel_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
BzScreenshotsCarousel *self = BZ_SCREENSHOTS_CAROUSEL (object);
switch (prop_id)
{
case PROP_MODEL:
bz_screenshots_carousel_set_model (self, g_value_get_object (value));
break;
case PROP_CAPTIONS:
bz_screenshots_carousel_set_captions (self, g_value_get_object (value));
break;
case PROP_COMPACT:
bz_screenshots_carousel_set_compact (self, g_value_get_boolean (value));
break;
case PROP_LIGHT_ACCENT_COLOR:
bz_screenshots_carousel_set_light_accent_color (self, g_value_get_string (value));
break;
case PROP_DARK_ACCENT_COLOR:
bz_screenshots_carousel_set_dark_accent_color (self, g_value_get_string (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_screenshots_carousel_class_init (BzScreenshotsCarouselClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = bz_screenshots_carousel_dispose;
object_class->get_property = bz_screenshots_carousel_get_property;
object_class->set_property = bz_screenshots_carousel_set_property;
properties[PROP_MODEL] =
g_param_spec_object ("model",
NULL,
NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
properties[PROP_CAPTIONS] =
g_param_spec_object ("captions",
NULL,
NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
properties[PROP_COMPACT] =
g_param_spec_boolean ("compact",
NULL,
NULL,
FALSE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
properties[PROP_LIGHT_ACCENT_COLOR] =
g_param_spec_string ("light-accent-color",
NULL,
NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
properties[PROP_DARK_ACCENT_COLOR] =
g_param_spec_string ("dark-accent-color",
NULL,
NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, properties);
gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-screenshots-carousel.ui");
bz_widget_class_bind_all_util_callbacks (widget_class);
gtk_widget_class_bind_template_child (widget_class, BzScreenshotsCarousel, carousel);
gtk_widget_class_bind_template_child (widget_class, BzScreenshotsCarousel, prev_button);
gtk_widget_class_bind_template_child (widget_class, BzScreenshotsCarousel, prev_button_revealer);
gtk_widget_class_bind_template_child (widget_class, BzScreenshotsCarousel, next_button);
gtk_widget_class_bind_template_child (widget_class, BzScreenshotsCarousel, next_button_revealer);
gtk_widget_class_bind_template_child (widget_class, BzScreenshotsCarousel, selection);
gtk_widget_class_bind_template_callback (widget_class, on_prev_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_next_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_notify_selected);
gtk_widget_class_bind_template_callback (widget_class, on_notify_model);
gtk_widget_class_bind_template_callback (widget_class, on_create_widget);
gtk_widget_class_bind_template_callback (widget_class, on_remove_widget);
gtk_widget_class_bind_template_callback (widget_class, on_expand_clicked);
gtk_widget_class_bind_template_callback (widget_class, get_carousel_height);
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
bz_screenshots_carousel_init (BzScreenshotsCarousel *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
g_signal_connect_object (
adw_style_manager_get_default (),
"notify::dark",
G_CALLBACK (dark_changed),
self,
G_CONNECT_SWAPPED);
}
GtkWidget *
bz_screenshots_carousel_new (void)
{
return g_object_new (BZ_TYPE_SCREENSHOTS_CAROUSEL, NULL);
}
void
bz_screenshots_carousel_set_model (BzScreenshotsCarousel *self, GListModel *model)
{
g_return_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self));
g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
if (self->model == model)
return;
if (self->model && self->items_changed_id)
{
g_signal_handler_disconnect (self->model, self->items_changed_id);
self->items_changed_id = 0;
}
g_clear_object (&self->model);
if (model)
{
self->model = g_object_ref (model);
gtk_single_selection_set_selected (self->selection, 0);
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]);
}
GListModel *
bz_screenshots_carousel_get_model (BzScreenshotsCarousel *self)
{
g_return_val_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self), NULL);
return self->model;
}
void
bz_screenshots_carousel_set_captions (BzScreenshotsCarousel *self, GListModel *captions)
{
g_clear_object (&self->captions);
if (captions)
self->captions = g_object_ref (captions);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CAPTIONS]);
}
void
bz_screenshots_carousel_set_compact (BzScreenshotsCarousel *self, gboolean compact)
{
g_return_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self));
if (self->compact == compact)
return;
self->compact = compact;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMPACT]);
}
gboolean
bz_screenshots_carousel_get_compact (BzScreenshotsCarousel *self)
{
g_return_val_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self), FALSE);
return self->compact;
}
void
bz_screenshots_carousel_set_light_accent_color (BzScreenshotsCarousel *self,
const char *color)
{
g_return_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self));
if (color == self->light_accent_color ||
(color != NULL &&
g_strcmp0 (self->light_accent_color, color) == 0))
return;
g_clear_pointer (&self->light_accent_color, g_free);
if (color != NULL)
self->light_accent_color = g_strdup (color);
refresh_css (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LIGHT_ACCENT_COLOR]);
}
const char *
bz_screenshots_carousel_get_light_accent_color (BzScreenshotsCarousel *self)
{
g_return_val_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self), NULL);
return self->light_accent_color;
}
void
bz_screenshots_carousel_set_dark_accent_color (BzScreenshotsCarousel *self,
const char *color)
{
g_return_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self));
if (color == self->dark_accent_color ||
(color != NULL &&
g_strcmp0 (self->dark_accent_color, color) == 0))
return;
g_clear_pointer (&self->dark_accent_color, g_free);
if (color != NULL)
self->dark_accent_color = g_strdup (color);
refresh_css (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DARK_ACCENT_COLOR]);
}
const char *
bz_screenshots_carousel_get_dark_accent_color (BzScreenshotsCarousel *self)
{
g_return_val_if_fail (BZ_IS_SCREENSHOTS_CAROUSEL (self), NULL);
return self->dark_accent_color;
}
static void
refresh_css (BzScreenshotsCarousel *self)
{
g_autofree char *css_string = NULL;
g_autofree char *light_bg = NULL;
g_autofree char *dark_bg = NULL;
gboolean is_dark;
clear_css (self);
if (self->light_accent_color == NULL &&
self->dark_accent_color == NULL)
return;
self->widget_css_class = g_strdup_printf ("screenshot-carousel-%p", (void *) self);
gtk_widget_add_css_class (GTK_WIDGET (self), self->widget_css_class);
if (self->light_accent_color != NULL && self->dark_accent_color != NULL)
light_bg = g_strdup_printf ("color-mix(in srgb, %s %d%%, rgb(255,255,255))",
self->light_accent_color, LIGHT_MIX_PERCENTAGE);
else if (self->light_accent_color != NULL)
light_bg = g_strdup (self->light_accent_color);
else if (self->dark_accent_color != NULL)
light_bg = g_strdup_printf ("color-mix(in srgb, %s %d%%, rgb(255,255,255))",
self->dark_accent_color, LIGHT_MIX_PERCENTAGE);
if (self->light_accent_color != NULL && self->dark_accent_color != NULL)
dark_bg = g_strdup_printf ("color-mix(in srgb, %s %d%%, rgb(29,29,32))",
self->dark_accent_color, DARK_MIX_PERCENTAGE);
else if (self->dark_accent_color != NULL)
dark_bg = g_strdup (self->dark_accent_color);
else if (self->light_accent_color != NULL)
dark_bg = g_strdup_printf ("color-mix(in srgb, %s %d%%, rgb(29,29,32))",
self->light_accent_color, DARK_MIX_PERCENTAGE);
css_string = g_strdup_printf (
".%s.%s{background-color:%s;}\n"
".%s.%s{background-color:%s;}",
self->widget_css_class, LIGHT_CLASS, light_bg,
self->widget_css_class, DARK_CLASS, dark_bg);
self->css = gtk_css_provider_new ();
gtk_css_provider_load_from_string (self->css, css_string);
gtk_style_context_add_provider_for_display (
gdk_display_get_default (),
GTK_STYLE_PROVIDER (self->css),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
is_dark = adw_style_manager_get_dark (adw_style_manager_get_default ());
gtk_widget_add_css_class (GTK_WIDGET (self), is_dark ? DARK_CLASS : LIGHT_CLASS);
}
static void
clear_css (BzScreenshotsCarousel *self)
{
gtk_widget_remove_css_class (GTK_WIDGET (self), LIGHT_CLASS);
gtk_widget_remove_css_class (GTK_WIDGET (self), DARK_CLASS);
if (self->widget_css_class != NULL)
{
gtk_widget_remove_css_class (GTK_WIDGET (self), self->widget_css_class);
g_clear_pointer (&self->widget_css_class, g_free);
}
if (self->css != NULL)
gtk_style_context_remove_provider_for_display (
gdk_display_get_default (),
GTK_STYLE_PROVIDER (self->css));
g_clear_pointer (&self->css, g_object_unref);
}