/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright © 2016 Red Hat, Inc. * Copyright © 2019 Endless Mobile, Inc. * * 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 2 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 . * * Authors: * - Felipe Borges * - Philip Withnall */ #include #include #include "carousel.h" #define ARROW_SIZE 20 #define MCT_TYPE_CAROUSEL_LAYOUT (mct_carousel_layout_get_type ()) G_DECLARE_FINAL_TYPE (MctCarouselLayout, mct_carousel_layout, MCT, CAROUSEL_LAYOUT, GtkLayoutManager) struct _MctCarouselItem { GtkButton parent; gint page; }; G_DEFINE_TYPE (MctCarouselItem, mct_carousel_item, GTK_TYPE_BUTTON) GtkWidget * mct_carousel_item_new (void) { return g_object_new (MCT_TYPE_CAROUSEL_ITEM, NULL); } void mct_carousel_item_set_child (MctCarouselItem *self, GtkWidget *child) { g_return_if_fail (MCT_IS_CAROUSEL_ITEM (self)); gtk_button_set_child (GTK_BUTTON (self), child); } static void mct_carousel_item_class_init (MctCarouselItemClass *klass) { gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "carousel-item"); } static void mct_carousel_item_init (MctCarouselItem *self) { } struct _MctCarousel { AdwBin parent; GtkRevealer *revealer; GList *children; gint visible_page; MctCarouselItem *selected_item; GtkWidget *last_box; GtkWidget *arrow; gint arrow_start_x; /* Widgets */ GtkStack *stack; GtkWidget *go_back_button; GtkWidget *go_next_button; GtkStyleProvider *provider; }; G_DEFINE_TYPE (MctCarousel, mct_carousel, ADW_TYPE_BIN) enum { ITEM_ACTIVATED, NUM_SIGNALS }; static guint signals[NUM_SIGNALS] = { 0, }; #define ITEMS_PER_PAGE 3 static gint mct_carousel_item_get_x (MctCarouselItem *item, MctCarousel *carousel) { GtkWidget *widget, *parent; gint width; gdouble dest_x; parent = GTK_WIDGET (carousel->stack); widget = GTK_WIDGET (item); width = gtk_widget_get_allocated_width (widget); if (!gtk_widget_translate_coordinates (widget, parent, width / 2, 0, &dest_x, NULL)) return 0; return CLAMP (dest_x - ARROW_SIZE, 0, gtk_widget_get_allocated_width (parent)); } static void mct_carousel_move_arrow (MctCarousel *self) { GtkStyleContext *context; gchar *css; gint end_x; GtkSettings *settings; gboolean animations; if (!self->selected_item) return; end_x = mct_carousel_item_get_x (self->selected_item, self); context = gtk_widget_get_style_context (self->arrow); if (self->provider) gtk_style_context_remove_provider (context, self->provider); g_clear_object (&self->provider); settings = gtk_widget_get_settings (GTK_WIDGET (self)); g_object_get (settings, "gtk-enable-animations", &animations, NULL); /* Animate the arrow movement if animations are enabled. Otherwise, * jump the arrow to the right location instantly. */ if (animations) { css = g_strdup_printf ("@keyframes arrow_keyframes-%d-%d {\n" " from { margin-left: %dpx; }\n" " to { margin-left: %dpx; }\n" "}\n" "* {\n" " animation-name: arrow_keyframes-%d-%d;\n" "}\n", self->arrow_start_x, end_x, self->arrow_start_x, end_x, self->arrow_start_x, end_x); } else { css = g_strdup_printf ("* { margin-left: %dpx }", end_x); } self->provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ()); gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (self->provider), css, -1); gtk_style_context_add_provider (context, self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_free (css); } static gint get_last_page_number (MctCarousel *self) { if (g_list_length (self->children) == 0) return 0; return ((g_list_length (self->children) - 1) / ITEMS_PER_PAGE); } static void update_buttons_visibility (MctCarousel *self) { gtk_widget_set_visible (self->go_back_button, (self->visible_page > 0)); gtk_widget_set_visible (self->go_next_button, (self->visible_page < get_last_page_number (self))); } /** * mct_carousel_find_item: * @carousel: an MctCarousel instance * @data: user data passed to the comparison function * @func: the function to call for each element. * It should return 0 when the desired element is found * * Finds an MctCarousel item using the supplied function to find the * desired element. * Ideally useful for matching a model object and its correspondent * widget. * * Returns: the found MctCarouselItem, or %NULL if it is not found */ MctCarouselItem * mct_carousel_find_item (MctCarousel *self, gconstpointer data, GCompareFunc func) { GList *list; list = self->children; while (list != NULL) { if (!func (list->data, data)) return list->data; list = list->next; } return NULL; } static void on_item_toggled (MctCarouselItem *item, GdkEvent *event, gpointer user_data) { MctCarousel *self = MCT_CAROUSEL (user_data); mct_carousel_select_item (self, item); } void mct_carousel_select_item (MctCarousel *self, MctCarouselItem *item) { gchar *page_name; gboolean page_changed = TRUE; /* Select first user if none is specified */ if (item == NULL) { if (self->children != NULL) item = self->children->data; else return; } if (self->selected_item != NULL) { page_changed = (self->selected_item->page != item->page); self->arrow_start_x = mct_carousel_item_get_x (self->selected_item, self); } self->selected_item = item; self->visible_page = item->page; g_signal_emit (self, signals[ITEM_ACTIVATED], 0, item); if (!page_changed) { mct_carousel_move_arrow (self); return; } page_name = g_strdup_printf ("%d", self->visible_page); gtk_stack_set_visible_child_name (self->stack, page_name); g_free (page_name); update_buttons_visibility (self); /* mct_carousel_move_arrow is called from on_transition_running */ } static void mct_carousel_select_item_at_index (MctCarousel *self, gint index) { GList *l = NULL; l = g_list_nth (self->children, index); mct_carousel_select_item (self, l->data); } static void mct_carousel_goto_previous_page (GtkWidget *button, gpointer user_data) { MctCarousel *self = MCT_CAROUSEL (user_data); self->visible_page--; if (self->visible_page < 0) self->visible_page = 0; /* Select first item of the page */ mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); } static void mct_carousel_goto_next_page (GtkWidget *button, gpointer user_data) { MctCarousel *self = MCT_CAROUSEL (user_data); gint last_page; last_page = get_last_page_number (self); self->visible_page++; if (self->visible_page > last_page) self->visible_page = last_page; /* Select first item of the page */ mct_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); } void mct_carousel_add (MctCarousel *self, MctCarouselItem *item) { gboolean last_box_is_full; g_return_if_fail (MCT_IS_CAROUSEL (self)); g_return_if_fail (MCT_IS_CAROUSEL_ITEM (item)); self->children = g_list_append (self->children, item); item->page = get_last_page_number (self); g_signal_connect (item, "clicked", G_CALLBACK (on_item_toggled), self); last_box_is_full = ((g_list_length (self->children) - 1) % ITEMS_PER_PAGE == 0); if (last_box_is_full) { g_autofree gchar *page = NULL; page = g_strdup_printf ("%d", item->page); self->last_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 18); gtk_widget_set_hexpand (self->last_box, TRUE); gtk_widget_set_valign (self->last_box, GTK_ALIGN_CENTER); gtk_box_set_homogeneous (GTK_BOX (self->last_box), TRUE); gtk_stack_add_named (self->stack, self->last_box, page); } gtk_box_append (GTK_BOX (self->last_box), GTK_WIDGET (item)); update_buttons_visibility (self); } void mct_carousel_purge_items (MctCarousel *self) { GtkWidget *child; while ((child = gtk_widget_get_first_child (GTK_WIDGET (self->stack))) != NULL) gtk_stack_remove (self->stack, child); g_list_free (self->children); self->children = NULL; self->visible_page = 0; self->selected_item = NULL; } MctCarousel * mct_carousel_new (void) { return g_object_new (MCT_TYPE_CAROUSEL, NULL); } static void mct_carousel_dispose (GObject *object) { MctCarousel *self = MCT_CAROUSEL (object); g_clear_object (&self->provider); if (self->children != NULL) { g_list_free (self->children); self->children = NULL; } G_OBJECT_CLASS (mct_carousel_parent_class)->dispose (object); } static void mct_carousel_class_init (MctCarouselClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); gtk_widget_class_set_template_from_resource (wclass, "/org/freedesktop/MalcontentControl/ui/carousel.ui"); gtk_widget_class_bind_template_child (wclass, MctCarousel, stack); gtk_widget_class_bind_template_child (wclass, MctCarousel, go_back_button); gtk_widget_class_bind_template_child (wclass, MctCarousel, go_next_button); gtk_widget_class_bind_template_child (wclass, MctCarousel, arrow); gtk_widget_class_bind_template_child (wclass, MctCarousel, revealer); gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_previous_page); gtk_widget_class_bind_template_callback (wclass, mct_carousel_goto_next_page); gtk_widget_class_set_layout_manager_type (wclass, MCT_TYPE_CAROUSEL_LAYOUT); object_class->dispose = mct_carousel_dispose; signals[ITEM_ACTIVATED] = g_signal_new ("item-activated", MCT_TYPE_CAROUSEL, G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, MCT_TYPE_CAROUSEL_ITEM); } static void on_transition_running (MctCarousel *self) { if (!gtk_stack_get_transition_running (self->stack)) mct_carousel_move_arrow (self); } static void mct_carousel_init (MctCarousel *self) { GtkStyleProvider *provider; gtk_widget_init_template (GTK_WIDGET (self)); provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ()); gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (provider), "/org/freedesktop/MalcontentControl/ui/carousel.css"); gtk_style_context_add_provider_for_display (gdk_display_get_default (), provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1); g_object_unref (provider); g_signal_connect_swapped (self->stack, "notify::transition-running", G_CALLBACK (on_transition_running), self); } guint mct_carousel_get_item_count (MctCarousel *self) { return g_list_length (self->children); } void mct_carousel_set_revealed (MctCarousel *self, gboolean revealed) { g_return_if_fail (MCT_IS_CAROUSEL (self)); gtk_revealer_set_reveal_child (self->revealer, revealed); } struct _MctCarouselLayout { GtkLayoutManager parent; }; G_DEFINE_FINAL_TYPE (MctCarouselLayout, mct_carousel_layout, GTK_TYPE_LAYOUT_MANAGER) static void mct_carousel_layout_measure (GtkLayoutManager *layout_manager, GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { MctCarousel *carousel; g_assert (MCT_IS_CAROUSEL (widget)); carousel = MCT_CAROUSEL (widget); gtk_widget_measure (GTK_WIDGET (carousel->revealer), orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); } static void mct_carousel_layout_allocate (GtkLayoutManager *layout_manager, GtkWidget *widget, int width, int height, int baseline) { MctCarousel *carousel; g_assert (MCT_IS_CAROUSEL (widget)); carousel = MCT_CAROUSEL (widget); gtk_widget_allocate (GTK_WIDGET (carousel->revealer), width, height, baseline, NULL); if (carousel->selected_item == NULL) return; if (gtk_stack_get_transition_running (carousel->stack)) return; carousel->arrow_start_x = mct_carousel_item_get_x (carousel->selected_item, carousel); mct_carousel_move_arrow (carousel); } static void mct_carousel_layout_class_init (MctCarouselLayoutClass *klass) { GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass); layout_manager_class->measure = mct_carousel_layout_measure; layout_manager_class->allocate = mct_carousel_layout_allocate; } static void mct_carousel_layout_init (MctCarouselLayout *self) { }