From 5e9dcb466c5506a8ea4b52e1eb452df6d806c823 Mon Sep 17 00:00:00 2001 From: "P. F. Chimento" Date: Fri, 3 May 2013 18:33:05 +0200 Subject: Implement EosPageManager Minimum functionality for EosPageManager. Currently uses a GtkNotebook internally until we decide what to do regarding GtkStack. (Philip; map/unmap Matt, Patrick) --- docs/reference/endless/Makefile.am | 3 +- docs/reference/endless/endless-docs.xml | 1 + docs/reference/endless/endless-sections.txt | 24 + endless/Makefile.am | 2 + endless/endless.h | 1 + endless/eospagemanager.c | 795 ++++++++++++++++++++++++++++ endless/eospagemanager.h | 97 ++++ test/Makefile.am | 1 + test/run-tests.c | 1 + test/run-tests.h | 1 + test/smoke-tests/app-window.js | 42 +- test/test-page-manager.c | 333 ++++++++++++ 12 files changed, 1297 insertions(+), 4 deletions(-) create mode 100644 endless/eospagemanager.c create mode 100644 endless/eospagemanager.h create mode 100644 test/test-page-manager.c diff --git a/docs/reference/endless/Makefile.am b/docs/reference/endless/Makefile.am index 01b7069..ec57db9 100644 --- a/docs/reference/endless/Makefile.am +++ b/docs/reference/endless/Makefile.am @@ -16,7 +16,8 @@ DOC_MAIN_SGML_FILE=$(DOC_MODULE)-docs.xml DOC_SOURCE_DIR=$(top_srcdir)/endless # Extra options to pass to gtkdoc-scangobj. Not normally needed. -SCANGOBJ_OPTIONS= +SCANGOBJ_OPTIONS= \ + --query-child-properties=gtk_container_class_list_child_properties # Extra options to supply to gtkdoc-scan. # e.g. SCAN_OPTIONS=--deprecated-guards="GTK_DISABLE_DEPRECATED" diff --git a/docs/reference/endless/endless-docs.xml b/docs/reference/endless/endless-docs.xml index 13866ca..38d4ae3 100644 --- a/docs/reference/endless/endless-docs.xml +++ b/docs/reference/endless/endless-docs.xml @@ -19,6 +19,7 @@ Open Endless SDK reference (C API) + diff --git a/docs/reference/endless/endless-sections.txt b/docs/reference/endless/endless-sections.txt index 8978d4d..4024e77 100644 --- a/docs/reference/endless/endless-sections.txt +++ b/docs/reference/endless/endless-sections.txt @@ -38,3 +38,27 @@ eos_window_get_type EosWindowPrivate + +
+page-manager +EosPageManager +eos_page_manager_new +eos_page_manager_get_visible_page +eos_page_manager_set_visible_page +eos_page_manager_get_visible_page_name +eos_page_manager_set_visible_page_name +eos_page_manager_get_page_name +eos_page_manager_set_page_name +eos_page_manager_remove_page_by_name + +EOS_IS_PAGE_MANAGER +EOS_IS_PAGE_MANAGER_CLASS +EOS_PAGE_MANAGER +EOS_PAGE_MANAGER_CLASS +EOS_PAGE_MANAGER_GET_CLASS +EOS_TYPE_PAGE_MANAGER +EosPageManagerClass +eos_page_manager_get_type + +EosPageManagerPrivate +
\ No newline at end of file diff --git a/endless/Makefile.am b/endless/Makefile.am index 4ccd55d..a313905 100644 --- a/endless/Makefile.am +++ b/endless/Makefile.am @@ -7,6 +7,7 @@ endless_private_installed_headers = \ endless/eosapplication.h \ endless/eosenums.h \ endless/eosmacros.h \ + endless/eospagemanager.h \ endless/eostypes.h \ endless/eoswindow.h @@ -14,6 +15,7 @@ endless_library_sources = \ endless/eosapplication.c \ endless/eoshello.c \ endless/eosinit.c endless/eosinit-private.h \ + endless/eospagemanager.c \ endless/eostopbar.c endless/eostopbar-private.h \ endless/eoswindow.c diff --git a/endless/endless.h b/endless/endless.h index 6bf4f29..9f5706c 100644 --- a/endless/endless.h +++ b/endless/endless.h @@ -13,6 +13,7 @@ G_BEGIN_DECLS /* Pull in other header files */ #include "eostypes.h" #include "eosapplication.h" +#include "eospagemanager.h" #include "eoswindow.h" #undef _EOS_SDK_INSIDE_ENDLESS_H diff --git a/endless/eospagemanager.c b/endless/eospagemanager.c new file mode 100644 index 0000000..4d847b0 --- /dev/null +++ b/endless/eospagemanager.c @@ -0,0 +1,795 @@ +/* Copyright 2013 Endless Mobile, Inc. */ + +#include "config.h" +#include "eospagemanager.h" + +#include + +#include + +/** + * SECTION:page-manager + * @short_description: Controlling the flow of your application + * @title: Page Manager + * + * Your users experience your application as a series of + * pages — screens that present a small amount + * of information or show one feature before moving on to the next page + * or a previous page. + * + * The page manager controls how these pages relate to + * each other. + * There are several different page managers available, each representing a + * different user interaction. + * The default page manager, described in this section of the manual, lets you + * add any number of pages and switch between them however you like, but there + * are also other, more specialized ones: + * for example, the #EosSplashScreenPageManager displays a splash screen and + * later turns control over to a different page or page manager when you signal + * it to; + * and the #EosTabbedPageManager creates a tabbed interface in your window, much + * like the one in your browser. + * + * Each window has a page manager; one is created by default when you create + * the window, but you can replace it by a different one. + * You can also nest page managers, one inside the other, in order to create + * more complex application flows. + * + * A page can be any widget, most likely a container widget with other widgets + * inside it. + * To add a page to a page manager, call + * |[ + * gtk_container_add (GTK_CONTAINER (page_manager), page); + * ]| + * If the added page is the only page, then the page manager will display it + * immediately. + * If the page manager was already displaying another page, then adding a new + * page will not change which page is displayed. + * + * To get information about how to display the pages, for example the background + * image to use, the page manager reads the page's child + * properties. + * These are like regular properties, but instead of modifying the page, they + * modify the relationship between the page and the page manager that contains + * it. + * Most pages have at least a name and a background image as child properties. + * You can add a page with child properties as follows: + * |[ + * gtk_container_add_with_properties (GTK_CONTAINER (page_manager), page, + * "name", "front-page", + * "background", "image.jpg", + * NULL); + * ]| + * In Javascript, this has been simplified to use JSON: + * |[ + * page_manager.add(page, { + * name: 'front-page', + * background: 'image.jpg' + * }); + * ]| + * To remove a page, use gtk_container_remove() or + * eos_page_manager_remove_page_by_name(). + * If the removed page was the only page, then the page manager will display + * nothing. + * If that page was currently displaying but was not the only page, then the + * page manager will display another page; which page is undefined. + * + * + * Removing pages with gtk_container_remove() is currently broken due to + * a bug in GTK. Use eos_page_manager_remove_page_by_name() for the time + * being. + * + * + * In general, it is convenient to refer to a page by its name when dealing with + * the page manager, so you should make a point of giving all your pages names. + */ + +G_DEFINE_TYPE (EosPageManager, eos_page_manager, GTK_TYPE_CONTAINER) + +#define PAGE_MANAGER_PRIVATE(o) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((o), EOS_TYPE_PAGE_MANAGER, EosPageManagerPrivate)) + +typedef struct _EosPageManagerPageInfo EosPageManagerPageInfo; +struct _EosPageManagerPageInfo +{ + GtkWidget *page; + gchar *name; +}; + +struct _EosPageManagerPrivate +{ + GtkWidget *stack; + GList *page_info; /* GList */ + EosPageManagerPageInfo *visible_page_info; +}; + +enum +{ + PROP_0, + PROP_VISIBLE_PAGE, + PROP_VISIBLE_PAGE_NAME, + NPROPS +}; + +enum +{ + CHILD_PROP_0, + CHILD_PROP_NAME, + NCHILDPROPS +}; + +static GParamSpec *eos_page_manager_props[NPROPS] = { NULL, }; +static GParamSpec *eos_page_manager_child_props[NCHILDPROPS] = { NULL, }; + +static void +page_info_free (EosPageManagerPageInfo *info) +{ + g_free (info->name); + g_slice_free (EosPageManagerPageInfo, info); +} + +/* Helper function for find_page_info_by_widget() */ +static gint +page_info_widget_compare (const EosPageManagerPageInfo *info, + const GtkWidget *page) +{ + return (info->page != page); /* not orderable */ +} + +/* + * find_page_info_by_widget: + * @self: the page manager + * @page: the page to look for + * + * Searches for the page info corresponding to the child @page. + * + * Returns: the #EosPageManagerPageInfo for @page, or %NULL if @page is not a + * child of @self. + */ +static EosPageManagerPageInfo * +find_page_info_by_widget (EosPageManager *self, + GtkWidget *page) +{ + GList *result = g_list_find_custom (self->priv->page_info, page, + (GCompareFunc)page_info_widget_compare); + if (result == NULL) + return NULL; + return result->data; +} + +/* Helper function for find_page_info_by_name() */ +static gint +page_info_name_compare (const EosPageManagerPageInfo *info, + const gchar *name) +{ + /* g_strcmp0() handles NULL */ + return g_strcmp0(info->name, name); +} + +/* + * find_page_info_by_name: + * @self: the page manager + * @name: the name to look for + * + * Searches for the page info corresponding to the child with name @name. + * + * Returns: the #EosPageManagerPageInfo for @name, or %NULL if @name is not the + * name of a child of @self. + */ +static EosPageManagerPageInfo * +find_page_info_by_name (EosPageManager *self, + const gchar *name) +{ + GList *result = g_list_find_custom (self->priv->page_info, name, + (GCompareFunc)page_info_name_compare); + if (result == NULL) + return NULL; + return result->data; +} + +/* Convenience function, since this warning occurs at several places */ +static void +warn_page_widget_not_found (EosPageManager *self, + GtkWidget *page) +{ + g_critical ("Page at %p (type %s) is not a child of EosPageManager %p", + page, + g_type_name (G_OBJECT_TYPE (page)), + self); +} + +/* Convenience function, since this warning occurs at several places */ +static void +warn_page_name_not_found (EosPageManager *self, + const gchar *name) +{ + g_critical ("EosPageManager %p has no page named %s", + self, + name); +} + +static void +set_visible_page_from_info (EosPageManager *self, + EosPageManagerPageInfo *info) +{ + /* FIXME when porting to GtkStack */ + GtkNotebook *stack_notebook = GTK_NOTEBOOK (self->priv->stack); + int page_pos = gtk_notebook_page_num (stack_notebook, info->page); + /* Invariant: if info is not NULL, then page must be in stack */ + g_assert (page_pos != -1); + gtk_notebook_set_current_page (stack_notebook, page_pos); + + self->priv->visible_page_info = info; + + GObject *self_object = G_OBJECT (self); + g_object_notify(self_object, "visible-page"); + g_object_notify(self_object, "visible-page-name"); +} + +static void +eos_page_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EosPageManager *self = EOS_PAGE_MANAGER (object); + + switch (property_id) + { + case PROP_VISIBLE_PAGE: + g_value_set_object (value, eos_page_manager_get_visible_page (self)); + break; + + case PROP_VISIBLE_PAGE_NAME: + g_value_set_string (value, eos_page_manager_get_visible_page_name (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +eos_page_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + EosPageManager *self = EOS_PAGE_MANAGER (object); + + switch (property_id) + { + case PROP_VISIBLE_PAGE: + eos_page_manager_set_visible_page (self, g_value_get_object (value)); + break; + + case PROP_VISIBLE_PAGE_NAME: + eos_page_manager_set_visible_page_name (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +eos_page_manager_finalize (GObject *object) +{ + EosPageManager *self = EOS_PAGE_MANAGER (object); + + g_list_foreach (self->priv->page_info, (GFunc)page_info_free, NULL); + + G_OBJECT_CLASS (eos_page_manager_parent_class)->finalize (object); +} + +static GtkSizeRequestMode +eos_page_manager_get_request_mode (GtkWidget *widget) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + return gtk_widget_get_request_mode (self->priv->stack); +} + +static void +eos_page_manager_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + gtk_widget_get_preferred_height (self->priv->stack, minimum, natural); +} + +static void +eos_page_manager_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + gtk_widget_get_preferred_height_for_width (self->priv->stack, height, + minimum, natural); +} + +static void +eos_page_manager_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + gtk_widget_get_preferred_width (self->priv->stack, minimum, natural); +} + +static void +eos_page_manager_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + gtk_widget_get_preferred_height_for_width (self->priv->stack, width, + minimum, natural); +} + +static void +eos_page_manager_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + gtk_widget_set_allocation (widget, allocation); + gtk_widget_size_allocate (self->priv->stack, allocation); +} + +static void +eos_page_manager_show_all (GtkWidget *widget) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + GTK_WIDGET_CLASS (eos_page_manager_parent_class)->show (widget); + if (self->priv->stack != NULL) + gtk_widget_show_all (self->priv->stack); +} + +static void +eos_page_manager_map (GtkWidget *widget) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + if (self->priv->stack != NULL && gtk_widget_get_visible (self->priv->stack)) + gtk_widget_map (self->priv->stack); + GTK_WIDGET_CLASS (eos_page_manager_parent_class)->map (widget); +} + +static void +eos_page_manager_unmap (GtkWidget *widget) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + if (self->priv->stack != NULL) + gtk_widget_unmap (self->priv->stack); + GTK_WIDGET_CLASS (eos_page_manager_parent_class)->unmap (widget); +} + +static gboolean +eos_page_manager_draw (GtkWidget *widget, + cairo_t *cr) +{ + EosPageManager *self = EOS_PAGE_MANAGER (widget); + + if (self->priv->stack != NULL) + gtk_widget_draw (self->priv->stack, cr); + + return FALSE; +} + +static void +eos_page_manager_add (GtkContainer *container, + GtkWidget *new_page) +{ + EosPageManager *self = EOS_PAGE_MANAGER (container); + + gtk_container_add (GTK_CONTAINER (self->priv->stack), new_page); + EosPageManagerPageInfo *info = g_slice_new0 (EosPageManagerPageInfo); + info->page = new_page; + self->priv->page_info = g_list_prepend (self->priv->page_info, info); + + /* If there were no pages yet, then this one must become the visible one */ + if (self->priv->visible_page_info == NULL) + self->priv->visible_page_info = info; +} + +static void +eos_page_manager_remove (GtkContainer *container, + GtkWidget *page) +{ + EosPageManager *self = EOS_PAGE_MANAGER (container); + + gtk_container_remove (GTK_CONTAINER (self->priv->stack), page); + EosPageManagerPageInfo *info = find_page_info_by_widget (self, page); + if (info == NULL) + { + warn_page_widget_not_found (self, page); + return; + } + self->priv->page_info = g_list_remove (self->priv->page_info, info); + + /* If this was the only page */ + if (self->priv->visible_page_info == info) + self->priv->visible_page_info = NULL; + + page_info_free (info); +} + +static void +eos_page_manager_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + EosPageManager *self = EOS_PAGE_MANAGER (container); + + if (self->priv->stack == NULL) + return; + + GtkContainerClass *stack_class = GTK_CONTAINER_GET_CLASS (self->priv->stack); + stack_class->forall (GTK_CONTAINER (self->priv->stack), + include_internals, + callback, + callback_data); + if (include_internals) + callback (self->priv->stack, callback_data); +} + +static void +eos_page_manager_get_child_property (GtkContainer *container, + GtkWidget *child, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EosPageManager *self = EOS_PAGE_MANAGER (container); + + switch (property_id) + { + case CHILD_PROP_NAME: + g_value_set_string (value, eos_page_manager_get_page_name (self, child)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, + property_id, pspec); + } +} + +static void +eos_page_manager_set_child_property (GtkContainer *container, + GtkWidget *child, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + EosPageManager *self = EOS_PAGE_MANAGER (container); + + switch (property_id) + { + case CHILD_PROP_NAME: + eos_page_manager_set_page_name (self, child, g_value_get_string (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, + property_id, pspec); + } +} + +static void +eos_page_manager_class_init (EosPageManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + g_type_class_add_private (klass, sizeof (EosPageManagerPrivate)); + + object_class->get_property = eos_page_manager_get_property; + object_class->set_property = eos_page_manager_set_property; + object_class->finalize = eos_page_manager_finalize; + + /* Pass all size requesting and allocation on to the stack */ + widget_class->get_request_mode = eos_page_manager_get_request_mode; + widget_class->get_preferred_height = eos_page_manager_get_preferred_height; + widget_class->get_preferred_height_for_width = + eos_page_manager_get_preferred_height_for_width; + widget_class->get_preferred_width = eos_page_manager_get_preferred_width; + widget_class->get_preferred_width_for_height = + eos_page_manager_get_preferred_width_for_height; + widget_class->size_allocate = eos_page_manager_size_allocate; + widget_class->show_all = eos_page_manager_show_all; + widget_class->map = eos_page_manager_map; + widget_class->unmap = eos_page_manager_unmap; + widget_class->draw = eos_page_manager_draw; + + container_class->add = eos_page_manager_add; + container_class->remove = eos_page_manager_remove; + container_class->forall = eos_page_manager_forall; + container_class->get_child_property = eos_page_manager_get_child_property; + container_class->set_child_property = eos_page_manager_set_child_property; + + /** + * EosPageManager:visible-page: + * + * A reference to the page widget that is currently being displayed by the + * page manager. + * If the page manager has no pages, then this is %NULL. + */ + eos_page_manager_props[PROP_VISIBLE_PAGE] = + g_param_spec_object ("visible-page", "Visible page", + "Page widget currently displaying in the page manager", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * EosPageManager:visible-page-name: + * + * The name of the page that is currently being displayed by the page manager. + * If the page manager has no pages, then this is %NULL. + * However, if there is a page currently being displayed but it has no name, + * then this is the empty string (""). + */ + eos_page_manager_props[PROP_VISIBLE_PAGE_NAME] = + g_param_spec_string ("visible-page-name", "Visible page name", + "Name of page currently displaying in the page " + "manager", + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, NPROPS, + eos_page_manager_props); + + /** + * EosPageManager:name: + * + * The name of this page. Make sure to choose a unique name. + */ + eos_page_manager_child_props[CHILD_PROP_NAME] = + g_param_spec_string ("name", "Name", "Unique ID for the page", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + gtk_container_class_install_child_property (container_class, CHILD_PROP_NAME, + eos_page_manager_child_props[CHILD_PROP_NAME]); +} + +static void +eos_page_manager_init (EosPageManager *self) +{ + GtkWidget *self_widget = GTK_WIDGET (self); + self->priv = PAGE_MANAGER_PRIVATE (self); + + gtk_widget_set_has_window (self_widget, FALSE); + + /* TODO replace with GtkStack */ + self->priv->stack = gtk_notebook_new (); + g_object_set (self->priv->stack, + "show-tabs", FALSE, + "show-border", FALSE, + NULL); + gtk_widget_set_parent (self->priv->stack, self_widget); +} + +/* Public API */ + +/** + * eos_page_manager_new: + * + * Creates a new default page manager. + * + * Returns: the new page manager. + */ +GtkWidget * +eos_page_manager_new (void) +{ + return g_object_new (EOS_TYPE_PAGE_MANAGER, NULL); +} + +/** + * eos_page_manager_get_visible_page: + * @self: the page manager + * + * Gets the page widget that @self is currently displaying. + * See #EosPageManager:visible-page for more information. + * + * Returns: (transfer none): the page #GtkWidget, or %NULL if @self does not + * have any pages. + */ +GtkWidget * +eos_page_manager_get_visible_page (EosPageManager *self) +{ + g_return_val_if_fail (EOS_IS_PAGE_MANAGER (self), NULL); + + if(self->priv->visible_page_info == NULL) + return NULL; + + return self->priv->visible_page_info->page; +} + +/** + * eos_page_manager_set_visible_page: + * @self: the page manager + * @page: the page to switch to + * + * Switches the page manager @self to display @page. + * The @page widget must previously have been added to the page manager. + * See #EosPageManager:visible-page for more information. + */ +void +eos_page_manager_set_visible_page (EosPageManager *self, + GtkWidget *page) +{ + g_return_if_fail (EOS_IS_PAGE_MANAGER (self)); + + EosPageManagerPageInfo *info = find_page_info_by_widget (self, page); + if (info == NULL) + { + warn_page_widget_not_found (self, page); + return; + } + + set_visible_page_from_info (self, info); +} + +/** + * eos_page_manager_get_visible_page_name: + * @self: the page manager + * + * Gets the name of the page widget that @self is currently displaying. + * See #EosPageManager:visible-page for more information. + * + * Returns: the name of the page, or %NULL if @self does not have any pages, + * or the empty string if the page does not have a name. + */ +const gchar * +eos_page_manager_get_visible_page_name (EosPageManager *self) +{ + g_return_val_if_fail (EOS_IS_PAGE_MANAGER (self), NULL); + + if(self->priv->visible_page_info == NULL) + return NULL; + + return self->priv->visible_page_info->name; +} + +/** + * eos_page_manager_set_visible_page_name: + * @self: the page manager + * @page_name: the name of the page to switch to + * + * Switches the page manager @self to display the page called @page_name. + * This page must previously have been added to the page manager. + * See #EosPageManager:visible-page for more information. + */ +void +eos_page_manager_set_visible_page_name (EosPageManager *self, + const gchar *page_name) +{ + g_return_if_fail (EOS_IS_PAGE_MANAGER (self)); + + EosPageManagerPageInfo *info = find_page_info_by_name (self, page_name); + if (info == NULL) + { + warn_page_name_not_found (self, page_name); + return; + } + + set_visible_page_from_info (self, info); +} + +/** + * eos_page_manager_get_page_name: + * @self: the page manager + * @page: the page to be queried + * + * Gets the name of @page, which must previously have been added to the + * page manager. + * See #EosPageManager:name for more information. + * + * Returns: the name of @page, or the empty string if @page does not have a + * name. + */ +const gchar * +eos_page_manager_get_page_name (EosPageManager *self, + GtkWidget *page) +{ + g_return_val_if_fail (EOS_IS_PAGE_MANAGER (self), NULL); + g_return_val_if_fail (GTK_IS_WIDGET (page), NULL); + + EosPageManagerPageInfo *info = find_page_info_by_widget (self, page); + if (info == NULL) + { + warn_page_widget_not_found (self, page); + return NULL; + } + + if (info->name == NULL) + return ""; + + return info->name; +} + +/** + * eos_page_manager_set_page_name: + * @self: the page manager + * @page: the page to be renamed + * @name: the new name for @page + * + * Changes the name of @page, which must previously have been added to the + * page manager. + * See #EosPageManager:name for more information. + */ +void +eos_page_manager_set_page_name (EosPageManager *self, + GtkWidget *page, + const gchar *name) +{ + g_return_if_fail (EOS_IS_PAGE_MANAGER (self)); + g_return_if_fail (GTK_IS_WIDGET (page)); + + /* Two pages with the same name are not allowed */ + EosPageManagerPageInfo *info = find_page_info_by_name (self, name); + if (info != NULL && info->page != page) + { + g_critical ("Not setting page name to \"%s\", because page manager " + "already contains a page by that name", + name); + return; + } + + info = find_page_info_by_widget (self, page); + if (info == NULL) + { + warn_page_widget_not_found (self, page); + return; + } + + if (g_strcmp0(info->name, name) == 0) + return; + + g_free (info->name); + info->name = g_strdup (name); + + gtk_container_child_notify (GTK_CONTAINER (self), page, "name"); +} + +/** + * eos_page_manager_remove_page_by_name: + * @self: the page manager + * @name: the name of the page to remove + * + * Removes the page called @name from the page manager. + * If that page was the only page, then the page manager will display nothing. + * If that page was currently displaying but was not the only page, then the + * page manager will display another page; which page is undefined. + * + * To remove a page without looking it up by name, use gtk_container_remove(). + */ +void +eos_page_manager_remove_page_by_name (EosPageManager *self, + const gchar *name) +{ + g_return_if_fail (EOS_IS_PAGE_MANAGER (self)); + + EosPageManagerPageInfo *info = find_page_info_by_name (self, name); + if (info == NULL) + { + warn_page_name_not_found (self, name); + return; + } + + /* FIXME: Can't use gtk_container_remove() directly because that asserts + gtk_widget_get_parent(child) == self || GTK_IS_ASSISTANT(self) + See https://bugzilla.gnome.org/show_bug.cgi?id=699756 [endlessm/eos-sdk#67] */ + g_signal_emit_by_name (self, "remove", info->page); +} \ No newline at end of file diff --git a/endless/eospagemanager.h b/endless/eospagemanager.h new file mode 100644 index 0000000..7f006ec --- /dev/null +++ b/endless/eospagemanager.h @@ -0,0 +1,97 @@ +/* Copyright 2013 Endless Mobile, Inc. */ + +#ifndef EOS_PAGE_MANAGER_H +#define EOS_PAGE_MANAGER_H + +#if !(defined(_EOS_SDK_INSIDE_ENDLESS_H) || defined(COMPILING_EOS_SDK)) +#error "Please do not include this header file directly." +#endif + +#include "eostypes.h" + +#include + +G_BEGIN_DECLS + +#define EOS_TYPE_PAGE_MANAGER eos_page_manager_get_type() + +#define EOS_PAGE_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + EOS_TYPE_PAGE_MANAGER, EosPageManager)) + +#define EOS_PAGE_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + EOS_TYPE_PAGE_MANAGER, EosPageManagerClass)) + +#define EOS_IS_PAGE_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + EOS_TYPE_PAGE_MANAGER)) + +#define EOS_IS_PAGE_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + EOS_TYPE_PAGE_MANAGER)) + +#define EOS_PAGE_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + EOS_TYPE_PAGE_MANAGER, EosPageManagerClass)) + +typedef struct _EosPageManager EosPageManager; +typedef struct _EosPageManagerClass EosPageManagerClass; +typedef struct _EosPageManagerPrivate EosPageManagerPrivate; + +/** + * EosPageManager: + * + * This structure contains no public members. + */ +struct _EosPageManager +{ + GtkContainer parent; + + EosPageManagerPrivate *priv; +}; + +struct _EosPageManagerClass +{ + GtkContainerClass parent_class; + + /* For further expansion */ + gpointer _padding[8]; +}; + +EOS_SDK_ALL_API_VERSIONS +GType eos_page_manager_get_type (void) G_GNUC_CONST; + +EOS_SDK_ALL_API_VERSIONS +GtkWidget *eos_page_manager_new (void); + +EOS_SDK_ALL_API_VERSIONS +GtkWidget *eos_page_manager_get_visible_page (EosPageManager *self); + +EOS_SDK_ALL_API_VERSIONS +void eos_page_manager_set_visible_page (EosPageManager *self, + GtkWidget *page); + +EOS_SDK_ALL_API_VERSIONS +const gchar *eos_page_manager_get_visible_page_name (EosPageManager *self); + +EOS_SDK_ALL_API_VERSIONS +void eos_page_manager_set_visible_page_name (EosPageManager *self, + const gchar *page_name); + +EOS_SDK_ALL_API_VERSIONS +const gchar *eos_page_manager_get_page_name (EosPageManager *self, + GtkWidget *page); + +EOS_SDK_ALL_API_VERSIONS +void eos_page_manager_set_page_name (EosPageManager *self, + GtkWidget *page, + const gchar *name); + +EOS_SDK_ALL_API_VERSIONS +void eos_page_manager_remove_page_by_name (EosPageManager *self, + const gchar *name); + +G_END_DECLS + +#endif /* EOS_PAGE_MANAGER_H */ diff --git a/test/Makefile.am b/test/Makefile.am index 39f4155..9435a9b 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -12,6 +12,7 @@ test_run_tests_SOURCES = \ test/test-init.c \ test/test-hello.c \ test/test-application.c \ + test/test-page-manager.c \ test/test-window.c test_run_tests_CPPFLAGS = $(TEST_FLAGS) test_run_tests_LDADD = $(TEST_LIBS) diff --git a/test/run-tests.c b/test/run-tests.c index aa6acbf..aaecef8 100644 --- a/test/run-tests.c +++ b/test/run-tests.c @@ -42,6 +42,7 @@ main (int argc, add_hello_tests (); add_application_tests (); add_window_tests (); + add_page_manager_tests (); return g_test_run (); } diff --git a/test/run-tests.h b/test/run-tests.h index 8d18015..77319a3 100644 --- a/test/run-tests.h +++ b/test/run-tests.h @@ -28,5 +28,6 @@ void add_init_tests (void); void add_hello_tests (void); void add_application_tests (void); void add_window_tests (void); +void add_page_manager_tests (void); #endif /* RUN_TESTS_H */ diff --git a/test/smoke-tests/app-window.js b/test/smoke-tests/app-window.js index 8f989ea..32b1736 100644 --- a/test/smoke-tests/app-window.js +++ b/test/smoke-tests/app-window.js @@ -6,6 +6,15 @@ const Gtk = imports.gi.Gtk; const TEST_APPLICATION_ID = 'com.endlessm.example.test'; +/* Override Endless.PageManager.add() */ +Endless.PageManager.prototype.add_real = Endless.PageManager.prototype.add +Endless.PageManager.prototype.add = function(child, props) { + this.add_real(child); + for(let prop_id in props) { + this.child_set_property(child, prop_id, props[prop_id]); + } +} + const TestApplication = new Lang.Class ({ Name: 'TestApplication', Extends: Endless.Application, @@ -13,14 +22,41 @@ const TestApplication = new Lang.Class ({ vfunc_startup: function() { this.parent(); - this._button = new Gtk.Button({label: 'Close me'}); - this._button.connect('clicked', Lang.bind(this, this._onButtonClicked)); + // First page + this._page0 = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL }); + let a1 = new Gtk.Button({ label: 'Go to page1' }); + a1.connect('clicked', Lang.bind(this, function () { + this._pm.visible_page = this._page1; + })); + this._page0.add(a1); + let a2 = new Gtk.Button({ label: 'Go to page named "page1"' }); + a2.connect('clicked', Lang.bind(this, function () { + this._pm.visible_page_name = "page1"; + })); + this._page0.add(a2); + + // Second page + this._page1 = new Gtk.Grid({ orientation: Gtk.Orientation.HORIZONTAL }); + let b1 = new Gtk.Button({ label: 'Go to page0' }); + b1.connect('clicked', Lang.bind(this, function () { + this._pm.visible_page = this._page0; + })); + this._page1.add(b1); + let b2 = new Gtk.Button({ label: 'Go to page named "page0"' }); + b2.connect('clicked', Lang.bind(this, function () { + this._pm.visible_page_name = "page0"; + })); + this._page1.add(b2); + + this._pm = new Endless.PageManager(); + this._pm.add(this._page0, { name: "page0" }); + this._pm.add(this._page1, { name: "page1" }); this._window = new Endless.Window({ application: this, border_width: 16 }); - this._window.add(this._button); + this._window.add(this._pm); this._window.show_all(); }, diff --git a/test/test-page-manager.c b/test/test-page-manager.c new file mode 100644 index 0000000..00ce0ad --- /dev/null +++ b/test/test-page-manager.c @@ -0,0 +1,333 @@ +#include +#include + +#include "run-tests.h" + +#define PAGE1_NAME "page1" +#define PAGE2_NAME "page2" +#define PAGE3_NAME "page3" +#define EXPECTED_PAGE_NAME PAGE2_NAME +#define EXPECTED_CHANGED_PAGE_NAME "changed-name" +#define DUPLICATE_PAGE_NAME "duplicate-name" +#define EXPECTED_DUPLICATE_PAGE_NAME_ERRMSG "*Not setting page name to \"" \ + DUPLICATE_PAGE_NAME "\", because page manager already contains a page by " \ + "that name*" +#define ADD_PAGE_MANAGER_TEST(path, test_func) \ + g_test_add ((path), PageManagerFixture, NULL, \ + pm_fixture_setup, (test_func), pm_fixture_teardown) +#define ADD_EMPTY_PAGE_MANAGER_TEST(path, test_func) \ + g_test_add ((path), PageManagerFixture, NULL, \ + empty_pm_fixture_setup, (test_func), pm_fixture_teardown); + +typedef struct +{ + GtkWidget *pm; + GtkWidget *page1; + GtkWidget *page2; + GtkWidget *page3; +} PageManagerFixture; + +static void +pm_fixture_setup (PageManagerFixture *fixture, + gconstpointer unused) +{ + fixture->pm = eos_page_manager_new (); + fixture->page1 = gtk_label_new ("1"); + fixture->page2 = gtk_label_new ("2"); + fixture->page3 = gtk_label_new ("3"); + gtk_container_add_with_properties (GTK_CONTAINER (fixture->pm), + fixture->page1, + "name", PAGE1_NAME, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (fixture->pm), + fixture->page2, + "name", PAGE2_NAME, + NULL); + gtk_container_add_with_properties (GTK_CONTAINER (fixture->pm), + fixture->page3, + "name", PAGE3_NAME, + NULL); +} + +static void +empty_pm_fixture_setup (PageManagerFixture *fixture, + gconstpointer unused) +{ + fixture->pm = eos_page_manager_new (); +} + +static void +pm_fixture_teardown (PageManagerFixture *fixture, + gconstpointer unused) +{ + gtk_widget_destroy (fixture->pm); +} + +static void +test_pm_get_set_visible_page (PageManagerFixture *fixture, + gconstpointer unused) +{ + GtkWidget *visible_page; + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page != fixture->page2); + eos_page_manager_set_visible_page (EOS_PAGE_MANAGER (fixture->pm), + fixture->page2); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == fixture->page2); +} + +static void +test_pm_prop_visible_page (PageManagerFixture *fixture, + gconstpointer unused) +{ + GtkWidget *visible_page; + g_object_get (fixture->pm, "visible-page", &visible_page, NULL); + g_assert (visible_page != fixture->page2); + g_object_set (fixture->pm, "visible-page", fixture->page2, NULL); + g_object_get (fixture->pm, "visible-page", &visible_page, NULL); + g_assert (visible_page == fixture->page2); +} + +static void +test_pm_get_set_visible_page_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + const gchar *name; + name = eos_page_manager_get_visible_page_name (EOS_PAGE_MANAGER (fixture->pm)); + g_assert_cmpstr (name, !=, EXPECTED_PAGE_NAME); + eos_page_manager_set_visible_page_name (EOS_PAGE_MANAGER (fixture->pm), + EXPECTED_PAGE_NAME); + name = eos_page_manager_get_visible_page_name (EOS_PAGE_MANAGER (fixture->pm)); + g_assert_cmpstr (name, ==, EXPECTED_PAGE_NAME); +} + +static void +test_pm_prop_visible_page_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + gchar *name; + g_object_get (fixture->pm, "visible-page-name", &name, NULL); + g_assert_cmpstr (name, !=, EXPECTED_PAGE_NAME); + g_free (name); + g_object_set (fixture->pm, "visible-page-name", EXPECTED_PAGE_NAME, NULL); + g_object_get (fixture->pm, "visible-page-name", &name, NULL); + g_assert_cmpstr (name, ==, EXPECTED_PAGE_NAME); + g_free (name); +} + +static void +test_pm_get_set_page_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + const gchar *name; + name = eos_page_manager_get_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page1); + g_assert_cmpstr (name, ==, PAGE1_NAME); + name = eos_page_manager_get_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page2); + g_assert_cmpstr (name, ==, PAGE2_NAME); + name = eos_page_manager_get_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page3); + g_assert_cmpstr (name, ==, PAGE3_NAME); + eos_page_manager_set_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page2, + EXPECTED_CHANGED_PAGE_NAME); + name = eos_page_manager_get_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page2); + g_assert_cmpstr (name, ==, EXPECTED_CHANGED_PAGE_NAME); +} + +static void +test_pm_child_prop_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + gchar *name; + gtk_container_child_get (GTK_CONTAINER (fixture->pm), fixture->page1, + "name", &name, + NULL); + g_assert_cmpstr (name, ==, PAGE1_NAME); + g_free (name); + gtk_container_child_get (GTK_CONTAINER (fixture->pm), fixture->page2, + "name", &name, + NULL); + g_assert_cmpstr (name, ==, PAGE2_NAME); + g_free (name); + gtk_container_child_get (GTK_CONTAINER (fixture->pm), fixture->page3, + "name", &name, + NULL); + g_assert_cmpstr (name, ==, PAGE3_NAME); + g_free (name); + gtk_container_child_set (GTK_CONTAINER (fixture->pm), fixture->page2, + "name", EXPECTED_CHANGED_PAGE_NAME, + NULL); + gtk_container_child_get (GTK_CONTAINER (fixture->pm), fixture->page2, + "name", &name, + NULL); + g_assert_cmpstr (name, ==, EXPECTED_CHANGED_PAGE_NAME); + g_free (name); +} + +static void +test_pm_page_no_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + const gchar *name_get; + gchar *name_prop; + GtkWidget *new_page = gtk_label_new("new"); + gtk_container_add (GTK_CONTAINER (fixture->pm), new_page); + name_get = eos_page_manager_get_page_name (EOS_PAGE_MANAGER (fixture->pm), new_page); + g_assert_cmpstr (name_get, ==, ""); + gtk_container_child_get (GTK_CONTAINER (fixture->pm), new_page, + "name", &name_prop, + NULL); + g_assert_cmpstr (name_prop, ==, ""); + g_free (name_prop); +} + +static void +test_pm_remove_page_behavior (PageManagerFixture *fixture, + gconstpointer unused) +{ + GtkWidget *visible_page; + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == fixture->page1); + gtk_container_remove (GTK_CONTAINER (fixture->pm), fixture->page3); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == fixture->page1); + gtk_container_remove (GTK_CONTAINER (fixture->pm), fixture->page2); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == fixture->page1); + gtk_container_remove (GTK_CONTAINER (fixture->pm), fixture->page1); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == NULL); +} + +static void +test_pm_remove_page_undefined_behavior (PageManagerFixture *fixture, + gconstpointer unused) +{ + GtkWidget *visible_page; + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == fixture->page1); + gtk_container_remove (GTK_CONTAINER (fixture->pm), fixture->page1); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page != fixture->page1); +} + +static void +test_pm_remove_page_by_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + GList *pages = gtk_container_get_children (GTK_CONTAINER (fixture->pm)); + guint length = g_list_length (pages); + g_list_free (pages); + + eos_page_manager_remove_page_by_name (EOS_PAGE_MANAGER (fixture->pm), + PAGE2_NAME); + pages = gtk_container_get_children (GTK_CONTAINER (fixture->pm)); + g_assert_cmpuint (g_list_length (pages), ==, length - 1); + g_assert (g_list_find (pages, fixture->page1) != NULL); + g_assert (g_list_find (pages, fixture->page2) == NULL); + g_assert (g_list_find (pages, fixture->page3) != NULL); + g_list_free (pages); +} + +static void +test_pm_duplicate_page_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + eos_page_manager_set_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page1, + DUPLICATE_PAGE_NAME); + /* Should not complain */ + eos_page_manager_set_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page1, + DUPLICATE_PAGE_NAME); + + g_test_expect_message (TEST_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + EXPECTED_DUPLICATE_PAGE_NAME_ERRMSG); + eos_page_manager_set_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page2, + DUPLICATE_PAGE_NAME); + g_test_assert_expected_messages (); + + const gchar *name = eos_page_manager_get_page_name (EOS_PAGE_MANAGER (fixture->pm), + fixture->page2); + g_assert_cmpstr (name, !=, DUPLICATE_PAGE_NAME); +} + +static void +test_empty_pm_visible_page (PageManagerFixture *fixture, + gconstpointer unused) +{ + GtkWidget *visible_page_get, *visible_page_prop; + visible_page_get = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page_get == NULL); + g_object_get (fixture->pm, "visible-page", &visible_page_prop, NULL); + g_assert (visible_page_prop == NULL); +} + +static void +test_empty_pm_visible_page_name (PageManagerFixture *fixture, + gconstpointer unused) +{ + const gchar *name_get; + gchar *name_prop; + name_get = eos_page_manager_get_visible_page_name (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (name_get == NULL); + g_object_get (fixture->pm, "visible-page-name", &name_prop, NULL); + g_assert (name_prop == NULL); +} + +static void +test_empty_pm_add_page_behavior (PageManagerFixture *fixture, + gconstpointer unused) +{ + GtkWidget *visible_page; + GtkWidget *page1 = gtk_label_new ("page1"); + GtkWidget *page2 = gtk_label_new ("page2"); + gtk_container_add (GTK_CONTAINER (fixture->pm), page1); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == page1); + gtk_container_add (GTK_CONTAINER (fixture->pm), page2); + visible_page = eos_page_manager_get_visible_page (EOS_PAGE_MANAGER (fixture->pm)); + g_assert (visible_page == page1); /* Not page2! */ +} + +void +add_page_manager_tests (void) +{ + ADD_PAGE_MANAGER_TEST ("/page-manager/get-set-visible-page", + test_pm_get_set_visible_page); + ADD_PAGE_MANAGER_TEST ("/page-manager/prop-visible-page", + test_pm_prop_visible_page); + ADD_PAGE_MANAGER_TEST ("/page-manager/get-set-visible-page-name", + test_pm_get_set_visible_page_name); + ADD_PAGE_MANAGER_TEST ("/page-manager/prop-visible-page-name", + test_pm_prop_visible_page_name); + ADD_PAGE_MANAGER_TEST ("/page-manager/get-set-page-name", + test_pm_get_set_page_name); + ADD_PAGE_MANAGER_TEST ("/page-manager/child-prop-name", + test_pm_child_prop_name); + ADD_PAGE_MANAGER_TEST ("/page-manager/page-no-name", test_pm_page_no_name); + ADD_PAGE_MANAGER_TEST ("/page-manager/remove-page-by-name", + test_pm_remove_page_by_name); + ADD_PAGE_MANAGER_TEST ("/page-manager/duplicate-page-name", + test_pm_duplicate_page_name); + ADD_EMPTY_PAGE_MANAGER_TEST ("/page-manager/empty-visible-page", + test_empty_pm_visible_page); + ADD_EMPTY_PAGE_MANAGER_TEST ("/page-manager/empty-visible-page-name", + test_empty_pm_visible_page_name); + ADD_EMPTY_PAGE_MANAGER_TEST ("/page-manager/add-page-behavior", + test_empty_pm_add_page_behavior); + + /* Disabled until https://bugzilla.gnome.org/show_bug.cgi?id=699756 is fixed + [endlessm/eos-sdk#67] */ + if (FALSE) + { + ADD_PAGE_MANAGER_TEST ("/page-manager/remove-page-behavior", + test_pm_remove_page_behavior); + ADD_PAGE_MANAGER_TEST ("/page-manager/remove-page-undefined-behavior", + test_pm_remove_page_undefined_behavior); + } +} -- cgit v1.2.3