/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright 2016 (c) Red Hat, 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 . * * Author: Felipe Borges */ #include "cc-carousel.h" #include #include #define ARROW_SIZE 20 struct _CcCarouselItem { GtkRadioButton parent; gint page; }; G_DEFINE_TYPE (CcCarouselItem, cc_carousel_item, GTK_TYPE_RADIO_BUTTON) GtkWidget * cc_carousel_item_new (void) { return g_object_new (CC_TYPE_CAROUSEL_ITEM, NULL); } static void cc_carousel_item_class_init (CcCarouselItemClass *klass) { } static void cc_carousel_item_init (CcCarouselItem *self) { gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE); gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)), "carousel-item"); } struct _CcCarousel { GtkRevealer parent; GList *children; gint visible_page; CcCarouselItem *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 (CcCarousel, cc_carousel, GTK_TYPE_REVEALER) enum { ITEM_ACTIVATED, NUM_SIGNALS }; static guint signals[NUM_SIGNALS] = { 0, }; #define ITEMS_PER_PAGE 3 static gint cc_carousel_item_get_x (CcCarouselItem *item, CcCarousel *carousel) { GtkWidget *widget, *parent; gint width; gint dest_x; parent = GTK_WIDGET (carousel->stack); widget = GTK_WIDGET (item); width = gtk_widget_get_allocated_width (widget); gtk_widget_translate_coordinates (widget, parent, width / 2, 0, &dest_x, NULL); return CLAMP (dest_x - ARROW_SIZE, 0, gtk_widget_get_allocated_width (parent)); } static void cc_carousel_move_arrow (CcCarousel *self) { GtkStyleContext *context; gchar *css; gint end_x; GtkSettings *settings; gboolean animations; if (!self->selected_item) return; end_x = cc_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, NULL); gtk_style_context_add_provider (context, self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_free (css); } static gint get_last_page_number (CcCarousel *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 (CcCarousel *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))); } /** * cc_carousel_find_item: * @carousel: an CcCarousel instance * @data: user data passed to the comparation function * @func: the function to call for each element. * It should return 0 when the desired element is found * * Finds an CcCarousel item using the supplied function to find the * desired element. * Ideally useful for matching a model object and its correspondent * widget. * * Returns: the found CcCarouselItem, or %NULL if it is not found */ CcCarouselItem * cc_carousel_find_item (CcCarousel *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 (CcCarouselItem *item, GdkEvent *event, gpointer user_data) { CcCarousel *self = CC_CAROUSEL (user_data); cc_carousel_select_item (self, item); } void cc_carousel_select_item (CcCarousel *self, CcCarouselItem *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 = cc_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) { cc_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); /* cc_carousel_move_arrow is called from on_transition_running */ } static void cc_carousel_select_item_at_index (CcCarousel *self, gint index) { GList *l = NULL; l = g_list_nth (self->children, index); cc_carousel_select_item (self, l->data); } static void cc_carousel_goto_previous_page (GtkWidget *button, gpointer user_data) { CcCarousel *self = CC_CAROUSEL (user_data); self->visible_page--; if (self->visible_page < 0) self->visible_page = 0; /* Select first item of the page */ cc_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); } static void cc_carousel_goto_next_page (GtkWidget *button, gpointer user_data) { CcCarousel *self = CC_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 */ cc_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE); } static void cc_carousel_add (GtkContainer *container, GtkWidget *widget) { CcCarousel *self = CC_CAROUSEL (container); gboolean last_box_is_full; if (!CC_IS_CAROUSEL_ITEM (widget)) { GTK_CONTAINER_CLASS (cc_carousel_parent_class)->add (container, widget); return; } gtk_style_context_add_class (gtk_widget_get_style_context (widget), "menu"); gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); self->children = g_list_append (self->children, widget); CC_CAROUSEL_ITEM (widget)->page = get_last_page_number (self); if (self->selected_item != NULL) gtk_radio_button_join_group (GTK_RADIO_BUTTON (widget), GTK_RADIO_BUTTON (self->selected_item)); g_signal_connect (widget, "button-press-event", 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) { gchar *page; page = g_strdup_printf ("%d", CC_CAROUSEL_ITEM (widget)->page); self->last_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_show (self->last_box); gtk_widget_set_valign (self->last_box, GTK_ALIGN_CENTER); gtk_stack_add_named (self->stack, self->last_box, page); } gtk_widget_show_all (widget); gtk_box_pack_start (GTK_BOX (self->last_box), widget, TRUE, FALSE, 10); update_buttons_visibility (self); } void cc_carousel_purge_items (CcCarousel *self) { gtk_container_forall (GTK_CONTAINER (self->stack), (GtkCallback) gtk_widget_destroy, NULL); g_list_free (self->children); self->children = NULL; self->visible_page = 0; self->selected_item = NULL; } CcCarousel * cc_carousel_new (void) { return g_object_new (CC_TYPE_CAROUSEL, NULL); } static void cc_carousel_class_init (CcCarouselClass *klass) { GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); gtk_widget_class_set_template_from_resource (wclass, "/org/gnome/control-center/user-accounts/cc-carousel.ui"); gtk_widget_class_bind_template_child (wclass, CcCarousel, stack); gtk_widget_class_bind_template_child (wclass, CcCarousel, go_back_button); gtk_widget_class_bind_template_child (wclass, CcCarousel, go_next_button); gtk_widget_class_bind_template_child (wclass, CcCarousel, arrow); gtk_widget_class_bind_template_callback (wclass, cc_carousel_goto_previous_page); gtk_widget_class_bind_template_callback (wclass, cc_carousel_goto_next_page); container_class->add = cc_carousel_add; signals[ITEM_ACTIVATED] = g_signal_new ("item-activated", CC_TYPE_CAROUSEL, G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, CC_TYPE_CAROUSEL_ITEM); } static void on_size_allocate (CcCarousel *self) { if (self->selected_item == NULL) return; if (gtk_stack_get_transition_running (self->stack)) return; self->arrow_start_x = cc_carousel_item_get_x (self->selected_item, self); cc_carousel_move_arrow (self); } static void on_transition_running (CcCarousel *self) { if (!gtk_stack_get_transition_running (self->stack)) cc_carousel_move_arrow (self); } static void cc_carousel_init (CcCarousel *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/gnome/control-center/user-accounts/carousel.css"); gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref (provider); g_signal_connect_swapped (self->stack, "size-allocate", G_CALLBACK (on_size_allocate), self); g_signal_connect_swapped (self->stack, "notify::transition-running", G_CALLBACK (on_transition_running), self); } guint cc_carousel_get_item_count (CcCarousel *self) { return g_list_length (self->children); }