gtk_widget_translate_coordinates() doesn't have to always set the dest_x argument. Let's initialize the argument before use to prevent usage of uninitialized value, which also prevents "Conditional jump or move depends on uninitialised value(s)" messages from valgrind.
439 lines
14 KiB
C
439 lines
14 KiB
C
/* -*- 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Author: Felipe Borges <felipeborges@gnome.org>
|
|
*/
|
|
|
|
#include "cc-carousel.h"
|
|
|
|
#include <glib-object.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#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 = 0;
|
|
|
|
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 {\n"
|
|
" from { margin-left: %dpx; }\n"
|
|
" to { margin-left: %dpx; }\n"
|
|
"}\n"
|
|
"* {\n"
|
|
" animation-name: arrow_keyframes-%d;\n"
|
|
"}\n",
|
|
end_x, self->arrow_start_x, end_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)
|
|
{
|
|
gboolean page_changed = TRUE;
|
|
GList *children;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
children = gtk_container_get_children (GTK_CONTAINER (self->stack));
|
|
gtk_stack_set_visible_child (self->stack, GTK_WIDGET (g_list_nth_data (children, self->visible_page)));
|
|
|
|
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) {
|
|
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_container_add (GTK_CONTAINER (self->stack), self->last_box);
|
|
}
|
|
|
|
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_dispose (GObject *object)
|
|
{
|
|
CcCarousel *self = CC_CAROUSEL (object);
|
|
|
|
g_clear_object (&self->provider);
|
|
if (self->children != NULL) {
|
|
g_list_free (self->children);
|
|
self->children = NULL;
|
|
}
|
|
|
|
G_OBJECT_CLASS (cc_carousel_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
cc_carousel_class_init (CcCarouselClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (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);
|
|
|
|
object_class->dispose = cc_carousel_dispose;
|
|
|
|
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);
|
|
}
|