From 74aa98b7c81b5a59577f2180352efdbce7235bcd Mon Sep 17 00:00:00 2001 From: Bastien Nocera Date: Mon, 30 Apr 2012 19:24:14 +0100 Subject: [PATCH] shell: Add animated notebook Powered by gtk-clutter. --- configure.ac | 2 +- shell/Makefile.am | 7 + shell/TODO | 4 + shell/cc-notebook.c | 466 ++++++++++++++++++++++++++++++++++++++++++ shell/cc-notebook.h | 69 +++++++ shell/test-notebook.c | 92 +++++++++ 6 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 shell/TODO create mode 100644 shell/cc-notebook.c create mode 100644 shell/cc-notebook.h create mode 100644 shell/test-notebook.c diff --git a/configure.ac b/configure.ac index beab7cf84..a4528d920 100644 --- a/configure.ac +++ b/configure.ac @@ -91,7 +91,7 @@ COMMON_MODULES="gtk+-3.0 >= $GTK_REQUIRED_VERSION PKG_CHECK_MODULES(LIBGNOME_CONTROL_CENTER, $COMMON_MODULES) PKG_CHECK_MODULES(LIBLANGUAGE, $COMMON_MODULES gnome-desktop-3.0 fontconfig) PKG_CHECK_MODULES(LIBSHORTCUTS, $COMMON_MODULES x11) -PKG_CHECK_MODULES(SHELL, $COMMON_MODULES libgnome-menu-3.0 gio-unix-2.0 x11) +PKG_CHECK_MODULES(SHELL, $COMMON_MODULES libgnome-menu-3.0 gio-unix-2.0 x11 clutter-gtk-1.0) PKG_CHECK_MODULES(BACKGROUND_PANEL, $COMMON_MODULES libxml-2.0 gnome-desktop-3.0 gdk-pixbuf-2.0 >= $GDKPIXBUF_REQUIRED_VERSION) PKG_CHECK_MODULES(DATETIME_PANEL, $COMMON_MODULES diff --git a/shell/Makefile.am b/shell/Makefile.am index 58117f8e4..d59ba3aba 100644 --- a/shell/Makefile.am +++ b/shell/Makefile.am @@ -19,6 +19,8 @@ gnome_control_center_SOURCES = \ cc-shell-log.h \ gnome-control-center.c \ gnome-control-center.h \ + cc-notebook.c \ + cc-notebook.h \ shell-search-renderer.c \ shell-search-renderer.h \ cc-shell-category-view.c \ @@ -67,6 +69,11 @@ directory_in_files = gnomecc.directory.in directory_DATA = $(directory_in_files:.directory.in=.directory) @INTLTOOL_DIRECTORY_RULE@ +noinst_PROGRAMS = test-notebook +test_notebook_SOURCES = test-notebook.c cc-notebook.c cc-notebook.h +test_notebook_CFLAGS = $(SHELL_CFLAGS) +test_notebook_LDADD = $(SHELL_LIBS) + EXTRA_DIST = \ $(ui_DATA) \ gnome-control-center.desktop.in.in \ diff --git a/shell/TODO b/shell/TODO new file mode 100644 index 000000000..10a6ddb23 --- /dev/null +++ b/shell/TODO @@ -0,0 +1,4 @@ +- Make transitions between pages always be contiguous + the children list order affects the paint order - so you can use set_child_above_sibling/set_child_below_sibling +- Add a scroll direction to the animation + (back to parent to the left, to a child to the right) diff --git a/shell/cc-notebook.c b/shell/cc-notebook.c new file mode 100644 index 000000000..da6e9cf4d --- /dev/null +++ b/shell/cc-notebook.c @@ -0,0 +1,466 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright © 2012 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Written by: + * Bastien Nocera + * Emmanuele Bassi + */ + +#include "config.h" + +#include +#include +#include + +#include "cc-notebook.h" + +#define GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CC_TYPE_NOTEBOOK, CcNotebookPrivate)) + +/* + * Structure: + * + * Notebook + * +---- GtkClutterEmbed + * +---- ClutterStage + * +---- ClutterScrollActor:scroll + * +---- ClutterActor:bin + * +---- ClutterActor:frame + * +---- GtkClutterActor:embed + * + * the frame element is needed to make the GtkClutterActor contents fill the allocation + */ + +struct _CcNotebookPrivate +{ + GtkWidget *embed; + + ClutterActor *stage; + ClutterActor *scroll; + ClutterActor *bin; + + int last_width; + + GtkWidget *selected_page; + GList *pages; /* GList of GtkWidgets */ +}; + +enum +{ + PROP_0, + PROP_CURRENT_PAGE, + LAST_PROP +}; + +static GParamSpec *obj_props[LAST_PROP] = { NULL, }; + +G_DEFINE_TYPE (CcNotebook, cc_notebook, GTK_TYPE_BOX) + +static void +cc_notebook_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcNotebookPrivate *priv = CC_NOTEBOOK (gobject)->priv; + + switch (prop_id) { + case PROP_CURRENT_PAGE: + g_value_set_pointer (value, priv->selected_page); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +cc_notebook_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcNotebook *self = CC_NOTEBOOK (gobject); + + switch (prop_id) { + case PROP_CURRENT_PAGE: + cc_notebook_select_page (self, g_value_get_pointer (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static GtkSizeRequestMode +cc_notebook_get_request_mode (GtkWidget *widget) +{ + CcNotebook *notebook; + GtkWidget *target; + + notebook = CC_NOTEBOOK (widget); + + target = notebook->priv->selected_page ? notebook->priv->selected_page : notebook->priv->embed; + + return gtk_widget_get_request_mode (target); +} + +static void +cc_notebook_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + CcNotebook *notebook; + GList *l; + + notebook = CC_NOTEBOOK (widget); + + if (notebook->priv->selected_page == NULL) { + gtk_widget_get_preferred_height (notebook->priv->selected_page, minimum_height, natural_height); + return; + } + + *minimum_height = 0; + *natural_height = 0; + for (l = notebook->priv->pages; l != NULL; l = l->next) { + GtkWidget *page = l->data; + int page_min, page_nat; + + gtk_widget_get_preferred_height (page, &page_min, &page_nat); + *minimum_height = MAX(page_min, *minimum_height); + *natural_height = MAX(page_nat, *natural_height); + } +} + +static void +cc_notebook_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width) +{ + CcNotebook *notebook; + GList *l; + + notebook = CC_NOTEBOOK (widget); + + if (notebook->priv->selected_page == NULL) { + gtk_widget_get_preferred_width_for_height (notebook->priv->selected_page, height, minimum_width, natural_width); + return; + } + + *minimum_width = 0; + *natural_width = 0; + for (l = notebook->priv->pages; l != NULL; l = l->next) { + GtkWidget *page = l->data; + int page_min, page_nat; + + gtk_widget_get_preferred_width_for_height (page, height, &page_min, &page_nat); + *minimum_width = MAX(page_min, *minimum_width); + *natural_width = MAX(page_nat, *natural_width); + } +} + +static void +cc_notebook_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + CcNotebook *notebook; + GList *l; + + notebook = CC_NOTEBOOK (widget); + + if (notebook->priv->selected_page == NULL) { + gtk_widget_get_preferred_height (notebook->priv->selected_page, minimum_width, natural_width); + return; + } + + *minimum_width = 0; + *natural_width = 0; + for (l = notebook->priv->pages; l != NULL; l = l->next) { + GtkWidget *page = l->data; + int page_min, page_nat; + + gtk_widget_get_preferred_width (page, &page_min, &page_nat); + *minimum_width = MAX(page_min, *minimum_width); + *natural_width = MAX(page_nat, *natural_width); + } +} + +static void +cc_notebook_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum_height, + gint *natural_height) +{ + CcNotebook *notebook; + GList *l; + + notebook = CC_NOTEBOOK (widget); + + if (notebook->priv->selected_page == NULL) { + gtk_widget_get_preferred_height_for_width (notebook->priv->selected_page, width, minimum_height, natural_height); + return; + } + + *minimum_height = 0; + *natural_height = 0; + for (l = notebook->priv->pages; l != NULL; l = l->next) { + GtkWidget *page = l->data; + int page_min, page_nat; + + gtk_widget_get_preferred_height_for_width (page, width, &page_min, &page_nat); + *minimum_height = MAX(page_min, *minimum_height); + *natural_height = MAX(page_nat, *natural_height); + } +} + +static void +cc_notebook_class_init (CcNotebookClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + g_type_class_add_private (klass, sizeof (CcNotebookPrivate)); + + obj_props[PROP_CURRENT_PAGE] = + g_param_spec_pointer (g_intern_static_string ("current-page"), + "Current Page", + "The currently selected page widget", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + gobject_class->get_property = cc_notebook_get_property; + gobject_class->set_property = cc_notebook_set_property; + g_object_class_install_properties (gobject_class, LAST_PROP, obj_props); + + widget_class->get_request_mode = cc_notebook_get_request_mode; + widget_class->get_preferred_height = cc_notebook_get_preferred_height; + widget_class->get_preferred_width_for_height = cc_notebook_get_preferred_width_for_height; + widget_class->get_preferred_width = cc_notebook_get_preferred_width; + widget_class->get_preferred_height_for_width = cc_notebook_get_preferred_height_for_width; +} + +static void +on_embed_size_allocate (GtkWidget *embed, + GtkAllocation *allocation, + CcNotebook *self) +{ + ClutterActorIter iter; + ClutterActor *child; + ClutterActor *frame; + float page_w, page_h; + float offset = 0.f; + ClutterPoint pos; + + if (self->priv->selected_page == NULL) + return; + + page_w = allocation->width; + page_h = allocation->height; + + clutter_actor_iter_init (&iter, self->priv->bin); + while (clutter_actor_iter_next (&iter, &child)) { + clutter_actor_set_x (child, offset); + clutter_actor_set_size (child, page_w, page_h); + + offset += page_w; + } + + /* This stops the non-animated scrolling from happening + * if we're still scrolling there */ + if (self->priv->last_width == allocation->width) + return; + + self->priv->last_width = allocation->width; + + frame = g_object_get_data (G_OBJECT (self->priv->selected_page), + "cc-notebook-frame"); + + pos.y = 0; + pos.x = clutter_actor_get_x (frame); + clutter_scroll_actor_scroll_to_point (CLUTTER_SCROLL_ACTOR (self->priv->scroll), &pos); +} + +static void +cc_notebook_init (CcNotebook *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, CC_TYPE_NOTEBOOK, CcNotebookPrivate); + + self->priv->embed = gtk_clutter_embed_new (); + gtk_widget_push_composite_child (); + gtk_container_add (GTK_CONTAINER (self), self->priv->embed); + gtk_widget_pop_composite_child (); + g_signal_connect (self->priv->embed, "size-allocate", G_CALLBACK (on_embed_size_allocate), self); + gtk_widget_show (self->priv->embed); + + self->priv->stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (self->priv->embed)); + + self->priv->scroll = clutter_scroll_actor_new (); + clutter_scroll_actor_set_scroll_mode (CLUTTER_SCROLL_ACTOR (self->priv->scroll), + CLUTTER_SCROLL_HORIZONTALLY); + clutter_actor_add_constraint (self->priv->scroll, clutter_bind_constraint_new (self->priv->stage, CLUTTER_BIND_SIZE, 0.f)); + clutter_actor_add_child (self->priv->stage, self->priv->scroll); + + self->priv->bin = clutter_actor_new (); + clutter_actor_add_child (self->priv->scroll, self->priv->bin); + + self->priv->selected_page = NULL; + gtk_widget_set_name (GTK_WIDGET (self), "GtkBox"); +} + +GtkWidget * +cc_notebook_new (void) +{ + return g_object_new (CC_TYPE_NOTEBOOK, NULL); +} + +static void +_cc_notebook_select_page (CcNotebook *self, + GtkWidget *widget, + int index) +{ + ClutterPoint pos; + + g_return_if_fail (CC_IS_NOTEBOOK (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + pos.y = 0; + pos.x = self->priv->last_width * index; + + clutter_actor_save_easing_state (self->priv->scroll); + clutter_actor_set_easing_duration (self->priv->scroll, 500); + + clutter_scroll_actor_scroll_to_point (CLUTTER_SCROLL_ACTOR (self->priv->scroll), &pos); + + clutter_actor_restore_easing_state (self->priv->scroll); + + /* Remember the last selected page */ + self->priv->selected_page = widget; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_CURRENT_PAGE]); +} + +void +cc_notebook_select_page (CcNotebook *self, + GtkWidget *widget) +{ + int i, n_children; + GList *children, *l; + ClutterActor *frame; + gboolean found; + + if (widget == self->priv->selected_page) + return; + + found = FALSE; + frame = g_object_get_data (G_OBJECT (widget), "cc-notebook-frame"); + + n_children = clutter_actor_get_n_children (self->priv->bin); + children = clutter_actor_get_children (self->priv->bin); + for (i = 0, l = children; i < n_children; i++, l = l->next) { + if (frame == l->data) { + _cc_notebook_select_page (self, widget, i); + found = TRUE; + break; + } + } + g_list_free (children); + if (found == FALSE) + g_warning ("Could not find widget '%p' in CcNotebook '%p'", widget, self); +} + +void +cc_notebook_add_page (CcNotebook *self, + GtkWidget *widget) +{ + ClutterActor *frame; + ClutterActor *embed; + int res; + + g_return_if_fail (CC_IS_NOTEBOOK (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + frame = clutter_actor_new (); + clutter_actor_set_layout_manager (frame, clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_FILL, + CLUTTER_BIN_ALIGNMENT_FILL)); + + embed = gtk_clutter_actor_new_with_contents (widget); + g_object_set_data (G_OBJECT (widget), "cc-notebook-frame", frame); + clutter_actor_add_child (frame, embed); + gtk_widget_show (widget); + + res = clutter_actor_get_n_children (self->priv->bin); + clutter_actor_insert_child_at_index (self->priv->bin, frame, res); + + self->priv->pages = g_list_prepend (self->priv->pages, widget); + + if (self->priv->selected_page == NULL) + _cc_notebook_select_page (self, widget, res); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +void +cc_notebook_remove_page (CcNotebook *self, + GtkWidget *widget) +{ + ClutterActorIter iter; + ClutterActor *child, *frame, *selected_frame; + int index; + gboolean found_current; + ClutterPoint pos; + + g_return_if_fail (CC_IS_NOTEBOOK (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (widget != self->priv->selected_page); + + found_current = FALSE; + frame = g_object_get_data (G_OBJECT (widget), "cc-notebook-frame"); + selected_frame = g_object_get_data (G_OBJECT (self->priv->selected_page), "cc-notebook-frame"); + + index = 0; + clutter_actor_iter_init (&iter, self->priv->bin); + while (clutter_actor_iter_next (&iter, &child)) { + if (frame == child) { + clutter_actor_iter_remove (&iter); + break; + } else if (selected_frame == child) { + found_current = TRUE; + } + + index++; + } + + self->priv->pages = g_list_remove (self->priv->pages, widget); + gtk_widget_queue_resize (GTK_WIDGET (self)); + + /* The current page is before the one we removed, so no + * need to shift the scroll view */ + if (found_current) + return; + + pos.y = 0; + pos.x = self->priv->last_width * index; + clutter_scroll_actor_scroll_to_point (CLUTTER_SCROLL_ACTOR (self->priv->scroll), &pos); +} + +GtkWidget * +cc_notebook_get_selected_page (CcNotebook *self) +{ + g_return_val_if_fail (CC_IS_NOTEBOOK (self), NULL); + + return self->priv->selected_page; +} diff --git a/shell/cc-notebook.h b/shell/cc-notebook.h new file mode 100644 index 000000000..508d54a9b --- /dev/null +++ b/shell/cc-notebook.h @@ -0,0 +1,69 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright © 2012 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Written by: + * Bastien Nocera + */ + +#ifndef _CC_NOTEBOOK_H_ +#define _CC_NOTEBOOK_H_ + +#include +#include + +G_BEGIN_DECLS + +#define CC_TYPE_NOTEBOOK (cc_notebook_get_type ()) +#define CC_NOTEBOOK(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CC_TYPE_NOTEBOOK, CcNotebook)) +#define CC_IS_NOTEBOOK(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CC_TYPE_NOTEBOOK)) +#define CC_NOTEBOOK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CC_TYPE_NOTEBOOK, CcNotebookClass)) +#define CC_IS_NOTEBOOK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CC_TYPE_NOTEBOOK)) +#define CC_NOTEBOOK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CC_TYPE_NOTEBOOK, CcNotebookClass)) + +typedef struct _CcNotebook CcNotebook; +typedef struct _CcNotebookPrivate CcNotebookPrivate; +typedef struct _CcNotebookClass CcNotebookClass; + +struct _CcNotebook +{ + GtkBox parent_class; + + CcNotebookPrivate *priv; +}; + +struct _CcNotebookClass +{ + GtkBoxClass parent_class; +}; + +GType cc_notebook_get_type (void) G_GNUC_CONST; + +GtkWidget * cc_notebook_new (void); + +void cc_notebook_add_page (CcNotebook *self, + GtkWidget *widget); +void cc_notebook_remove_page (CcNotebook *self, + GtkWidget *widget); + +void cc_notebook_select_page (CcNotebook *self, + GtkWidget *widget); + +GtkWidget * cc_notebook_get_selected_page (CcNotebook *self); + +G_END_DECLS + +#endif /* _CC_NOTEBOOK_H_ */ diff --git a/shell/test-notebook.c b/shell/test-notebook.c new file mode 100644 index 000000000..a488a8a10 --- /dev/null +++ b/shell/test-notebook.c @@ -0,0 +1,92 @@ +#include +#include "cc-notebook.h" + +#define NUM_PAGES 5 + +static GHashTable *pages; +static GtkWidget *notebook; + +static void +goto_page (GtkButton *button, + gpointer user_data) +{ + int target = GPOINTER_TO_INT (user_data); + GtkWidget *widget; + + if (target < 1) + target = NUM_PAGES; + else if (target > NUM_PAGES) + target = 1; + widget = g_hash_table_lookup (pages, GINT_TO_POINTER (target)); + cc_notebook_select_page (CC_NOTEBOOK (notebook), widget); +} + +static GtkWidget * +create_page_contents (int page_num) +{ + GtkWidget *vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + char *text = g_strdup_printf ("Page number %d", page_num); + + GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + GtkWidget *back_button = gtk_button_new_with_label ("←"); + gtk_widget_set_halign (back_button, GTK_ALIGN_START); + gtk_box_pack_start (GTK_BOX (hbox), back_button, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (back_button), "clicked", + G_CALLBACK (goto_page), GINT_TO_POINTER (page_num - 1)); + + GtkWidget *fwd_button = gtk_button_new_with_label ("→"); + gtk_widget_set_halign (fwd_button, GTK_ALIGN_END); + gtk_box_pack_end (GTK_BOX (hbox), fwd_button, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (fwd_button), "clicked", + G_CALLBACK (goto_page), GINT_TO_POINTER (page_num + 1)); + + GtkWidget *label = gtk_label_new (text); + + gtk_box_pack_end (GTK_BOX (vbox), label, TRUE, TRUE, 0); + + gtk_widget_show_all (vbox); + g_object_set_data_full (G_OBJECT (vbox), "display-name", text, g_free); + + g_hash_table_insert (pages, GINT_TO_POINTER (page_num), vbox); + + return vbox; +} + +static void +on_page_change (CcNotebook *notebook) +{ + g_print (G_STRLOC ": Currently selected page: %s\n", + (char *) g_object_get_data (G_OBJECT (cc_notebook_get_selected_page (notebook)), "display-name")); +} + +int +main (int argc, char *argv[]) +{ + guint i; + + if (gtk_clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS) + return EXIT_FAILURE; + + GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL); + gtk_widget_show (window); + + notebook = cc_notebook_new (); + gtk_container_add (GTK_CONTAINER (window), notebook); + g_signal_connect (notebook, "notify::current-page", G_CALLBACK (on_page_change), NULL); + + pages = g_hash_table_new (g_direct_hash, g_direct_equal); + + for (i = 1; i <= NUM_PAGES; i++) { + GtkWidget *page = create_page_contents (i); + cc_notebook_add_page ((CcNotebook *) notebook, page); + } + + gtk_widget_show_all (window); + + gtk_main (); + + return EXIT_SUCCESS; +}