diff options
64 files changed, 1860 insertions, 594 deletions
@@ -1,7 +1,7 @@ # Build products endless-0.pc -test/run-tests -test/flexy-grid +test/endless/run-tests +test/demos/flexy-grid test/smoke-tests/hello Endless-0.gir Endless-0.typelib @@ -26,6 +26,7 @@ tools/eos-json-extractor/eos-json-extractor .libs stamp* *.stamp +*.trs /Makefile /Makefile.in /aclocal.m4 @@ -45,6 +46,7 @@ stamp* /m4/ltsugar.m4 /m4/ltversion.m4 /m4/serial-tests.m4 +/test-driver .dirstamp # Gettext droppings diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..beb3d46 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/jasmine"] + path = test/jasmine + url = git@github.com:endlessm/eos-jasmine.git diff --git a/Makefile.am b/Makefile.am index e021742..c635f63 100644 --- a/Makefile.am +++ b/Makefile.am @@ -159,4 +159,4 @@ m4_DATA = \ # # # TESTS # # # -include $(top_srcdir)/test/Makefile.am +include $(top_srcdir)/test/Makefile.am.inc @@ -20,6 +20,10 @@ if [ -d .git ]; then chmod +x .git/hooks/commit-msg fi +git remote set-url origin http://github.com/endlessm/eos-sdk.git +git submodule init . +git submodule update --recursive + # GNU gettext automake support doesn't get along with git # https://bugzilla.gnome.org/show_bug.cgi?id=661128 touch -t 200001010000 $POT_FILE diff --git a/data/assets/category_hover_arrow.png b/data/assets/category_hover_arrow.png Binary files differdeleted file mode 100644 index 13a2fb0..0000000 --- a/data/assets/category_hover_arrow.png +++ /dev/null diff --git a/data/assets/category_splash_separator_shadow.png b/data/assets/category_splash_separator_shadow.png Binary files differdeleted file mode 100644 index 6ef03e0..0000000 --- a/data/assets/category_splash_separator_shadow.png +++ /dev/null diff --git a/data/assets/introduction_back_button_hover.png b/data/assets/introduction_back_button_hover.png Binary files differdeleted file mode 100644 index 418fee7..0000000 --- a/data/assets/introduction_back_button_hover.png +++ /dev/null diff --git a/data/assets/introduction_back_button_normal.png b/data/assets/introduction_back_button_normal.png Binary files differdeleted file mode 100644 index 54ea015..0000000 --- a/data/assets/introduction_back_button_normal.png +++ /dev/null diff --git a/data/assets/introduction_back_button_pressed.png b/data/assets/introduction_back_button_pressed.png Binary files differdeleted file mode 100644 index a1d3bc0..0000000 --- a/data/assets/introduction_back_button_pressed.png +++ /dev/null diff --git a/data/assets/wikipedia-category-back-symbolic.svg b/data/assets/wikipedia-category-back-symbolic.svg new file mode 100644 index 0000000..f844163 --- /dev/null +++ b/data/assets/wikipedia-category-back-symbolic.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" + width="68px" height="68px" viewBox="-0.5 0.5 68 68" enable-background="new -0.5 0.5 68 68" xml:space="preserve"> +<path style="fill:#bebebe" d="M33.5,68.5c18.776,0,34-15.224,34-34c0-18.775-15.224-34-34-34c-18.775,0-34,15.225-34,34 + C-0.5,53.276,14.725,68.5,33.5,68.5z M37.456,16.652l5.151,5.15L29.73,34.682l12.876,12.88l-5.151,5.152L19.424,34.682 + L37.456,16.652z"/> +</svg> diff --git a/data/assets/wikipedia-category-forward-symbolic.svg b/data/assets/wikipedia-category-forward-symbolic.svg new file mode 100644 index 0000000..2fe909d --- /dev/null +++ b/data/assets/wikipedia-category-forward-symbolic.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" + width="42px" height="42px" viewBox="-0.5 0.5 42 42" enable-background="new -0.5 0.5 42 42" xml:space="preserve"> +<path style="fill:#bebebe" d="M20.5,0.5c-11.598,0-21,9.402-21,21s9.402,21,21,21s21-9.402,21-21S32.098,0.5,20.5,0.5z M17.999,32.499L15,29.5l7.998-8 + L15,13.5l2.999-2.999l11,10.999L17.999,32.499z"/> +</svg> diff --git a/data/css/eos-wikipedia-domain.css b/data/css/eos-wikipedia-domain.css index c3c8752..e352407 100644 --- a/data/css/eos-wikipedia-domain.css +++ b/data/css/eos-wikipedia-domain.css @@ -10,15 +10,14 @@ EosWindow { font-family: "Lato Light"; color: #ffffff; text-shadow: 0px 1px 0px alpha(#23326e, 0.15); - padding-top: 15px; } .title.main-element.category.front-page { - font-size: 110px; + font-size: 9.17em; } .title.category.front-page { - font-size: 33px; + font-size: 2.75em; } Gjs_ArticleList{ @@ -30,7 +29,7 @@ Gjs_ArticleList .button { padding-right: 25px; padding-top: 15px; padding-bottom: 15px; - font-size: 14px; + font-size: 1.45em; color: #464646; background-image: url('resource:///com/endlessm/wikipedia-domain/assets/submenu_bg_normal.jpg'); background-size: cover; @@ -54,14 +53,14 @@ Gjs_ArticleList .scrollbar.slider { background-color: #babdb6; } -.category-page #category_info{ - background-color: rgba(0, 0, 0, 0.5); +.category-page #category_info { + background-color: alpha(#464646, 0.8); } .category-page #category_description{ color:rgba(255, 255, 255, 1.0); background-color: rgba(0, 0, 0, 0); - font-size: 11px; + font-size: 1.5em; } .category-page #category_scrolled_window .scrollbar.slider { @@ -71,33 +70,70 @@ Gjs_ArticleList .scrollbar.slider { .category-page #category_title{ font-family: "Lato Light"; - font-size: 50px; + font-size: 4.17em; color: rgba(255, 255, 255, 0.9); } Gjs_CategoryButton.clickable { - transition: background-color 1000ms ease-in-out; + transition: background-color 150ms ease-in-out; } Gjs_CategoryButton.clickable:hover { background-color: alpha(#212121, 0.5); - transition: background-color 100ms ease-in-out; +} + +Gjs_CategoryButton .image { + opacity: 0.0; + transition: opacity 150ms ease-in-out; +} + +Gjs_CategoryButton.clickable .image:hover { + opacity: 1.0; } #side_bar_button:hover { background-color: rgba(0, 0, 0, 0.2); } -.category-page Gjs_AssetButton { +.category-page .back .image { + padding: 15px; + color: black; + opacity: 0.6; + icon-shadow: inset 0 1px 1px alpha(black, 0.5), 0 1px alpha(white, 0.2); +} + +.category-page .back .image:hover { + color: white; +} + +.category-page .back .image:active { + color: #d7d7d7; +} + +.category-page .back .image:hover, +.category-page .back .image:active { + opacity: 0.95; + icon-shadow: inset 0 1px 1px alpha(black, 0.5), + 0 1px alpha(white, 0.2), + 0 0 15px alpha(black, 0.15); +} + +.category-page .back .label { font-weight: bold; color: alpha(white, 0.0); } -.category-page Gjs_AssetButton:prelight { +.category-page .back .label:hover, +.category-page .back .label:active { text-shadow: 0px 1px 0px alpha(#000000, 0.5), 0px 0px 12px alpha(#000000, 0.3); color: alpha(white, 1.0); } +.category-page .back { + padding-left: 25px; + padding-right: 25px; +} + Gjs_BackButton { -GtkButton-image-spacing: 7; padding-top: 2px; diff --git a/data/eos-wikipedia-domain.gresource.xml b/data/eos-wikipedia-domain.gresource.xml index 8442d5e..0d319b1 100644 --- a/data/eos-wikipedia-domain.gresource.xml +++ b/data/eos-wikipedia-domain.gresource.xml @@ -10,12 +10,9 @@ <file>assets/submenu_separator_shadow_b.png</file> <file>assets/submenu_hover_arrow.png</file> <file>assets/submenu_background.jpg</file> - <file>assets/category_splash_separator_shadow.png</file> - <file>assets/category_hover_arrow.png</file> <file>assets/image_strip_back_button.png</file> - <file>assets/introduction_back_button_normal.png</file> - <file>assets/introduction_back_button_pressed.png</file> - <file>assets/introduction_back_button_hover.png</file> + <file compressed="true">assets/wikipedia-category-back-symbolic.svg</file> + <file compressed="true">assets/wikipedia-category-forward-symbolic.svg</file> <file>assets/topbar_back_icon_normal.png</file> </gresource> </gresources> diff --git a/docs/reference/endless/endless-docs.xml b/docs/reference/endless/endless-docs.xml index c602ed2..f946d1f 100644 --- a/docs/reference/endless/endless-docs.xml +++ b/docs/reference/endless/endless-docs.xml @@ -23,6 +23,7 @@ <xi:include href="xml/splash-page-manager.xml"/> <xi:include href="xml/action-button.xml"/> <xi:include href="xml/flexy-grid.xml"/> + <xi:include href="xml/custom-container.xml"/> <!--<xi:include href="xml/hello.xml"/>--> </chapter> diff --git a/docs/reference/endless/endless-sections.txt b/docs/reference/endless/endless-sections.txt index 8752058..963629f 100644 --- a/docs/reference/endless/endless-sections.txt +++ b/docs/reference/endless/endless-sections.txt @@ -32,6 +32,14 @@ EosWindow eos_window_new eos_window_get_page_manager eos_window_set_page_manager +eos_window_get_font_scaling_active +eos_window_set_font_scaling_active +eos_window_get_font_scaling_default_size +eos_window_set_font_scaling_default_size +eos_window_get_font_scaling_default_window_size +eos_window_set_font_scaling_default_window_size +eos_window_get_font_scaling_min_font_size +eos_window_set_font_scaling_min_font_size <SUBSECTION Standard> EosWindowClass EOS_IS_WINDOW @@ -184,3 +192,18 @@ eos_flexy_grid_get_type eos_flexy_grid_cell_get_type eos_flexy_shape_get_type </SECTION> + +<SECTION> +<FILE>custom-container</FILE> +EosCustomContainer +eos_custom_container_new +<SUBSECTION Standard> +EOS_CUSTOM_CONTAINER +EOS_CUSTOM_CONTAINER_CLASS +EOS_CUSTOM_CONTAINER_GET_CLASS +EOS_IS_CUSTOM_CONTAINER +EOS_IS_CUSTOM_CONTAINER_CLASS +EOS_TYPE_CUSTOM_CONTAINER +EosCustomContainerClass +eos_custom_container_get_type +</SECTION> diff --git a/endless/Makefile.am b/endless/Makefile.am index 3a5b95c..74d8b3c 100644 --- a/endless/Makefile.am +++ b/endless/Makefile.am @@ -28,6 +28,7 @@ endless_private_installed_headers = \ endless/eosversion.h \ endless/eosactionbutton.h \ endless/eosapplication.h \ + endless/eoscustomcontainer.h \ endless/eosenums.h \ endless/eosmacros.h \ endless/eospagemanager.h \ @@ -39,6 +40,7 @@ endless_private_installed_headers = \ endless_library_sources = \ endless/eosapplication.c \ + endless/eoscustomcontainer.c \ endless/eoshello.c \ endless/eosinit.c endless/eosinit-private.h \ endless/eospagemanager.c endless/eospagemanager-private.h \ diff --git a/endless/endless.h b/endless/endless.h index 9efbe6e..f3f2061 100644 --- a/endless/endless.h +++ b/endless/endless.h @@ -18,6 +18,7 @@ G_BEGIN_DECLS #include "eospagemanager.h" #include "eossplashpagemanager.h" #include "eoswindow.h" +#include "eoscustomcontainer.h" #undef _EOS_SDK_INSIDE_ENDLESS_H diff --git a/endless/eoscustomcontainer.c b/endless/eoscustomcontainer.c new file mode 100644 index 0000000..a68745b --- /dev/null +++ b/endless/eoscustomcontainer.c @@ -0,0 +1,114 @@ +/* Copyright 2014 Endless Mobile, Inc. */ + +#include "config.h" +#include "eoscustomcontainer.h" + +#include <gtk/gtk.h> + +/** + * SECTION:custom-container + * @short_description: For gjs container implementations + * @title: Custom Container + * + * This container allows for implementing a custom size allocate routine in + * gjs. This container implements the bare minimum of virtual functions from + * GtkContainer, add, remove and forall. Add and remove simply append to and + * remove from an internal list, and forall iterates over that list. Forall + * cannot be implemented in gjs, it's not supported by gobject-introspection, + * so this is needed for custom gjs containers. This class will not + * size_allocate any children or ever queue_resize, so that is up to + * subclasses in gjs. + * + * Here's an example gjs program which allocates a GtkFrame the top right + * quarter of it's allocation. + * |[ + * const TestContainer = Lang.Class({ + * Name: 'TestContainer', + * Extends: Endless.CustomContainer, + * + * _init: function() { + * this.parent(); + * + * this._frame = new Gtk.Frame(); + * this.add(this._frame); + * }, + * + * vfunc_size_allocate: function (alloc) { + * this.parent(alloc); + * alloc.width = alloc.width / 2; + * alloc.height = alloc.height / 2; + * this._frame.size_allocate(alloc); + * } + * }); + * ]| + */ + +typedef struct { + GList *children; +} EosCustomContainerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (EosCustomContainer, eos_custom_container, GTK_TYPE_CONTAINER) + +static void +eos_custom_container_add (GtkContainer *container, + GtkWidget *child) +{ + EosCustomContainer *self = EOS_CUSTOM_CONTAINER (container); + EosCustomContainerPrivate *priv = eos_custom_container_get_instance_private (self); + + priv->children = g_list_prepend (priv->children, child); + gtk_widget_set_parent (child, GTK_WIDGET (container)); +} + +static void +eos_custom_container_remove (GtkContainer *container, + GtkWidget *child) +{ + EosCustomContainer *self = EOS_CUSTOM_CONTAINER (container); + EosCustomContainerPrivate *priv = eos_custom_container_get_instance_private (self); + + priv->children = g_list_remove (priv->children, child); + gtk_widget_unparent (child); +} + +static void +eos_custom_container_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + EosCustomContainer *self = EOS_CUSTOM_CONTAINER (container); + EosCustomContainerPrivate *priv = eos_custom_container_get_instance_private (self); + + g_list_foreach (priv->children, (GFunc)callback, callback_data); +} + +static void +eos_custom_container_class_init (EosCustomContainerClass *klass) +{ + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + container_class->add = eos_custom_container_add; + container_class->remove = eos_custom_container_remove; + container_class->forall = eos_custom_container_forall; +} + +static void +eos_custom_container_init (EosCustomContainer *self) +{ + GtkWidget *widget = GTK_WIDGET (self); + gtk_widget_set_has_window (widget, FALSE); +} + +/** + * eos_custom_container_new: + * + * Creates a new custom container. + * + * Returns: the custom container. + */ +GtkWidget * +eos_custom_container_new (void) +{ + return g_object_new (EOS_TYPE_CUSTOM_CONTAINER, NULL); +} diff --git a/endless/eoscustomcontainer.h b/endless/eoscustomcontainer.h new file mode 100644 index 0000000..743bc59 --- /dev/null +++ b/endless/eoscustomcontainer.h @@ -0,0 +1,67 @@ +/* Copyright 2014 Endless Mobile, Inc. */ + +#ifndef EOS_CUSTOM_CONTAINER_H +#define EOS_CUSTOM_CONTAINER_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 <gtk/gtk.h> + +G_BEGIN_DECLS + +#define EOS_TYPE_CUSTOM_CONTAINER eos_custom_container_get_type() + +#define EOS_CUSTOM_CONTAINER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + EOS_TYPE_CUSTOM_CONTAINER, EosCustomContainer)) + +#define EOS_CUSTOM_CONTAINER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + EOS_TYPE_CUSTOM_CONTAINER, EosCustomContainerClass)) + +#define EOS_IS_CUSTOM_CONTAINER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + EOS_TYPE_CUSTOM_CONTAINER)) + +#define EOS_IS_CUSTOM_CONTAINER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + EOS_TYPE_CUSTOM_CONTAINER)) + +#define EOS_CUSTOM_CONTAINER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + EOS_TYPE_CUSTOM_CONTAINER, EosCustomContainerClass)) + +typedef struct _EosCustomContainer EosCustomContainer; +typedef struct _EosCustomContainerClass EosCustomContainerClass; + +/** + * EosCustomContainer: + * + * This structure contains no public members. + */ +struct _EosCustomContainer +{ + GtkContainer parent; +}; + +struct _EosCustomContainerClass +{ + GtkContainerClass parent_class; + + /* For further expansion */ + gpointer _padding[8]; +}; + +EOS_SDK_ALL_API_VERSIONS +GType eos_custom_container_get_type (void) G_GNUC_CONST; + +EOS_SDK_ALL_API_VERSIONS +GtkWidget *eos_custom_container_new (void); + +G_END_DECLS + +#endif /* EOS_CUSTOM_CONTAINER_H */ diff --git a/endless/eospagemanager.c b/endless/eospagemanager.c index bb0878f..3ceb4c5 100644 --- a/endless/eospagemanager.c +++ b/endless/eospagemanager.c @@ -1073,12 +1073,12 @@ eos_page_manager_set_page_name (EosPageManager *self, GtkWidget *page, const gchar *name) { - EosPageManagerPrivate *priv = eos_page_manager_get_instance_private (self); - EosPageManagerPageInfo *info; - g_return_if_fail (EOS_IS_PAGE_MANAGER (self)); g_return_if_fail (GTK_IS_WIDGET (page)); + EosPageManagerPrivate *priv = eos_page_manager_get_instance_private (self); + EosPageManagerPageInfo *info; + /* Two pages with the same name are not allowed */ if (name != NULL) { diff --git a/endless/eostopbar-private.h b/endless/eostopbar-private.h index cabb414..3d0f4a7 100644 --- a/endless/eostopbar-private.h +++ b/endless/eostopbar-private.h @@ -44,16 +44,18 @@ struct _EosTopBarClass GtkEventBoxClass parent_class; }; -GType eos_top_bar_get_type (void) G_GNUC_CONST; +GType eos_top_bar_get_type (void) G_GNUC_CONST; -GtkWidget *eos_top_bar_new (void); +GtkWidget *eos_top_bar_new (void); -void eos_top_bar_set_left_widget (EosTopBar *self, - GtkWidget *left_top_bar_widget); +void eos_top_bar_set_left_widget (EosTopBar *self, + GtkWidget *left_top_bar_widget); -void -eos_top_bar_set_center_widget (EosTopBar *self, - GtkWidget *center_top_bar_widget); +void eos_top_bar_set_center_widget (EosTopBar *self, + GtkWidget *center_top_bar_widget); + +void eos_top_bar_update_window_maximized (EosTopBar *self, + gboolean is_maximized); G_END_DECLS diff --git a/endless/eostopbar.c b/endless/eostopbar.c index 6975e39..735ba6c 100644 --- a/endless/eostopbar.c +++ b/endless/eostopbar.c @@ -15,7 +15,7 @@ * The #EosTopBar has three different areas that can be managed through this * class: a left widget, center widget, and action buttons area. * - * The action buttons area contain "minimize" and "close" buttons. + * The action buttons area contain "minimize", "maximize" and "close" buttons. */ #define _EOS_STYLE_CLASS_TOP_BAR "top-bar" #define _EOS_TOP_BAR_HEIGHT_PX 36 @@ -25,6 +25,8 @@ #define _EOS_TOP_BAR_BUTTON_SEPARATION_PX 8 #define _EOS_TOP_BAR_VERTICAL_BUTTON_MARGIN_PX 6 #define _EOS_TOP_BAR_MINIMIZE_ICON_NAME "window-minimize-symbolic" +#define _EOS_TOP_BAR_MAXIMIZE_ICON_NAME "window-maximize-symbolic" +#define _EOS_TOP_BAR_UNMAXIMIZE_ICON_NAME "window-unmaximize-symbolic" #define _EOS_TOP_BAR_CLOSE_ICON_NAME "window-close-symbolic" typedef struct { @@ -37,6 +39,8 @@ typedef struct { GtkWidget *minimize_button; GtkWidget *minimize_icon; + GtkWidget *maximize_button; + GtkWidget *maximize_icon; GtkWidget *close_button; GtkWidget *close_icon; } EosTopBarPrivate; @@ -46,6 +50,7 @@ G_DEFINE_TYPE_WITH_PRIVATE (EosTopBar, eos_top_bar, GTK_TYPE_EVENT_BOX) enum { CLOSE_CLICKED, MINIMIZE_CLICKED, + MAXIMIZE_CLICKED, LAST_SIGNAL }; @@ -107,6 +112,17 @@ eos_top_bar_class_init (EosTopBarClass *klass) G_TYPE_NONE, 0); /* + * Emitted when the maximize button has been activated. + */ + top_bar_signals[MAXIMIZE_CLICKED] = + g_signal_new ("maximize-clicked", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /* * Emitted when the close button has been activated. */ top_bar_signals[CLOSE_CLICKED] = @@ -127,6 +143,14 @@ on_minimize_clicked_cb (GtkButton *button, } static void +on_maximize_clicked_cb (GtkButton *button, + gpointer user_data) +{ + EosTopBar *self = EOS_TOP_BAR (user_data); + g_signal_emit (self, top_bar_signals[MAXIMIZE_CLICKED], 0); +} + +static void on_close_clicked_cb (GtkButton *button, gpointer user_data) { @@ -179,6 +203,20 @@ eos_top_bar_init (EosTopBar *self) gtk_container_add (GTK_CONTAINER (priv->minimize_button), priv->minimize_icon); + priv->maximize_button = + g_object_new (GTK_TYPE_BUTTON, + "halign", GTK_ALIGN_END, + "valign", GTK_ALIGN_CENTER, + NULL); + priv->maximize_icon = gtk_image_new (); + eos_top_bar_update_window_maximized (self, TRUE); + g_object_set(priv->maximize_icon, + "pixel-size", _EOS_TOP_BAR_ICON_SIZE_PX, + "margin", _EOS_TOP_BAR_BUTTON_PADDING_PX, + NULL); + gtk_container_add (GTK_CONTAINER (priv->maximize_button), + priv->maximize_icon); + priv->close_button = g_object_new (GTK_TYPE_BUTTON, "halign", GTK_ALIGN_END, @@ -199,6 +237,8 @@ eos_top_bar_init (EosTopBar *self) gtk_container_add (GTK_CONTAINER (priv->actions_grid), priv->center_top_bar_attach); gtk_container_add (GTK_CONTAINER (priv->actions_grid), + priv->maximize_button); + gtk_container_add (GTK_CONTAINER (priv->actions_grid), priv->minimize_button); gtk_container_add (GTK_CONTAINER (priv->actions_grid), priv->close_button); @@ -210,6 +250,8 @@ eos_top_bar_init (EosTopBar *self) g_signal_connect (priv->minimize_button, "clicked", G_CALLBACK (on_minimize_clicked_cb), self); + g_signal_connect (priv->maximize_button, "clicked", + G_CALLBACK (on_maximize_clicked_cb), self); g_signal_connect (priv->close_button, "clicked", G_CALLBACK (on_close_clicked_cb), self); } @@ -281,3 +323,25 @@ eos_top_bar_set_center_widget (EosTopBar *self, priv->center_top_bar_widget); } } + +/* + * eos_top_bar_update_window_maximized: + * @self: the top bar + * @is_maximized: whether the window is currently maximized + * + * Private method for eos_window to update the topbar on the window maximized + * state. The top bar will flip the asset of the maximized button depending on + * the state + */ +void +eos_top_bar_update_window_maximized (EosTopBar *self, + gboolean is_maximized) +{ + g_return_if_fail (EOS_IS_TOP_BAR (self)); + EosTopBarPrivate *priv = eos_top_bar_get_instance_private (self); + + gchar *icon_name = is_maximized ? _EOS_TOP_BAR_UNMAXIMIZE_ICON_NAME : _EOS_TOP_BAR_MAXIMIZE_ICON_NAME; + gtk_image_set_from_icon_name (GTK_IMAGE (priv->maximize_icon), + icon_name, + GTK_ICON_SIZE_SMALL_TOOLBAR); +} diff --git a/endless/eoswindow.c b/endless/eoswindow.c index 3849fb0..bbe2422 100644 --- a/endless/eoswindow.c +++ b/endless/eoswindow.c @@ -33,6 +33,31 @@ * }, * }); * ]| + * + * We will use an application-configurable base font size for application- + * configurable resolution and scale up/down from there for different screen sizes. + * + * Font scaling can be enabled by setting #EosWindow:font-scaling-active to + * true. Font scaling is turned off and the property is false by default. + * + * The default font size by which font scaling will occur can be set by + * #EosWindow:font-scaling-default-size. + * + * The default window resolution height by which font scaling will occur can be + * set by #EosWindow:font-scaling-default-window-size. + * + * The default minimum font size under which a font will never scale can be set + * by #EosWindow:font-scaling-min-font-size. + * + * For instance, supose we have a default font size of 12px, a default window size + * of 720px, and a window allocation of 360px. The calculated base pixel size + * will be 12px * (360px / 720px) = 6px. A corresponding CSS font-size of 1em will + * be equivalent to 6 px. A CSS font-size of 0.5em will be equivalent to 3px. If the + * window is resized to a height of 720px, then the calculated base pixel size will + * be 12px, and the CSS font-size of 1em will be equivalent to 12px. A CSS + * font-size of 0.5em will be equivalent to 6px. If the minimum font size is set + * to 12px, then the font-size will be forced to 12px, ignoring the calculated font + * size of 6px. */ #define DEFAULT_WINDOW_WIDTH 800 @@ -40,6 +65,8 @@ #define BACKGROUND_FRAME_NAME_TEMPLATE "_eos-window-background-%d" +#define FONT_SIZE_TEMPLATE "EosWindow { font-size: %fpx; }" + #define TRANSPARENT_FRAME_CSS_PROPERTIES "{ background-image: none;\n" \ " background-color: transparent\n;" \ " border-width: 0px; }\n" @@ -68,6 +95,15 @@ typedef struct { EosPageManager *page_manager; + gboolean maximized; + + /* For scaling base font-size */ + GtkCssProvider *font_size_provider; + gboolean font_scaling_active; + gint font_scaling_default_size; + gint font_scaling_default_window_size; + gint font_scaling_min_font_size; + /* For keeping track of what to display alongside the current page */ GtkWidget *current_page; gulong visible_page_property_handler; @@ -82,6 +118,10 @@ enum PROP_0, PROP_APPLICATION, PROP_PAGE_MANAGER, + PROP_FONT_SCALING_ACTIVE, + PROP_FONT_SCALING_DEFAULT_SIZE, + PROP_FONT_SCALING_DEFAULT_WINDOW_SIZE, + PROP_FONT_SCALING_MIN_FONT_SIZE, NPROPS }; @@ -236,7 +276,7 @@ format_background_css (EosPageManager *pm, // transparent. So any css styling of EosWindow will "show through" the // pages. if (background_uri == NULL) - return TRANSPARENT_FRAME_CSS_PROPERTIES; + return g_strdup (TRANSPARENT_FRAME_CSS_PROPERTIES); return g_strdup_printf (BACKGROUND_FRAME_CSS_PROPERTIES_TEMPLATE, background_uri, background_size, @@ -274,10 +314,13 @@ update_page_background (EosWindow *self) override_background_css (self, background_css); gtk_stack_set_visible_child (GTK_STACK (priv->background_stack), priv->next_background); + g_free (background_css); // Swap our background frames for next animation GtkWidget *temp = priv->next_background; priv->next_background = priv->current_background; priv->current_background = temp; + + g_free (priv->current_background_css_props); priv->current_background_css_props = next_background_css_props; } @@ -370,6 +413,22 @@ eos_window_get_property (GObject *object, g_value_set_object (value, eos_window_get_page_manager (self)); break; + case PROP_FONT_SCALING_ACTIVE: + g_value_set_boolean (value, priv->font_scaling_active); + break; + + case PROP_FONT_SCALING_DEFAULT_SIZE: + g_value_set_int (value, priv->font_scaling_default_size); + break; + + case PROP_FONT_SCALING_DEFAULT_WINDOW_SIZE: + g_value_set_int (value, priv->font_scaling_default_window_size); + break; + + case PROP_FONT_SCALING_MIN_FONT_SIZE: + g_value_set_int (value, priv->font_scaling_min_font_size); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -409,11 +468,40 @@ eos_window_set_property (GObject *object, eos_window_set_page_manager (self, g_value_get_object (value)); break; + case PROP_FONT_SCALING_ACTIVE: + eos_window_set_font_scaling_active (self, g_value_get_boolean (value)); + break; + + case PROP_FONT_SCALING_DEFAULT_SIZE: + eos_window_set_font_scaling_default_size (self, g_value_get_int (value)); + break; + + case PROP_FONT_SCALING_DEFAULT_WINDOW_SIZE: + eos_window_set_font_scaling_default_window_size (self, g_value_get_int (value)); + break; + + case PROP_FONT_SCALING_MIN_FONT_SIZE: + eos_window_set_font_scaling_min_font_size (self, g_value_get_int (value)); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } +static void +eos_window_finalize (GObject *object) +{ + EosWindow *self = EOS_WINDOW (object); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + + g_object_unref (priv->background_provider); + g_object_unref (priv->font_size_provider); + g_free (priv->current_background_css_props); + + G_OBJECT_CLASS (eos_window_parent_class)->finalize (object); +} + /* Clamp our size request calls so we never ask for a minimal size greater than the available work area. */ static void @@ -483,6 +571,39 @@ eos_window_get_preferred_height (GtkWidget *widget, natural_height); } +/* Updates the base font size depending on the window size. */ +static void +eos_window_size_allocate (GtkWidget *window, GtkAllocation *allocation) +{ + EosWindow *self = EOS_WINDOW (window); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + + if (priv->font_scaling_active) + { + GtkStyleProvider *provider = GTK_STYLE_PROVIDER (priv->font_size_provider); + gdouble base_pixel_size = (gdouble) priv->font_scaling_default_size * + ((gdouble) allocation->height / (gdouble) priv->font_scaling_default_window_size); + + if (base_pixel_size < priv->font_scaling_min_font_size) + base_pixel_size = priv->font_scaling_min_font_size; + + GError *error = NULL; + + gchar *font_size_css = g_strdup_printf (FONT_SIZE_TEMPLATE, base_pixel_size); + GdkScreen *screen = gdk_screen_get_default (); + + gtk_style_context_remove_provider_for_screen (screen, provider); + gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (provider), + font_size_css, -1, &error); + gtk_style_context_add_provider_for_screen (screen, provider, + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_free(font_size_css); + } + + GTK_WIDGET_CLASS (eos_window_parent_class)->size_allocate (window, allocation); +} + /* Our default delete event handler destroys the window. */ static gboolean eos_window_default_delete (GtkWidget* window, @@ -500,8 +621,10 @@ eos_window_class_init (EosWindowClass *klass) object_class->get_property = eos_window_get_property; object_class->set_property = eos_window_set_property; + object_class->finalize = eos_window_finalize; widget_class->get_preferred_height = eos_window_get_preferred_height; widget_class->get_preferred_width = eos_window_get_preferred_width; + widget_class->size_allocate = eos_window_size_allocate; /** * EosWindow:application: @@ -527,23 +650,94 @@ eos_window_class_init (EosWindowClass *klass) EOS_TYPE_PAGE_MANAGER, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * EosWindow:font-scaling-active: + * + * The scaling flag that determines if the windows scale or not. + */ + eos_window_props[PROP_FONT_SCALING_ACTIVE] = + g_param_spec_boolean ("font-scaling-active", "Font scaling active", + "Whether or not EosWindow objects scale font size", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * EosWindow:font-scaling-default-size: + * + * The default font-size by which font scaling will occur. Units are in pixels. + */ + eos_window_props[PROP_FONT_SCALING_DEFAULT_SIZE] = + g_param_spec_int ("font-scaling-default-size", "Font scaling default size", + "This is the default font-size by which font-size for children widgets will scale", + 1, + G_MAXINT, + 12, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * EosWindow:font-scaling-default-window-size: + * + * The base resolution by which font scaling will occur. Units are in pixels. + */ + eos_window_props[PROP_FONT_SCALING_DEFAULT_WINDOW_SIZE] = + g_param_spec_int ("font-scaling-default-window-size", "Font scaling default window size", + "This is the base resolution by which font-size for children widgets will scale", + 1, + G_MAXINT, + 1080, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * EosWindow:font-scaling-min-font-size: + * + * The minimum font-size under which font scaling won't occur. Units are in pixels. + */ + eos_window_props[PROP_FONT_SCALING_MIN_FONT_SIZE] = + g_param_spec_int ("font-scaling-min-font-size", "Font scaling default size", + "This is the minimum font-size under which font-size for children widgets won't scale", + 1, + G_MAXINT, + 8, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, NPROPS, eos_window_props); } static void -on_minimize_clicked_cb (GtkWidget* top_bar) +on_minimize_clicked_cb (GtkWidget *top_bar, + EosWindow *self) { - GtkWidget *window = gtk_widget_get_toplevel (top_bar); + gtk_window_iconify (GTK_WINDOW (self)); +} + +static void +on_maximize_clicked_cb (GtkWidget *top_bar, + EosWindow *self) +{ + EosWindowPrivate *priv = eos_window_get_instance_private (self); - gtk_window_iconify (GTK_WINDOW (window)); + if (priv->maximized) + gtk_window_unmaximize (GTK_WINDOW (self)); + else + gtk_window_maximize (GTK_WINDOW (self)); } static void -on_close_clicked_cb (GtkWidget* top_bar) +on_close_clicked_cb (GtkWidget *top_bar, + EosWindow *self) { - GtkWidget *window = gtk_widget_get_toplevel (top_bar); + gtk_window_close (GTK_WINDOW (self)); +} - gtk_window_close (GTK_WINDOW (window)); +static void +on_window_state_event_cb (GtkWidget *widget, + GdkEventWindowState *event) +{ + EosWindow *self = EOS_WINDOW (widget); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + GdkWindowState window_state = event->new_window_state; + priv->maximized = window_state & GDK_WINDOW_STATE_MAXIMIZED; + eos_top_bar_update_window_maximized (EOS_TOP_BAR (priv->top_bar), priv->maximized); } /* Make sure that the edge finishing does not catch input events */ @@ -597,22 +791,28 @@ eos_window_init (EosWindow *self) gchar *background_name1 = g_strdup_printf (BACKGROUND_FRAME_NAME_TEMPLATE, 1); priv->next_background = g_object_new (GTK_TYPE_FRAME, "name", background_name1, NULL); gtk_container_add (GTK_CONTAINER (priv->background_stack), priv->next_background); + g_free (background_name1); // Add the current background to the stack second. I think the latest added // will be the first visible page in the stack gchar *background_name0 = g_strdup_printf (BACKGROUND_FRAME_NAME_TEMPLATE, 0); priv->current_background = g_object_new (GTK_TYPE_FRAME, "name", background_name0, NULL); gtk_container_add (GTK_CONTAINER (priv->background_stack), priv->current_background); + g_free (background_name0); + + /* Dynamically set the base font-size based on the given window allocation. */ + priv->font_size_provider = gtk_css_provider_new (); priv->background_provider = gtk_css_provider_new (); // We start all the background frames transparent with no styling - priv->current_background_css_props = TRANSPARENT_FRAME_CSS_PROPERTIES; + priv->current_background_css_props = g_strdup (TRANSPARENT_FRAME_CSS_PROPERTIES); gchar *background_css = g_strdup_printf(CSS_TEMPLATE, gtk_widget_get_name (priv->current_background), TRANSPARENT_FRAME_CSS_PROPERTIES, gtk_widget_get_name (priv->next_background), TRANSPARENT_FRAME_CSS_PROPERTIES); override_background_css (self, background_css); + g_free (background_css); priv->main_area = eos_main_area_new (); gtk_overlay_add_overlay (GTK_OVERLAY (priv->overlay), priv->main_area); @@ -642,9 +842,13 @@ eos_window_init (EosWindow *self) gtk_window_set_default_size (GTK_WINDOW (self), DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT); g_signal_connect (priv->top_bar, "minimize-clicked", - G_CALLBACK (on_minimize_clicked_cb), NULL); + G_CALLBACK (on_minimize_clicked_cb), self); + g_signal_connect (priv->top_bar, "maximize-clicked", + G_CALLBACK (on_maximize_clicked_cb), self); g_signal_connect (priv->top_bar, "close-clicked", - G_CALLBACK (on_close_clicked_cb), NULL); + G_CALLBACK (on_close_clicked_cb), self); + g_signal_connect (self, "window-state-event", + G_CALLBACK (on_window_state_event_cb), NULL); eos_window_set_page_manager (self, EOS_PAGE_MANAGER (eos_page_manager_new ())); @@ -696,10 +900,10 @@ void eos_window_set_page_manager (EosWindow *self, EosPageManager *page_manager) { - EosWindowPrivate *priv = eos_window_get_instance_private (self); g_return_if_fail (self != NULL && EOS_IS_WINDOW (self)); g_return_if_fail (page_manager != NULL && EOS_IS_PAGE_MANAGER (page_manager)); + EosWindowPrivate *priv = eos_window_get_instance_private (self); EosMainArea *main_area = EOS_MAIN_AREA (priv->main_area); priv->page_manager = page_manager; @@ -712,3 +916,144 @@ eos_window_set_page_manager (EosWindow *self, g_signal_connect_swapped (priv->page_manager, "notify::visible-page", G_CALLBACK (update_page), self); } + +/** + * eos_window_get_font_scaling_active: + * @self: the window + * + * See #EosWindow:font-scaling-active for details. + * + * Returns: whether or not the font will automatically scale. + */ +gboolean +eos_window_get_font_scaling_active (EosWindow *self) +{ + g_return_val_if_fail (self != NULL && EOS_IS_WINDOW (self), FALSE); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + + return priv->font_scaling_active; +} + +/** + * eos_window_set_font_scaling_active: + * @self: the window + * @is_scaling: true for enabling font scaling and + * false for disabling font scaling + * + * Sets whether or not the font will automatically scale. + * See #EosWindow:font-scaling-active for details. + */ +void +eos_window_set_font_scaling_active (EosWindow *self, + gboolean is_scaling) +{ + g_return_if_fail (self != NULL && EOS_IS_WINDOW (self)); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + priv->font_scaling_active = is_scaling; +} + +/** + * eos_window_get_font_scaling_default_size: + * @self: the window + * + * See #EosWindow:font-scaling-default-size for details. + * + * Returns: the default font size by which the font size of children widgets + * will scale. + */ +gint +eos_window_get_font_scaling_default_size (EosWindow *self) +{ + g_return_val_if_fail (self != NULL && EOS_IS_WINDOW (self), -1); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + + return priv->font_scaling_default_size; +} + +/** + * eos_window_set_font_scaling_default_size: + * @self: the window + * @new_default_font_size: the new default font size + * + * Sets the default font size by which the font size of children widgets + * will scale. See #EosWindow:font-scaling-default-size for details. + */ +void +eos_window_set_font_scaling_default_size (EosWindow *self, + gint new_default_font_size) +{ + g_return_if_fail (self != NULL && EOS_IS_WINDOW (self)); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + priv->font_scaling_default_size = new_default_font_size; +} + +/** + * eos_window_get_font_scaling_default_window_size: + * @self: the window + * + * See #EosWindow:font-scaling-default-window-size for details. + * + * Returns: the default window size by which font scaling + * will occur. + */ +gint +eos_window_get_font_scaling_default_window_size (EosWindow *self) +{ + g_return_val_if_fail (self != NULL && EOS_IS_WINDOW (self), -1); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + + return priv->font_scaling_default_window_size; +} + +/** + * eos_window_set_font_scaling_default_window_size: + * @self: the window + * @new_default_window_size: the new default window size + * + * Sets the default window size by which the font size of children widgets + * will scale. See #EosWindow:font-scaling-default-window-size for details. + */ +void +eos_window_set_font_scaling_default_window_size (EosWindow *self, + gint new_default_window_size) +{ + g_return_if_fail (self != NULL && EOS_IS_WINDOW (self)); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + priv->font_scaling_default_window_size = new_default_window_size; +} + +/** + * eos_window_get_font_scaling_min_font_size: + * @self: the window + * + * See #EosWindow:font-scaling-min-font-size for details. + * + * Returns: the minimum font size below which font scaling + * won't occur. + */ +gint +eos_window_get_font_scaling_min_font_size (EosWindow *self) +{ + g_return_val_if_fail (self != NULL && EOS_IS_WINDOW (self), -1); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + + return priv->font_scaling_min_font_size; +} + +/** + * eos_window_set_font_scaling_min_font_size: + * @self: the window + * @new_min_font_size: the new min font size + * + * Sets the min font size by which the font size of children widgets + * will scale. See #EosWindow:font-scaling-min-font-size for + * details. + */ +void +eos_window_set_font_scaling_min_font_size (EosWindow *self, + gint new_min_font_size) +{ + g_return_if_fail (self != NULL && EOS_IS_WINDOW (self)); + EosWindowPrivate *priv = eos_window_get_instance_private (self); + priv->font_scaling_min_font_size = new_min_font_size; +} diff --git a/endless/eoswindow.h b/endless/eoswindow.h index 820aa09..317f0c5 100644 --- a/endless/eoswindow.h +++ b/endless/eoswindow.h @@ -59,17 +59,45 @@ struct _EosWindowClass }; EOS_SDK_ALL_API_VERSIONS -GType eos_window_get_type (void) G_GNUC_CONST; +GType eos_window_get_type (void) G_GNUC_CONST; EOS_SDK_ALL_API_VERSIONS -GtkWidget *eos_window_new (EosApplication *application); +GtkWidget *eos_window_new (EosApplication *application); EOS_SDK_ALL_API_VERSIONS -EosPageManager *eos_window_get_page_manager (EosWindow *self); +EosPageManager *eos_window_get_page_manager (EosWindow *self); EOS_SDK_ALL_API_VERSIONS -void eos_window_set_page_manager (EosWindow *self, - EosPageManager *page_manager); +void eos_window_set_page_manager (EosWindow *self, + EosPageManager *page_manager); + +EOS_SDK_ALL_API_VERSIONS +gboolean eos_window_get_font_scaling_active (EosWindow *self); + +EOS_SDK_ALL_API_VERSIONS +void eos_window_set_font_scaling_active (EosWindow *self, + gboolean is_scaling); + +EOS_SDK_ALL_API_VERSIONS +gint eos_window_get_font_scaling_default_size (EosWindow *self); + +EOS_SDK_ALL_API_VERSIONS +void eos_window_set_font_scaling_default_size (EosWindow *self, + gint new_default_font_size); + +EOS_SDK_ALL_API_VERSIONS +gint eos_window_get_font_scaling_default_window_size (EosWindow *self); + +EOS_SDK_ALL_API_VERSIONS +void eos_window_set_font_scaling_default_window_size (EosWindow *self, + gint new_default_window_size); + +EOS_SDK_ALL_API_VERSIONS +gint eos_window_get_font_scaling_min_font_size (EosWindow *self); + +EOS_SDK_ALL_API_VERSIONS +void eos_window_set_font_scaling_min_font_size (EosWindow *self, + gint new_min_font_size); G_END_DECLS diff --git a/overrides/endless_private/asset_button.js b/overrides/endless_private/asset_button.js index dbb2282..4216536 100644 --- a/overrides/endless_private/asset_button.js +++ b/overrides/endless_private/asset_button.js @@ -46,7 +46,7 @@ const AssetButton = new Lang.Class({ this.parent(params); this.set_image(this._image); - this._force_center_valign(this); + this.forall(Lang.bind(this, this._force_center_valign)); this.connect('state-flags-changed', Lang.bind(this, this._update_appearance)); }, diff --git a/test/Makefile.am b/test/Makefile.am.inc index e5fef17..74f6fe6 100644 --- a/test/Makefile.am +++ b/test/Makefile.am.inc @@ -1,38 +1,16 @@ # Copyright 2013 Endless Mobile, Inc. -noinst_PROGRAMS = \ - test/run-tests \ - test/flexy-grid \ - test/smoke-tests/hello - TEST_FLAGS = @EOS_SDK_CFLAGS@ -I$(top_srcdir) -DCOMPILING_EOS_SDK TEST_LIBS = @EOS_SDK_LIBS@ $(top_builddir)/libendless-@EOS_SDK_API_VERSION@.la +ENDLESS_TESTS_DIRECTORY = $(top_srcdir)/test -test_run_tests_SOURCES = \ - test/run-tests.c test/run-tests.h \ - test/test-init.c \ - test/test-hello.c \ - test/test-application.c \ - test/test-page-manager.c \ - test/test-splash-page-manager.c \ - test/test-window.c \ - test/test-action-menu.c \ - test/test-action-button.c \ - test/test-flexy-grid.c \ - $(NULL) -test_run_tests_CPPFLAGS = $(TEST_FLAGS) -test_run_tests_LDADD = $(TEST_LIBS) - -test_smoke_tests_hello_SOURCES = test/smoke-tests/hello.c -test_smoke_tests_hello_CPPFLAGS = $(TEST_FLAGS) -test_smoke_tests_hello_LDADD = $(TEST_LIBS) - -test_flexy_grid_SOURCES = test/flexy-grid.c -test_flexy_grid_CPPFLAGS = $(TEST_FLAGS) -test_flexy_grid_LDADD = $(TEST_LIBS) +noinst_PROGRAMS = \ + test/endless/run-tests \ + test/smoke-tests/hello \ + test/demos/flexy-grid +# This variable will be updated in Makefile subdirs (test/endless/Makefile.am.inc) javascript_tests = \ - test/tools/eos-run-test/sanitycheck.js \ test/tools/eos-application-manifest/testInit.js \ test/webhelper/testTranslate.js \ test/webhelper/testWebActions.js \ @@ -42,13 +20,16 @@ javascript_tests = \ $(NULL) EXTRA_DIST += $(javascript_tests) +include test/endless/Makefile.am.inc +include test/demos/Makefile.am.inc +include test/smoke-tests/Makefile.am.inc + # Run tests when running 'make check' TESTS = \ - test/run-tests \ + test/endless/run-tests \ $(javascript_tests) \ $(NULL) -TEST_EXTENSIONS = .js -JS_LOG_COMPILER = tools/eos-run-test +TEST_EXTENSIONS = AM_JS_LOG_FLAGS = \ --include-path=$(top_srcdir)/webhelper \ --include-path=$(top_srcdir) \ @@ -56,6 +37,9 @@ AM_JS_LOG_FLAGS = \ LOG_COMPILER = gtester AM_LOG_FLAGS = -k --verbose +JASMINE_SUBMODULE_PATH = $(top_srcdir)/test/jasmine +include test/jasmine/Makefile-jasmine.am.inc + # Use locally built versions of Endless-0.gir and libraries; this may need to be # changed to AM_TESTS_ENVIRONMENT in a future version of Automake # Set XDG_CONFIG_HOME so as to avoid cluttering the user's actual config diff --git a/test/demos/Makefile.am.inc b/test/demos/Makefile.am.inc new file mode 100644 index 0000000..797cfd3 --- /dev/null +++ b/test/demos/Makefile.am.inc @@ -0,0 +1,5 @@ +# Copyright 2013 Endless Mobile, Inc. + +test_demos_flexy_grid_SOURCES = $(ENDLESS_TESTS_DIRECTORY)/demos/flexy-grid.c +test_demos_flexy_grid_CPPFLAGS = $(TEST_FLAGS) +test_demos_flexy_grid_LDADD = $(TEST_LIBS) diff --git a/test/flexy-grid.c b/test/demos/flexy-grid.c index 2008af4..2008af4 100644 --- a/test/flexy-grid.c +++ b/test/demos/flexy-grid.c diff --git a/test/endless/Makefile.am.inc b/test/endless/Makefile.am.inc new file mode 100644 index 0000000..ac030a3 --- /dev/null +++ b/test/endless/Makefile.am.inc @@ -0,0 +1,22 @@ +# Copyright 2013 Endless Mobile, Inc. + +test_endless_run_tests_SOURCES = \ + $(ENDLESS_TESTS_DIRECTORY)/endless/run-tests.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/run-tests.h \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-init.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-hello.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-application.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-page-manager.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-splash-page-manager.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-window.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-action-menu.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-action-button.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-flexy-grid.c \ + $(ENDLESS_TESTS_DIRECTORY)/endless/test-custom-container.c \ + $(NULL) +test_endless_run_tests_CPPFLAGS = $(TEST_FLAGS) +test_endless_run_tests_LDADD = $(TEST_LIBS) + +javascript_tests += \ + test/endless/testCustomContainer.js \ + $(NULL) diff --git a/test/run-tests.c b/test/endless/run-tests.c index db9c289..4bc4006 100644 --- a/test/run-tests.c +++ b/test/endless/run-tests.c @@ -111,6 +111,7 @@ main (int argc, add_action_menu_tests (); add_action_button_tests (); add_flexy_grid_test (); + add_custom_container_tests (); return g_test_run (); } diff --git a/test/run-tests.h b/test/endless/run-tests.h index 8947a5a..cb52ab7 100644 --- a/test/run-tests.h +++ b/test/endless/run-tests.h @@ -41,5 +41,6 @@ void add_splash_page_manager_tests (void); void add_action_menu_tests (void); void add_action_button_tests (void); void add_flexy_grid_test (void); +void add_custom_container_tests (void); #endif /* RUN_TESTS_H */ diff --git a/test/test-action-button.c b/test/endless/test-action-button.c index 7e4ad15..7e4ad15 100644 --- a/test/test-action-button.c +++ b/test/endless/test-action-button.c diff --git a/test/test-action-menu.c b/test/endless/test-action-menu.c index 0450f6c..0450f6c 100644 --- a/test/test-action-menu.c +++ b/test/endless/test-action-menu.c diff --git a/test/test-application.c b/test/endless/test-application.c index 75165d4..75165d4 100644 --- a/test/test-application.c +++ b/test/endless/test-application.c diff --git a/test/endless/test-custom-container.c b/test/endless/test-custom-container.c new file mode 100644 index 0000000..42cf99f --- /dev/null +++ b/test/endless/test-custom-container.c @@ -0,0 +1,83 @@ +/* Copyright 2014 Endless Mobile, Inc. */ + +#include <gtk/gtk.h> +#include <endless/endless.h> + +#include "run-tests.h" + +typedef struct +{ + GtkContainer *container; + GtkWidget *child1; + GtkWidget *child2; + GtkWidget *child3; +} CustomContainerFixture; + +#define ADD_CUSTOM_CONTAINER_TEST(path, test_func) \ + g_test_add ((path), CustomContainerFixture, NULL, \ + custom_container_fixture_setup, \ + (test_func), \ + custom_container_fixture_teardown) + + +static void +custom_container_fixture_setup (CustomContainerFixture *fixture, + gconstpointer unused G_GNUC_UNUSED) +{ + // We acquire the widget ref so they don't automatically get destroyed after + // being removed from the container. + fixture->child1 = g_object_ref_sink (gtk_label_new ("1")); + fixture->child2 = g_object_ref_sink (gtk_label_new ("2")); + fixture->child3 = g_object_ref_sink (gtk_label_new ("3")); + fixture->container = GTK_CONTAINER (eos_custom_container_new ()); +} + +static void +custom_container_fixture_teardown (CustomContainerFixture *fixture, + gconstpointer unused G_GNUC_UNUSED) +{ + gtk_widget_destroy (fixture->child1); + gtk_widget_destroy (fixture->child2); + gtk_widget_destroy (fixture->child3); + gtk_widget_destroy ((GtkWidget *) fixture->container); + g_object_unref (fixture->child1); + g_object_unref (fixture->child2); + g_object_unref (fixture->child3); +} + +static void +test_custom_container_add (CustomContainerFixture *fixture, + gconstpointer unused G_GNUC_UNUSED) +{ + gtk_container_add (fixture->container, fixture->child1); + gtk_container_add (fixture->container, fixture->child2); + gtk_container_add (fixture->container, fixture->child3); + + g_assert (gtk_widget_get_parent (fixture->child1) == GTK_WIDGET (fixture->container)); + GList *children = gtk_container_get_children (fixture->container); + g_assert (g_list_length (children) == 3); + g_assert (g_list_find (children, fixture->child1) != NULL); + g_assert (g_list_find (children, fixture->child2) != NULL); + g_assert (g_list_find (children, fixture->child3) != NULL); +} + +static void +test_custom_container_remove (CustomContainerFixture *fixture, + gconstpointer unused G_GNUC_UNUSED) +{ + gtk_container_add (fixture->container, fixture->child1); + gtk_container_add (fixture->container, fixture->child2); + gtk_container_add (fixture->container, fixture->child3); + gtk_container_remove (fixture->container, fixture->child2); + + g_assert (gtk_widget_get_parent (fixture->child2) != GTK_WIDGET (fixture->container)); + GList *children = gtk_container_get_children (fixture->container); + g_assert (g_list_find (children, fixture->child2) == NULL); +} + +void +add_custom_container_tests (void) +{ + ADD_CUSTOM_CONTAINER_TEST ("/custom-container/add", test_custom_container_add); + ADD_CUSTOM_CONTAINER_TEST ("/custom-container/remove", test_custom_container_remove); +} diff --git a/test/test-flexy-grid.c b/test/endless/test-flexy-grid.c index 0c6c3cc..0c6c3cc 100644 --- a/test/test-flexy-grid.c +++ b/test/endless/test-flexy-grid.c diff --git a/test/test-hello.c b/test/endless/test-hello.c index 687f14d..687f14d 100644 --- a/test/test-hello.c +++ b/test/endless/test-hello.c diff --git a/test/test-init.c b/test/endless/test-init.c index 482c079..482c079 100644 --- a/test/test-init.c +++ b/test/endless/test-init.c diff --git a/test/test-page-manager.c b/test/endless/test-page-manager.c index a39fb3f..a39fb3f 100644 --- a/test/test-page-manager.c +++ b/test/endless/test-page-manager.c diff --git a/test/test-splash-page-manager.c b/test/endless/test-splash-page-manager.c index 06105aa..06105aa 100644 --- a/test/test-splash-page-manager.c +++ b/test/endless/test-splash-page-manager.c diff --git a/test/test-window.c b/test/endless/test-window.c index 0b47ac6..32639be 100644 --- a/test/test-window.c +++ b/test/endless/test-window.c @@ -94,6 +94,63 @@ test_get_set_page_manager (GApplication *app) } static void +test_get_set_font_scaling_active (GApplication *app) +{ + GtkWidget *win = eos_window_new (EOS_APPLICATION (app)); + + gboolean is_scaling_default = eos_window_get_font_scaling_active (EOS_WINDOW (win)); + g_assert (!is_scaling_default); + + eos_window_set_font_scaling_active (EOS_WINDOW (win), TRUE); + gboolean is_scaling = eos_window_get_font_scaling_active (EOS_WINDOW (win)); + g_assert (is_scaling); + + gtk_widget_destroy (win); +} + +static void +test_get_set_font_scaling_default_size (GApplication *app) +{ + GtkWidget *win = eos_window_new (EOS_APPLICATION (app)); + gint new_font_size = 10; + + eos_window_set_font_scaling_default_size (EOS_WINDOW (win), new_font_size); + gint returned_font_size = eos_window_get_font_scaling_default_size (EOS_WINDOW (win)); + + g_assert (new_font_size == returned_font_size); + + gtk_widget_destroy (win); +} + +static void +test_get_set_font_scaling_default_window_size (GApplication *app) +{ + GtkWidget *win = eos_window_new (EOS_APPLICATION (app)); + gint new_window_size = 720; + + eos_window_set_font_scaling_default_window_size (EOS_WINDOW (win), new_window_size); + gint returned_window_size = eos_window_get_font_scaling_default_window_size (EOS_WINDOW (win)); + + g_assert (new_window_size == returned_window_size); + + gtk_widget_destroy (win); +} + +static void +test_get_set_font_scaling_min_font_size (GApplication *app) +{ + GtkWidget *win = eos_window_new (EOS_APPLICATION (app)); + gint new_min_font_size = 10; + + eos_window_set_font_scaling_min_font_size (EOS_WINDOW (win), new_min_font_size); + gint returned_min_font_size = eos_window_get_font_scaling_min_font_size (EOS_WINDOW (win)); + + g_assert (new_min_font_size == returned_min_font_size); + + gtk_widget_destroy (win); +} + +static void test_prop_page_manager (GApplication *app) { GtkWidget *win = eos_window_new (EOS_APPLICATION (app)); @@ -181,6 +238,14 @@ add_window_tests (void) test_has_default_page_manager); ADD_APP_WINDOW_TEST ("/window/get-set-page-manager", test_get_set_page_manager); + ADD_APP_WINDOW_TEST ("/window/get-set-font-scaling-active", + test_get_set_font_scaling_active); + ADD_APP_WINDOW_TEST ("/window/get-set-font-scaling-default-size", + test_get_set_font_scaling_default_size); + ADD_APP_WINDOW_TEST ("/window/get-set-font-scaling-default-window-size", + test_get_set_font_scaling_default_window_size); + ADD_APP_WINDOW_TEST ("/window/get-set-font-scaling-min-font-size", + test_get_set_font_scaling_min_font_size); ADD_APP_WINDOW_TEST ("/window/prop-page-manager", test_prop_page_manager); ADD_APP_WINDOW_TEST ("/window/main-area-widgets-visibility", test_main_area_widgets_visibility); diff --git a/test/endless/testCustomContainer.js b/test/endless/testCustomContainer.js new file mode 100644 index 0000000..7038beb --- /dev/null +++ b/test/endless/testCustomContainer.js @@ -0,0 +1,26 @@ +const Endless = imports.gi.Endless; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const TestContainer = new Lang.Class({ + Name: 'TestContainer', + Extends: Endless.CustomContainer, + + _init: function (params) { + this.parent(params); + }, + + vfunc_size_allocate: function (alloc) { + this.parent(alloc); + } +}); + +describe("CustomContainer", function () { + it("Instantiates a CustomContainer subclass to make sure no exceptions/segfaults", function () { + let createContainer = function () { + let container = new TestContainer(); + }; + + expect(createContainer).not.toThrow(); + }); +}); diff --git a/test/jasmine b/test/jasmine new file mode 160000 +Subproject 7f9fe7348a1167aee95ac472f4bb7da7701a3e7 diff --git a/test/smoke-tests/Makefile.am.inc b/test/smoke-tests/Makefile.am.inc new file mode 100644 index 0000000..2c0fba3 --- /dev/null +++ b/test/smoke-tests/Makefile.am.inc @@ -0,0 +1,5 @@ +# Copyright 2013 Endless Mobile, Inc. + +test_smoke_tests_hello_SOURCES = $(ENDLESS_TESTS_DIRECTORY)/smoke-tests/hello.c +test_smoke_tests_hello_CPPFLAGS = $(TEST_FLAGS) +test_smoke_tests_hello_LDADD = $(TEST_LIBS) diff --git a/test/smoke-tests/app-window.js b/test/smoke-tests/app-window.js index 6821358..d7554b9 100644 --- a/test/smoke-tests/app-window.js +++ b/test/smoke-tests/app-window.js @@ -3,6 +3,7 @@ const Lang = imports.lang; const Endless = imports.gi.Endless; const Gtk = imports.gi.Gtk; +const Gdk = imports.gi.Gdk; const GObject = imports.gi.GObject; const TEST_APPLICATION_ID = 'com.endlessm.example.test'; @@ -103,12 +104,16 @@ const Toolbox = new Lang.Class ({ this._label2 = new Gtk.Label({ label: 'Actions on page 1' }); this.switch1 = new Gtk.Switch({ active: false }); this.switch2 = new Gtk.Switch({ active: true }); + this.button1 = new Gtk.Button({ label: 'Scale font down' }); + this.button2 = new Gtk.Button({ label: 'Scale font up' }); this.add(this._label); this.add(this._label1); this.add(this.switch1); this.add(this._label2); this.add(this.switch2); + this.add(this.button1); + this.add(this.button2); } }); @@ -214,8 +219,25 @@ const TestApplication = new Lang.Class ({ this._window = new Endless.Window({ application: this, - page_manager: this._pm + page_manager: this._pm, + 'font-scaling-active': true, + 'font-scaling-default-size': 16 }); + + this._toolbox.button1.connect('clicked', Lang.bind(this, function () { + let current_font_size = this._window.get_font_scaling_default_size(); + this._window.set_font_scaling_default_size(current_font_size - 1); + })); + this._toolbox.button2.connect('clicked', Lang.bind(this, function () { + let current_font_size = this._window.get_font_scaling_default_size(); + this._window.set_font_scaling_default_size(current_font_size + 1); + })); + + let provider = new Gtk.CssProvider(); + provider.load_from_data("EosWindow { font-size: 1em; }"); + Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + this._window.show_all(); }, diff --git a/test/smoke-tests/custom-container.js b/test/smoke-tests/custom-container.js new file mode 100644 index 0000000..679e0be --- /dev/null +++ b/test/smoke-tests/custom-container.js @@ -0,0 +1,44 @@ +// Copyright 2014 Endless Mobile, Inc. + +const Lang = imports.lang; +const Endless = imports.gi.Endless; +const Gtk = imports.gi.Gtk; +const GObject = imports.gi.GObject; + +const TEST_APPLICATION_ID = 'com.endlessm.example.test'; + +const TestContainer = Lang.Class({ + Name: 'TestContainer', + Extends: Endless.CustomContainer, + + _init: function() { + this.parent(); + + this._frame = new Gtk.Frame(); + this.add(this._frame); + }, + + vfunc_size_allocate: function (alloc) { + this.parent(alloc); + alloc.width = alloc.width / 2; + alloc.height = alloc.height / 2; + this._frame.size_allocate(alloc); + } +}); + +const TestApplication = new Lang.Class ({ + Name: 'TestApplication', + Extends: Gtk.Application, + + vfunc_startup: function() { + this.parent(); + let window = new Gtk.Window(); + window.add(new TestContainer()); + window.show_all(); + this.add_window(window); + } +}); + +let app = new TestApplication({ application_id: TEST_APPLICATION_ID, + flags: 0 }); +app.run(ARGV); diff --git a/test/webhelper/smoke-tests/webview.js b/test/smoke-tests/webhelper/webview.js index a3b91e5..a3b91e5 100644 --- a/test/webhelper/smoke-tests/webview.js +++ b/test/smoke-tests/webhelper/webview.js diff --git a/test/webhelper/testTranslate.js b/test/webhelper/testTranslate.js index 009efaf..f4d4f68 100644 --- a/test/webhelper/testTranslate.js +++ b/test/webhelper/testTranslate.js @@ -5,14 +5,20 @@ const Lang = imports.lang; const WebHelper = imports.webhelper; const WebKit = imports.gi.WebKit; -const TestClass = new Lang.Class({ - Name: 'testclass', +const WebHelperApplicationWithTranslatableText = new Lang.Class({ + Name: 'WebHelperApplicationWithTranslatableText', Extends: WebHelper.Application, + + get_translation_string: function() { + return 'Translate Me'; + }, vfunc_startup: function() { this.parent(); this.webview = new WebKit.WebView(); - let string = '<html><body><p name="translatable">Translate Me</p></body></html>'; + let string = '<html><body><p name="translatable">' + + this.get_translation_string() + + '</p></body></html>'; this.webview.load_string(string, 'text/html', 'UTF-8', 'file://'); this.win = new Endless.Window({ application: this @@ -36,62 +42,67 @@ const TestClass = new Lang.Class({ } }); -let app; +describe("Translation strategy", function() { + let app; -function setUp() { - // Generate a unique ID for each app instance that we test - let fake_pid = GLib.random_int(); - // FIXME In this version of GJS there is no Posix module, so fake the PID - let id_string = 'com.endlessm.webhelper.test' + GLib.get_real_time() + fake_pid; - app = new TestClass({ - application_id: id_string + beforeEach(function() { + // FIXME In this version of GJS there is no Posix module, so fake the PID + let id_string = 'com.endlessm.webhelper.test' + GLib.get_real_time() + fake_pid; + // Generate a unique ID for each app instance that we test + let fake_pid = GLib.random_int(); + app = new WebHelperApplicationWithTranslatableText({ + application_id: id_string + }); }); -} - -function testStringIsTranslated() { - let translationFunctionWasCalled = false; - let translationFunctionCalledWithString; - app.set_translation_function(function(s) { - translationFunctionWasCalled = true; - translationFunctionCalledWithString = s; - return s; + + describe("translation function", function() { + let translationFunctionSpy; + beforeEach(function() { + translationFunctionSpy = jasmine.createSpy('translate').and.returnValue('Translated'); + }); + it("gets called with string to translate on run", function() { + app.set_translation_function(translationFunctionSpy); + app.run([]); + expect(translationFunctionSpy).toHaveBeenCalledWith(app.get_translation_string()); + }); }); - app.run([]); - assertTrue(translationFunctionWasCalled); - assertEquals('Translate Me', translationFunctionCalledWithString); -} - -// The following test is commented out because GJS cannot catch exceptions -// across FFI interfaces (e.g. in GObject callbacks.) -// function testMissingTranslationFunctionIsHandled() { -// assertRaises(function() { -// app.run([]); -// }); -// } + it("throws when an incompatible type is set as the translation function", function() { + expect(function() { + app.set_translation_function({}); + }).toThrow(); + }); -function testSetBadTranslationFunction() { - assertRaises(function() { - app.set_translation_function("I am not a function"); + // Can't test this right now as there is no support for propagating exceptions across + // GI interfaces + xit("throws when there isn't a translation function set", function() { + expect(function() { + app.run([]); + }).toThrow(); }); -} -function testGetSetTranslationFunction() { - let translationFunction = function(string) { - return string; - }; - app.set_translation_function(translationFunction); - let actualTranslationFunction = app.get_translation_function(); - assertEquals(translationFunction, actualTranslationFunction); -} + it("has a null translation function by default", function() { + expect(app.get_translation_function()).toBe(null); + }); -function testTranslationFunctionIsNullByDefault() { - assertNull(app.get_translation_function()); -} + it("stores the expected translation function", function() { + let translation = function(str) { + return str; + }; + + app.set_translation_function(translation); + expect(app.get_translation_function()).toBe(translation); + }); -function testGetSetNullTranslationFunction() { - app.set_translation_function(function (s) { return s; }); - assertNotNull(app.get_translation_function()); - app.set_translation_function(null); - assertNull(app.get_translation_function()); -} + it("allows us to store a null translation function", function() { + let nonNullTranslation = function(str) { + return str; + } + + // set a non-null translation function first so that we get + // the non-default behaviour for get_translation_function + app.set_translation_function(nonNullTranslation); + app.set_translation_function(null); + expect(app.get_translation_function()).toBe(null); + }); +}); diff --git a/test/webhelper/testWebActions.js b/test/webhelper/testWebActions.js index 8c790b2..b2ddaf0 100644 --- a/test/webhelper/testWebActions.js +++ b/test/webhelper/testWebActions.js @@ -6,8 +6,8 @@ const Lang = imports.lang; const WebHelper = imports.webhelper; const WebKit = imports.gi.WebKit; -const TestClass = new Lang.Class({ - Name: 'testclass', +const WebActionTestApplication = new Lang.Class({ + Name: 'WebActionTestApplication', Extends: WebHelper.Application, vfunc_startup: function() { @@ -33,128 +33,113 @@ const TestClass = new Lang.Class({ } }); -let app; +// TODO: These tests depend on a running X Server and Window Manager. That means +// that they are not runnable in a continuous-integration server +describe("Web Actions Bindings", function() { + let app; + let webActionSpy; + + beforeEach(function() { + // Generate a unique ID for each app instance that we test + let fake_pid = GLib.random_int(); + // FIXME In this version of GJS there is no Posix module, so fake the PID + let id_string = 'com.endlessm.webhelper.test' + GLib.get_real_time() + fake_pid; + app = new WebActionTestApplication({ + application_id: id_string + }); + webActionSpy = jasmine.createSpy('quitAction').and.callFake(function() { + app.quit(); + }); + }); + + let RunApplicationWithWebAction = function(app, action) { + app.webActionToTest = action; + app.run([]); + } + it("has a working quitApplication uri upon defining quitApplication as a string", function() { + app.define_web_action('quitApplication', webActionSpy); + RunApplicationWithWebAction(app, 'endless://quitApplication'); -function setUp() { - // Generate a unique ID for each app instance that we test - let fake_pid = GLib.random_int(); - // FIXME In this version of GJS there is no Posix module, so fake the PID - let id_string = 'com.endlessm.webhelper.test' + GLib.get_real_time() + fake_pid; - app = new TestClass({ - application_id: id_string + expect(webActionSpy).toHaveBeenCalled(); }); -} -function testWebActionIsCalled() { - let actionWasCalled = false; - app.define_web_action('quitApplication', function() { - actionWasCalled = true; - app.quit(); + it("is called with a parameter", function() { + app.define_web_action('getParameterAndQuit', webActionSpy); + RunApplicationWithWebAction(app, 'endless://getParameterAndQuit?param=value'); + + expect(webActionSpy).toHaveBeenCalledWith(new jasmine.ObjectContaining({ param: 'value' })); }); - app.webActionToTest = 'endless://quitApplication'; - app.run([]); - assertTrue(actionWasCalled); -} - -function testWebActionIsCalledWithParameter() { - let actionParameter; - app.define_web_action('getParameterAndQuit', function(dict) { - actionParameter = dict['param']; - app.quit(); + + it("can be called with many parameters", function() { + app.define_web_action('getParametersAndQuit', webActionSpy); + RunApplicationWithWebAction(app, 'endless://getParametersAndQuit?first=thefirst&second=thesecond&third=thethird'); + + expect(webActionSpy).toHaveBeenCalledWith(new jasmine.ObjectContaining({ + first: 'thefirst', + second: 'thesecond', + third: 'thethird' + })); }); - app.webActionToTest = 'endless://getParameterAndQuit?param=value'; - app.run([]); - assertEquals('value', actionParameter); -} - -function testWebActionIsCalledWithManyParameters() { - let firstParameter, secondParameter, thirdParameter; - app.define_web_action('getParametersAndQuit', function(dict) { - firstParameter = dict['first']; - secondParameter = dict['second']; - thirdParameter = dict['third']; - app.quit(); + + it("decodes parameter URI names", function() { + app.define_web_action('getUriDecodedParameterAndQuit', webActionSpy); + RunApplicationWithWebAction(app, 'endless://getUriDecodedParameterAndQuit?p%C3%A4r%C3%A4m%F0%9F%92%A9=value'); + + expect(webActionSpy).toHaveBeenCalledWith(new jasmine.ObjectContaining({ + 'päräm💩' : 'value' + })); }); - app.webActionToTest = 'endless://getParametersAndQuit?first=thefirst&second=thesecond&third=thethird'; - app.run([]); - assertEquals('thefirst', firstParameter); - assertEquals('thesecond', secondParameter); - assertEquals('thethird', thirdParameter); -} - -function testParameterNameIsUriDecoded() { - let expectedParameter = 'päräm💩'; - let parameterWasFound = false; - app.define_web_action('getUriDecodedParameterAndQuit', function(dict) { - parameterWasFound = (expectedParameter in dict); - app.quit(); + + it("decodes parameter URI values", function() { + app.define_web_action('getUriDecodedParameterValueAndQuit', webActionSpy); + RunApplicationWithWebAction(app, 'endless://getUriDecodedParameterValueAndQuit?param=v%C3%A1lu%C3%A9%F0%9F%92%A9'); + + expect(webActionSpy).toHaveBeenCalledWith(new jasmine.ObjectContaining({ + param : 'válué💩' + })); }); - app.webActionToTest = 'endless://getUriDecodedParameterAndQuit?p%C3%A4r%C3%A4m%F0%9F%92%A9=value'; - app.run([]); - assertTrue(parameterWasFound); -} - -function testParameterValueIsUriDecoded() { - let expectedValue = 'válué💩'; - let actualValue; - app.define_web_action('getUriDecodedValueAndQuit', function(dict) { - actualValue = dict['param']; - app.quit(); + + // We currently can't catch exceptions across GObject-Introspection callbacks + xit('bad action is not called', function() { + expect(function() { RunApplicationWithWebAction(app, 'endless://nonexistentWebAction') }).toThrow(); }); - app.webActionToTest = 'endless://getUriDecodedValueAndQuit?param=v%C3%A1lu%C3%A9%F0%9F%92%A9'; - app.run([]); - assertEquals(expectedValue, actualValue); -} - -// This is commented out because GJS cannot catch exceptions across FFI -// interfaces (e.g. in GObject callbacks.) -// function testBadActionIsNotCalled() { -// app.webActionToTest = 'endless://nonexistentAction?param=value'; -// assertRaises(function() { app.run([]); }); -// } - -function testWebActionIsCalledWithBlankParameter() { - let parameterWasFound = false; - let parameterValue; - app.define_web_action('getBlankValueAndQuit', function(dict) { - parameterWasFound = ('param' in dict); - if(parameterWasFound) - parameterValue = dict['param']; - app.quit(); + + describe("with blank parameters", function() { + beforeEach(function() { + app.define_web_action('getBlankValueAndQuit', webActionSpy); + RunApplicationWithWebAction(app, 'endless://getBlankValueAndQuit?param='); + }); + + it("can be called", function() { + expect(webActionSpy).toHaveBeenCalled(); + }); + + it("is called with a paramater that is an empty string", function() { + expect(webActionSpy).toHaveBeenCalledWith(new jasmine.ObjectContaining({ + 'param' : '' + })); + }); }); - app.webActionToTest = 'endless://getBlankValueAndQuit?param='; - app.run([]); - assertTrue(parameterWasFound); - assertNotUndefined(parameterValue); - assertEquals('', parameterValue); -} - -function testWebActionIsUriDecoded() { - let actionWasCalled = false; - app.define_web_action('äction💩Quit', function(dict) { - actionWasCalled = true; - app.quit(); + + it("URI decodes the action", function() { + app.define_web_action('äction💩Quit', webActionSpy); + RunApplicationWithWebAction(app, 'endless://%C3%A4ction%F0%9F%92%A9Quit'); + expect(webActionSpy).toHaveBeenCalled(); }); - app.webActionToTest = 'endless://%C3%A4ction%F0%9F%92%A9Quit'; - app.run([]); - assertTrue(actionWasCalled); -} - -function testDefineMultipleActionsOverride() { - let actionWasCalled = false; - app.define_web_actions({ - quitApplication: function() { - actionWasCalled = true; - app.quit(); - } + + it("allows web actions to be defined as object properties", function() { + app.define_web_actions({ + quitApplication: webActionSpy + }); + + RunApplicationWithWebAction(app, 'endless://quitApplication'); + + expect(webActionSpy).toHaveBeenCalled(); }); - app.webActionToTest = 'endless://quitApplication'; - app.run([]); - assertTrue(actionWasCalled); -} - -function testDefineBadAction() { - assertRaises(function() { - app.define_web_action('badAction', 'not a function'); + + it("throws an error when trying to define an action that is not a function", function() { + expect(function() { + app.define_web_action('action', {}); + }).toThrow(); }); -} +}); diff --git a/test/wikipedia/models/testArticleModel.js b/test/wikipedia/models/testArticleModel.js index 655f187..f916369 100644 --- a/test/wikipedia/models/testArticleModel.js +++ b/test/wikipedia/models/testArticleModel.js @@ -1,72 +1,110 @@ const ArticleModel = imports.wikipedia.models.article_model; -let mockJsonData = { - title: 'Article Title', - url: 'file:///', - source: 'Mock data', - categories: [ - 'Category One', - 'Category Two' - ] -}; - -function _assertCategoryListHasIds(categoryList, idList) { - assertEquals(idList.length, categoryList.length); - idList.forEach(function (id) { - assertTrue(categoryList.some(function (actualId) { - return actualId == id; - })); - }); -} - -function testNewModelFromJson() { - let model = ArticleModel.newFromJson(mockJsonData); - assertTrue(model instanceof ArticleModel.ArticleModel); - assertEquals('Article Title', model.title); - assertEquals('file:///', model.uri); - _assertCategoryListHasIds(model.getCategories(), - ['Category One', 'Category Two']); -} - -function testNewWithProperties() { - let model = new ArticleModel.ArticleModel({ +describe("Wikipedia article model", function() { + let mockJsonData = { title: 'Article Title', - uri: 'file:///' + url: 'file:///', + source: 'Mock data', + categories: [ + 'Category One', + 'Category Two' + ] + }; + + describe("from JSON", function() { + let model; + + beforeEach(function() { + model = ArticleModel.newFromJson(mockJsonData); + }); + + it("has an article title", function() { + expect(model.title).toEqual(mockJsonData.title); + }); + + it("has a uri", function() { + expect(model.uri).toEqual(mockJsonData.url); + }); + + it("has a list of categories", function() { + expect(model.getCategories()).toEqual(mockJsonData.categories); + }); + }); + + describe("from properties", function() { + let model; + beforeEach(function() { + model = new ArticleModel.ArticleModel({ + title: 'Article Title', + uri: 'file://' + }); + }); + + it("is an instance of an ArticleModel", function() { + expect(model instanceof ArticleModel.ArticleModel).toBeTruthy(); + }); + + it("has a title", function() { + expect(model.title).toEqual('Article Title'); + }); + + it("has a URI", function() { + expect(model.uri).toEqual('file://'); + }); + + it("has no categories", function() { + expect(model.getCategories().length).toEqual(0); + }); + }); + + describe("setCategories method", function() { + let model; + + beforeEach(function() { + model = new ArticleModel.ArticleModel(); + }); + + it("adds categories", function() { + let expectedCategories = ['One', 'Two', 'Three']; + model.setCategories(expectedCategories); + expect(model.getCategories()).toEqual(expectedCategories); + }); + + it("replaces existing categories", function() { + model.setCategories(['One', 'Two']); + let expectedCategories = ['One', 'Two', 'Three']; + model.setCategories(expectedCategories); + expect(model.getCategories()).toEqual(expectedCategories); + }); }); - assertEquals('Article Title', model.title); - assertEquals('file:///', model.uri); - assertEquals(0, model.getCategories().length); -} - -function testSetAndGetCategories() { - let model = new ArticleModel.ArticleModel(); - let expectedCategories = ['One', 'Two', 'Three']; - model.setCategories(expectedCategories); - _assertCategoryListHasIds(model.getCategories(), expectedCategories); -} - -function testSetCategoriesWipesPreviousCategories() { - let model = new ArticleModel.ArticleModel(); - let firstCategories = ['One', 'Two', 'Three']; - model.setCategories(firstCategories); - let expectedCategories = ['A', 'B', 'C', 'D']; - model.setCategories(expectedCategories); - _assertCategoryListHasIds(model.getCategories(), expectedCategories); -} - -function testAddAndGetCategories() { - let model = new ArticleModel.ArticleModel(); - model.addCategory('One'); - model.addCategory('Two'); - model.addCategory('Three'); - _assertCategoryListHasIds(model.getCategories(), ['One', 'Two', 'Three']); -} - -function testHasCategories() { - let model = new ArticleModel.ArticleModel(); - let expectedCategories = ['One', 'Two', 'Three']; - model.setCategories(expectedCategories); - expectedCategories.forEach(function (id) { - assertTrue(model.hasCategory(id)); + + it("appends new categories on addCategory", function() { + let model = new ArticleModel.ArticleModel(); + + model.addCategory('One'); + model.addCategory('Two'); + model.addCategory('Three'); + expect(model.getCategories()).toEqual(['One', 'Two', 'Three']); + }); + describe("hasCategory method", function() { + let model; + let expectedCategories = ['One', 'Two', 'Three']; + + beforeEach(function() { + model = new ArticleModel.ArticleModel; + model.setCategories(expectedCategories); + }); + + expectedCategories.forEach(function(category) { + (function(categoryName) { + it("returns true for category named " + categoryName, function() { + expect(model.hasCategory(categoryName)).toBeTruthy(); + }); + }); + }); + + it("returns false for an unexpected category", function() { + expect(model.hasCategory('unexpected')).toBeFalsy(); + }); }); -} +}); diff --git a/test/wikipedia/models/testCategoryModel.js b/test/wikipedia/models/testCategoryModel.js index 8d15665..4ffc1a0 100644 --- a/test/wikipedia/models/testCategoryModel.js +++ b/test/wikipedia/models/testCategoryModel.js @@ -1,76 +1,180 @@ const CategoryModel = imports.wikipedia.models.category_model; -let mockJsonData = { - category_name: 'Category Name', - content_text: 'Lorem Ipsum', - image_file: 'file:///image.jpg', - image_thumb_uri: 'file:///thumb.jpg', - is_main_category: false, - subcategories: [ 'Category Two' ] -}; - -function testNewModelFromJson() { - let model = CategoryModel.newFromJson(mockJsonData); - assertTrue(model instanceof CategoryModel.CategoryModel); - assertEquals('Category Name', model.id); - assertEquals(0, model.getSubcategories().length); -} - -function testNewWithProperties() { - let model = new CategoryModel.CategoryModel({ - id: 'id', - title: 'title', - description: 'description', - image_uri: 'image-uri', - image_thumbnail_uri: 'image-thumbnail-uri', - is_main_category: true, - has_articles: true +describe("Category Model", function() { + let mockJsonData = { + category_name: 'Category Name', + content_text: 'Lorem Ipsum', + image_file: 'file:///image.jpg', + image_thumb_uri: 'file:///thumb.jpg', + is_main_category: false, + subcategories: [ 'Category Two' ] + }; + describe("from JSON", function() { + + let model; + beforeEach(function() { + model = CategoryModel.newFromJson(mockJsonData); + }); + + it("is a CategoryModel", function() { + expect(model instanceof CategoryModel.CategoryModel).toBeTruthy(); + }); + + it("has an id", function() { + expect(model.id).toEqual(mockJsonData.category_name); + }); + + it("has no subcategories", function() { + expect(model.getSubcategories().length).toEqual(0); + }); }); - assertEquals('id', model.id); - assertEquals('title', model.title); - assertEquals('description', model.description); - assertEquals('image-uri', model.image_uri); - assertEquals('image-thumbnail-uri', model.image_thumbnail_uri); - assertEquals(true, model.is_main_category); - assertEquals(true, model.has_articles); - - model.has_articles = false; - assertEquals(false, model.has_articles); -} - -function testGetSubcategoriesEmpty() { - let model = new CategoryModel.CategoryModel(); - assertEquals(0, model.getSubcategories().length); -} - -function _assertCategoryListContainsCategoryIds(categoryList, idList) { - assertEquals(idList.length, categoryList.length); - idList.forEach(function (id) { - assertTrue(categoryList.some(function (categoryModel) { - return categoryModel.id == id; - })); + + describe("from properties", function() { + let model; + + beforeEach(function() { + model = new CategoryModel.CategoryModel({ + id: 'id', + title: 'title', + description: 'description', + image_uri: 'image-uri', + image_thumbnail_uri: 'image-thumbnail-uri', + is_main_category: true, + has_articles: true + }); + }); + + it("has an id", function() { + expect(model.id).toEqual('id'); + }); + + it("has a title", function() { + expect(model.title).toEqual('title'); + }); + + it("has a description", function() { + expect(model.description).toEqual('description'); + }); + + it("has an image uri", function() { + expect(model.image_uri).toEqual('image-uri'); + }); + + it("has an image thumbnail uri", function() { + expect(model.image_thumbnail_uri).toEqual('image-thumbnail-uri'); + }); + + it("is a main category", function() { + expect(model.is_main_category).toBeTruthy(); + }); + + it("has articles", function() { + expect(model.has_articles).toBeTruthy(); + }); + + // FIXME: This seems to be a fairly useless test. Does it actually + // test anything? + it("does not have articles once the flag is unset", function() { + model.has_articles = false; + expect(model.has_articles).toBeFalsy(); + }); + }); + + it("starts with no subcategories", function() { + let model = new CategoryModel.CategoryModel(); + + expect(model.getSubcategories().length).toEqual(0); + }); + + describe("in a tree-like structure", function() { + let parent; + + beforeEach(function() { + jasmine.addMatchers({ + toContainCategoriesWithNames: function() { + return { + compare: function(actual, names) { + let result = { + pass: (function() { + let outer_pass = true; + names.forEach(function (id) { + let categories = actual.getSubcategories(); + if (!categories.some(function(category) { + return category.id == id; + })) { + outer_pass = false; + } + }); + return outer_pass; + })(), + + message: (function() { + let msg = "Expected categories with the following names\n"; + names.forEach(function(name) { + msg += " " + name + "\n"; + }); + msg += "Object actually has the following categories\n"; + actual.getSubcategories().forEach(function(category) { + msg += " " + category.id + "\n"; + }); + return msg; + })() + } + + return result; + } + } + }, + toHaveOnlyTheFollowingCategoriesInOrder: function() { + return { + compare: function(actual, names) { + let result = { + pass: (function() { + let categories = actual.getSubcategories(); + if (categories.length != names.length) + return false; + + for (let i = 0; i < categories.length; i++) { + if (categories[i].id != names[i]) + return false; + } + + return true; + })(), + + message: (function() { + let msg = "Expected exactly the following category names\n"; + names.forEach(function(name) { + msg += " " + name + "\n"; + }); + + msg += "Actually had the following category names\n"; + actual.getSubcategories().forEach(function(category) { + msg += " " + category.id + "\n"; + }); + + return msg; + })() + } + + return result; + } + } + } + }); + + parent = new CategoryModel.CategoryModel({ id: 'Category One' }); + parent.addSubcategory(new CategoryModel.CategoryModel({ id: 'Category Two' })); + parent.addSubcategory(new CategoryModel.CategoryModel({ id: 'Category Three' })); + }); + + it("has subcategories", function() { + expect(parent).toContainCategoriesWithNames(['Category Two', 'Category Three']); + }); + + it("silently does not add duplicates", function() { + parent.addSubcategory(new CategoryModel.CategoryModel({ id: 'Category Two' })); + expect(parent).toHaveOnlyTheFollowingCategoriesInOrder(['Category Two', 'Category Three']); + }); }); -} - -function testAddAndGetSubcategories() { - let model1 = new CategoryModel.CategoryModel({ id: 'Category One' }); - let model2 = new CategoryModel.CategoryModel({ id: 'Category Two' }); - let model3 = new CategoryModel.CategoryModel({ id: 'Category Three' }); - model1.addSubcategory(model2); - model1.addSubcategory(model3); - - let categories = model1.getSubcategories(); - _assertCategoryListContainsCategoryIds(categories, - ['Category Two', 'Category Three']); -} - -function testAddSubcategoryDoesNothingForDuplicate() { - let model1 = new CategoryModel.CategoryModel({ id: 'Category One' }); - let model2 = new CategoryModel.CategoryModel({ id: 'Category Two' }); - let model3 = new CategoryModel.CategoryModel({ id: 'Category Two' }); - model1.addSubcategory(model2); - model1.addSubcategory(model3); - - let categories = model1.getSubcategories(); - _assertCategoryListContainsCategoryIds(categories, ['Category Two']); -} +}); diff --git a/test/wikipedia/models/testDomainWikiModel.js b/test/wikipedia/models/testDomainWikiModel.js index e4e4d3f..f30d1bd 100644 --- a/test/wikipedia/models/testDomainWikiModel.js +++ b/test/wikipedia/models/testDomainWikiModel.js @@ -1,144 +1,208 @@ const DomainWikiModel = imports.wikipedia.models.domain_wiki_model; -let model; - -let mockJsonData = { - categories: [ - { - category_name: 'Main Category', - content_text: 'Lorem Ipsum', - image_file: 'file:///image.jpg', - image_thumb_uri: 'file:///image_thumb.jpg', - is_main_category: true, - subcategories: [ - 'Category One', - 'Category Two' - ] - }, - { - category_name: 'Category One', - content_text: 'Lorem Ipsum', - image_file: 'file:///image.jpg', - image_thumb_uri: 'file:///image_thumb.jpg', - is_main_category: false, - subcategories: [] - }, - { - category_name: 'Category Two', - content_text: 'Lorem Ipsum', - image_file: 'file:///image.jpg', - image_thumb_uri: 'file:///image_thumb.jpg', - is_main_category: false, - subcategories: [ - 'Category Three' - ] - }, - { - category_name: 'Category Three', - content_text: 'Lorem Ipsum', - image_file: 'file:///image.jpg', - image_thumb_uri: 'file:///image_thumb.jpg', - is_main_category: false, - subcategories: [] - }, - ], - articles: [ - { - title: 'Article One', - url: 'file:///article1.html', - source: 'Mock data', - categories: [ - 'Category One' - ] - }, - { - title: 'Article Two', - url: 'file:///article2.html', - source: 'Mock data', - categories: [ - 'Category One', - 'Category Two' - ] - }, - { - title: 'Article Three', - url: 'file:///article3.html', - source: 'Mock data', - categories: [ - 'Category Two' - ] - } - ] -}; - -function setUp() { - model = new DomainWikiModel.DomainWikiModel(); -} - -function _assertArticleListContainsArticleTitles(articleList, titleList) { - assertEquals(titleList.length, articleList.length); - titleList.forEach(function (title) { - assertTrue(articleList.some(function (articleModel) { - return articleModel.title == title; - })); +describe('Domain Wiki Model', function () { + const mockJsonData = { + categories: [ + { + category_name: 'Main Category', + content_text: 'Lorem Ipsum', + image_file: 'file:///image.jpg', + image_thumb_uri: 'file:///image_thumb.jpg', + is_main_category: true, + subcategories: [ + 'Category One', + 'Category Two' + ] + }, + { + category_name: 'Category One', + content_text: 'Lorem Ipsum', + image_file: 'file:///image.jpg', + image_thumb_uri: 'file:///image_thumb.jpg', + is_main_category: false, + subcategories: [] + }, + { + category_name: 'Category Two', + content_text: 'Lorem Ipsum', + image_file: 'file:///image.jpg', + image_thumb_uri: 'file:///image_thumb.jpg', + is_main_category: false, + subcategories: [ + 'Category Three' + ] + }, + { + category_name: 'Category Three', + content_text: 'Lorem Ipsum', + image_file: 'file:///image.jpg', + image_thumb_uri: 'file:///image_thumb.jpg', + is_main_category: false, + subcategories: [] + } + ], + articles: [ + { + title: 'Article One', + url: 'file:///article1.html', + source: 'Mock data', + categories: [ + 'Category One' + ] + }, + { + title: 'Article Two', + url: 'file:///article2.html', + source: 'Mock data', + categories: [ + 'Category One', + 'Category Two' + ] + }, + { + title: 'Article Three', + url: 'file:///article3.html', + source: 'Mock data', + categories: [ + 'Category Two' + ] + } + ] + }; + beforeEach(function () { + let model = new DomainWikiModel.DomainWikiModel(); + + jasmine.addMatchers({ + toHaveObjectsContainingProperties: function () { + return { + compare: function (actual, propertyMap) { + let result = { + pass: (function () { + for (let property in propertyMap) { + let allValuesListedHaveAMatchForObject = actual.some(function (object) { + if (object[property] == 'undefined') { + return false; + } + + let propertyValueMatchedForObject = + propertyMap[property].some(function (value) { + return object[property] == value; + }); + + return propertyValueMatchedForObject; + }); + + if (!allValuesListedHaveAMatchForObject) + return false; + } + + return true; + })(), + + message: (function () { + let msg = 'Expected objects to have the following values for the following properties \n'; + for (let property in propertyMap) { + msg += ' - Property: ' + property + '\n'; + for (let value in propertyMap[property]) { + msg += ' * Value: ' + propertyMap[property][value].toString() + '\n'; + } + } + + msg += 'Object actually has the following toplevel properties\n'; + + for (let i = 0; i < actual.length; i++) { + let object = actual[i]; + msg += ' Object in position ' + i + '\n'; + for (let property in object) { + msg += ' - ' + property + ' : ' + object[property] + '\n'; + } + } + + return msg; + })() + }; + + return result; + } + }; + } + }); + }); + + describe('when loaded from some mock JSON data', function () { + let model; + beforeEach(function () { + model = new DomainWikiModel.DomainWikiModel(); + model.loadFromJson(mockJsonData); + }); + + it('returns all articles when getting articles', function () { + let articles = model.getArticles(); + expect(articles).toHaveObjectsContainingProperties({ + title: [ 'Article One', 'Article Two', 'Article Three' ] + }); + }); + + it('can get articles for a category', function () { + let articles = model.getArticlesForCategory('Category One'); + expect(articles).toHaveObjectsContainingProperties({ + title: [ 'Article One', 'Article Two' ] + }); + }); + + it('has no articles on a category that does not have articles', function () { + let articles = model.getArticlesForCategory('Main Category'); + expect(articles.length).toEqual(0); + }); + + it('has no articles for a category that does not exist', function () { + let articles = model.getArticlesForCategory('Nonexistent'); + expect(articles.length).toEqual(0); + }); + + it('can check whether or not a category has articles', function () { + expect(model._getCategoryHasArticles('Category Two')).toBeTruthy(); + }); + + it('can check whether or not a category does not have articles', function () { + expect(model._getCategoryHasArticles('Category Three')).toBeFalsy(); + }); + + it('verifies that a category that does not exist has no articles', function () { + expect(model._getCategoryHasArticles('Nonexistent')).toBeFalsy(); + }); + + describe('category fetch', function () { + let category; + + beforeEach(function () { + category = model.getCategory('Category One'); + }); + + it('actually returns a category', function () { + expect(category.__name__).toEqual('CategoryModel'); + }); + + it('returns the right category', function () { + expect(category.title).toEqual('Category One'); + }); + }); + + it("returns an undefined value if we try to get a category that doesn't exist", function () { + expect(model.getCategory('Nonexistent')).toBeUndefined(); + }); + + it("returns 'Main Category' when getting the main category", function () { + let category = model.getMainCategory(); + + expect(category).toEqual(new jasmine.ObjectContaining({ + 'title' : 'Main Category' + })); + }); + }); + + it('returns null when the Main Category is unset', function () { + let model = new DomainWikiModel.DomainWikiModel(); + expect(model.getMainCategory()).toBeNull(); }); -} - -function testGetArticlesReturnsAllArticles() { - model.loadFromJson(mockJsonData); - let articles = model.getArticles(); - _assertArticleListContainsArticleTitles(articles, - [ 'Article One', 'Article Two', 'Article Three' ]); -} - -function testGetArticlesForCategoryWithArticles() { - model.loadFromJson(mockJsonData); - let articles = model.getArticlesForCategory('Category One'); - _assertArticleListContainsArticleTitles(articles, - [ 'Article One', 'Article Two' ]); -} - -function testGetArticlesForCategoryWithoutArticles() { - model.loadFromJson(mockJsonData); - assertEquals(0, model.getArticlesForCategory('Main Category').length); -} - -function testGetArticlesForCategoryWithNonexistentId() { - assertEquals(0, model.getArticlesForCategory('Nonexistent').length); -} - -function testCategoryHasArticlesReturnsTrue() { - model.loadFromJson(mockJsonData); - assertTrue(model._getCategoryHasArticles('Category Two')); -} - -function testCategoryHasArticlesReturnsFalse() { - model.loadFromJson(mockJsonData); - assertFalse(model._getCategoryHasArticles('Category Three')); -} - -function testCategoryHasArticlesWithNonexistentId() { - assertFalse(model._getCategoryHasArticles('Nonexistent')); -} - -function testGetCategory() { - model.loadFromJson(mockJsonData); - let category = model.getCategory('Category One'); - assertEquals('CategoryModel', category.__name__); - assertEquals('Category One', category.title); -} - -function testGetNonexistentCategory() { - assertUndefined(model.getCategory('Nonexistent')); -} - -function testGetMainCategory() { - model.loadFromJson(mockJsonData); - let category = model.getMainCategory(); - assertTrue(category.__name__ == 'CategoryModel'); - assertEquals('Main Category', category.title); -} - -function testGetUnsetMainCategory() { - assertNull(model.getMainCategory()); -} +}); diff --git a/tools/eos-application-manifest/eos-application-manifest.in b/tools/eos-application-manifest/eos-application-manifest.in index caf70e2..507ff2b 100644 --- a/tools/eos-application-manifest/eos-application-manifest.in +++ b/tools/eos-application-manifest/eos-application-manifest.in @@ -1,4 +1,4 @@ -#!/usr/bin/gjs +#!/usr/bin/env gjs // Copyright 2013 Endless Mobile, Inc. const Format = imports.format; diff --git a/tools/eos-json-extractor/eos-json-extractor.in b/tools/eos-json-extractor/eos-json-extractor.in index 28c5e8e..fd61d4b 100644 --- a/tools/eos-json-extractor/eos-json-extractor.in +++ b/tools/eos-json-extractor/eos-json-extractor.in @@ -1,4 +1,4 @@ -#!/usr/bin/gjs +#!/usr/bin/env gjs // Copyright 2013 Endless Mobile, Inc. const Format = imports.format; diff --git a/tools/eos-run-test.in b/tools/eos-run-test.in index e36fdd7..d58bd5e 100644 --- a/tools/eos-run-test.in +++ b/tools/eos-run-test.in @@ -1,4 +1,4 @@ -#!/usr/bin/gjs +#!/usr/bin/env gjs const Format = imports.format; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; diff --git a/wikipedia/Makefile.am.inc b/wikipedia/Makefile.am.inc index 6f214d1..00e5984 100644 --- a/wikipedia/Makefile.am.inc +++ b/wikipedia/Makefile.am.inc @@ -24,6 +24,8 @@ js_sources = \ wikipedia/ArticleList.js \ wikipedia/widgets/BackButton.js \ wikipedia/widgets/BoxWithBg.js \ + wikipedia/widgets/category_back_button.js \ + wikipedia/widgets/composite_button.js \ wikipedia/widgets/FixedSizeTextView.js \ wikipedia/EndlessWikipedia.js \ wikipedia/PrebuiltArticlesPage.js \ diff --git a/wikipedia/PrebuiltCategoryPage.js b/wikipedia/PrebuiltCategoryPage.js index 4f38b41..3211c59 100644 --- a/wikipedia/PrebuiltCategoryPage.js +++ b/wikipedia/PrebuiltCategoryPage.js @@ -1,25 +1,19 @@ const Endless = imports.gi.Endless; -const Gettext = imports.gettext; -const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const BoxWithBg = imports.wikipedia.widgets.BoxWithBg; -const Config = imports.wikipedia.config; +const CategoryBackButton = imports.wikipedia.widgets.category_back_button; const FixedSizeTextView = imports.wikipedia.widgets.FixedSizeTextView; const ScaledImage = imports.wikipedia.widgets.scaled_image; -const SUBMENU_SEPARATOR_A_URI = "/com/endlessm/wikipedia-domain/assets/submenu_separator_shadow_a.png"; -const SPLASH_SEPARATOR_URI = "/com/endlessm/wikipedia-domain/assets/category_splash_separator_shadow.png"; +const SHADOW_SEPARATOR_RESOURCE_PATH = "/com/endlessm/wikipedia-domain/assets/submenu_separator_shadow_a.png"; const INTRO_TITLE_SEPARATOR_URI = "/com/endlessm/wikipedia-domain/assets/introduction_title_separator.png"; const LEFT_MARGIN_FOR_TEXT = 45; GObject.ParamFlags.READWRITE = GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE; -const _ = function(string) { return GLib.dgettext('eos-sdk', string); }; -Gettext.bindtextdomain('eos-sdk', Config.DATADIR + '/locale'); - function _resourceUriToPath(uri) { if(uri.startsWith('resource://')) return uri.slice('resource://'.length); @@ -80,13 +74,13 @@ const PrebuiltCategoryPage = new Lang.Class({ }); this._submenu_separator = new ScaledImage.ScaledImage({ - resource: SUBMENU_SEPARATOR_A_URI, + resource: SHADOW_SEPARATOR_RESOURCE_PATH, constraint: Gtk.Orientation.VERTICAL, halign: Gtk.Align.END }); this._splash_separator = new ScaledImage.ScaledImage({ - resource: SPLASH_SEPARATOR_URI, + resource: SHADOW_SEPARATOR_RESOURCE_PATH, constraint: Gtk.Orientation.VERTICAL, halign: Gtk.Align.END }); @@ -119,16 +113,12 @@ const PrebuiltCategoryPage = new Lang.Class({ this._description_scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); - this._back_button = new Endless.AssetButton({ - valign: Gtk.Align.CENTER, - normal_image_uri: "resource://com/endlessm/wikipedia-domain/assets/introduction_back_button_normal.png", - active_image_uri: "resource://com/endlessm/wikipedia-domain/assets/introduction_back_button_pressed.png", - prelight_image_uri: "resource://com/endlessm/wikipedia-domain/assets/introduction_back_button_hover.png", - label: _("OTHER CATEGORIES"), - margin_right: 10, - margin_left: 40 + this._back_button = new CategoryBackButton.CategoryBackButton({ + name: "category-back-button", + expand: true, + halign: Gtk.Align.START, + valign: Gtk.Align.FILL }); - this._back_button.connect('clicked', Lang.bind(this, function() { this.emit('go-back-home'); })); diff --git a/wikipedia/presenters/domain_wiki_presenter.js b/wikipedia/presenters/domain_wiki_presenter.js index b348825..764bb17 100644 --- a/wikipedia/presenters/domain_wiki_presenter.js +++ b/wikipedia/presenters/domain_wiki_presenter.js @@ -58,6 +58,7 @@ const DomainWikiPresenter = new Lang.Class({ this._view.set_personality(personality); this._view.set_app_name(app_name); + this.parent(); }, initPageRankFromJsonFile: function(filename){ diff --git a/wikipedia/utils.js b/wikipedia/utils.js index f08deed..0838bd5 100644 --- a/wikipedia/utils.js +++ b/wikipedia/utils.js @@ -164,7 +164,8 @@ const array_contains = function (arr, obj, same_type) { /* * Loads a pixbuf sized to cover the dest_width and dest_height with the - * image in res_path, while mataining the aspect ratio of the image + * image in res_path, while mataining the aspect ratio of the image. + * The anchor point for cropping is the bottom left of the image. */ function load_pixbuf_cover(res_path, dest_width, dest_height) { let [load_width, load_height] = [dest_width, dest_height]; @@ -184,7 +185,8 @@ function load_pixbuf_cover(res_path, dest_width, dest_height) { load_width, load_height, true); let cropped_pixbuf = source_pixbuf; if(dest_width < source_pixbuf.width || dest_height < source_pixbuf.height) - cropped_pixbuf = source_pixbuf.new_subpixbuf(0, 0, dest_width, dest_height); + cropped_pixbuf = source_pixbuf.new_subpixbuf(0, source_pixbuf.height - dest_height, + dest_width, dest_height); return cropped_pixbuf; } diff --git a/wikipedia/views/domain_wiki_view.js b/wikipedia/views/domain_wiki_view.js index a238f6b..7483767 100644 --- a/wikipedia/views/domain_wiki_view.js +++ b/wikipedia/views/domain_wiki_view.js @@ -1,7 +1,10 @@ const EndlessWikipedia = imports.wikipedia.EndlessWikipedia; const Lang = imports.lang; +const System = imports.system; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; const Endless = imports.gi.Endless; const BackButton = imports.wikipedia.widgets.BackButton; @@ -29,7 +32,8 @@ const DomainWikiView = new Lang.Class({ this._presenter = null; this._window = new Endless.Window({ - application: application + 'application': application, + 'font-scaling-active': true }); // These need to be called first @@ -53,6 +57,16 @@ const DomainWikiView = new Lang.Class({ }) this._window.show_all(); + + // A temporary measure to prevent memory usage from blowing up. The app + // will sometimes allocate pixbufs as part of its draw function and gjs + // will not cleanup unused pixbufs unless we force it to. + this._window.connect_after("draw", function () { + Gdk.threads_add_idle(GLib.PRIORITY_LOW, function () { + System.gc(); + return false; + }); + }); }, create_front_page: function(){ diff --git a/wikipedia/widgets/ListTextButton.js b/wikipedia/widgets/ListTextButton.js index b676b8c..c87150f 100644 --- a/wikipedia/widgets/ListTextButton.js +++ b/wikipedia/widgets/ListTextButton.js @@ -6,7 +6,7 @@ const Pango = imports.gi.Pango; // This is an approximate number of characters that will keep the label from // going over its specified width -const ARTICLE_LABEL_MAX_WIDTH_CHARS = 22; +const ARTICLE_LABEL_MAX_WIDTH_CHARS = 20; const ListTextButton = new Lang.Class({ Name: 'EndlessListTextButton', @@ -30,7 +30,7 @@ const ListTextButton = new Lang.Class({ }); this._label = new Gtk.Label({ - label: label_text.toUpperCase(), + label: label_text, max_width_chars: ARTICLE_LABEL_MAX_WIDTH_CHARS, ellipsize: Pango.EllipsizeMode.END }); diff --git a/wikipedia/widgets/category_back_button.js b/wikipedia/widgets/category_back_button.js new file mode 100644 index 0000000..312d7af --- /dev/null +++ b/wikipedia/widgets/category_back_button.js @@ -0,0 +1,50 @@ +// Copyright 2014 Endless Mobile, Inc. + +const Gettext = imports.gettext; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const CompositeButton = imports.wikipedia.widgets.composite_button; +const Config = imports.wikipedia.config; + +const _ = function (string) { return GLib.dgettext('eos-sdk', string); }; +Gettext.bindtextdomain('eos-sdk', Config.DATADIR + '/locale'); + +const CATEGORY_BACK_BUTTON_RESOURCE_URI = 'resource:///com/endlessm/wikipedia-domain/assets/wikipedia-category-back-symbolic.svg'; +const CATEGORY_BACK_BUTTON_SIZE_PIXELS = 68; +const STYLE_CONTEXT_LABEL = 'label'; +const STYLE_CONTEXT_BACK = 'back'; + +const CategoryBackButton = new Lang.Class({ + Name: 'CategoryBackButton', + GTypeName: 'CategoryBackButton', + Extends: CompositeButton.CompositeButton, + + _init: function(props) { + this.parent(props); + + let gicon = new Gio.FileIcon({ + file: Gio.File.new_for_uri(CATEGORY_BACK_BUTTON_RESOURCE_URI) + }); + let icon = Gtk.Image.new_from_gicon(gicon, Gtk.IconSize.DIALOG); + icon.pixel_size = CATEGORY_BACK_BUTTON_SIZE_PIXELS; + let label = new Gtk.Label({ + label: _("OTHER CATEGORIES") + }); + let innerGrid = new Gtk.Grid({ + expand: true, + valign: Gtk.Align.CENTER + }); + + innerGrid.add(icon); + innerGrid.add(label); + this.add(innerGrid); + this.setSensitiveChildren([icon, label]); + + // Define style classes for CSS + icon.get_style_context().add_class(Gtk.STYLE_CLASS_IMAGE); + label.get_style_context().add_class(STYLE_CONTEXT_LABEL); + this.get_style_context().add_class(STYLE_CONTEXT_BACK); + } +}); diff --git a/wikipedia/widgets/category_button.js b/wikipedia/widgets/category_button.js index e6cfdde..08ce2ed 100644 --- a/wikipedia/widgets/category_button.js +++ b/wikipedia/widgets/category_button.js @@ -4,24 +4,20 @@ const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; +const CompositeButton = imports.wikipedia.widgets.composite_button; const Utils = imports.wikipedia.utils; -const CATEGORY_LABEL_LEFT_MARGIN = 25; // pixels -const CATEGORY_LABEL_BOTTOM_MARGIN = 20; // pixels -const CATEGORY_BUTTON_RIGHT_MARGIN = 20; // pixels -const CATEGORY_BUTTON_BOTTOM_MARGIN = 20; // pixels -// The following two are corrections because GTK 3.8 doesn't have baseline -// alignment. Remove and align properly in GTK 3.10. FIXME -const CATEGORY_LABEL_BASELINE_CORRECTION = 0; // pixels -const CATEGORY_BUTTON_BASELINE_CORRECTION = 10; // pixels -const _HOVER_ARROW_URI = '/com/endlessm/wikipedia-domain/assets/category_hover_arrow.png'; +const CATEGORY_LABEL_LEFT_MARGIN_PIXELS = 5; // in addition to the 20px below +const CATEGORY_LABEL_SPACING_PIXELS = 20; +const CATEGORY_BUTTON_SIZE_PIXELS = 42; +const CATEGORY_BUTTON_RESOURCE_URI = 'resource:///com/endlessm/wikipedia-domain/assets/wikipedia-category-forward-symbolic.svg'; const CATEGORY_MIN_WIDTH = 120; // pixels GObject.ParamFlags.READWRITE = GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE; const CategoryButton = new Lang.Class({ Name: 'CategoryButton', - Extends: Gtk.Button, + Extends: CompositeButton.CompositeButton, Properties: { // resource URI for the category's accompanying image 'image-uri': GObject.ParamSpec.string('image-uri', @@ -60,31 +56,31 @@ const CategoryButton = new Lang.Class({ this._is_main_category = null; this._pixbuf = null; - this._overlay = new Gtk.Overlay({ + this._inner_grid = new Gtk.Grid({ + valign: Gtk.Align.END, + halign: Gtk.Align.FILL, + border_width: CATEGORY_LABEL_SPACING_PIXELS, + column_spacing: CATEGORY_LABEL_SPACING_PIXELS, expand: true }); this._label = new Gtk.Label({ - margin_left: CATEGORY_LABEL_LEFT_MARGIN, - margin_bottom: CATEGORY_LABEL_BOTTOM_MARGIN - CATEGORY_LABEL_BASELINE_CORRECTION, + margin_left: CATEGORY_LABEL_LEFT_MARGIN_PIXELS, halign: Gtk.Align.START, - valign: Gtk.Align.END, + valign: Gtk.Align.BASELINE, xalign: 0.0, // deprecated Gtk.Misc properties; necessary because wrap: true, // "wrap" doesn't respect "halign" - width_chars: 18, max_width_chars: 20 }); this._arrow = new Gtk.Image({ - resource: _HOVER_ARROW_URI, - margin_right: CATEGORY_BUTTON_RIGHT_MARGIN, - margin_bottom: CATEGORY_BUTTON_BOTTOM_MARGIN + CATEGORY_BUTTON_BASELINE_CORRECTION, + gicon: new Gio.FileIcon({ + file: Gio.File.new_for_uri(CATEGORY_BUTTON_RESOURCE_URI) + }), + pixel_size: CATEGORY_BUTTON_SIZE_PIXELS, + hexpand: true, halign: Gtk.Align.END, valign: Gtk.Align.END }); - // Make the arrow image transparent to mouse events - this._arrow.connect_after('realize', function (frame) { - let gdk_window = frame.get_window(); - gdk_window.set_child_input_shapes(); - }); + this._arrow.get_style_context().add_class(Gtk.STYLE_CLASS_IMAGE); let context = this._label.get_style_context(); context.add_class(EndlessWikipedia.STYLE_CLASS_TITLE); @@ -95,21 +91,11 @@ const CategoryButton = new Lang.Class({ this.parent(props); // Put widgets together - let alignment = new Gtk.Alignment({ expand: true }); - alignment.add(this._label); - this._overlay.add(alignment); - this._overlay.add_overlay(this._arrow); - this.add(this._overlay); + this.setSensitiveChildren([this._arrow]); + this._inner_grid.add(this._label); + this._inner_grid.add(this._arrow); + this.add(this._inner_grid); this.show_all(); - this._arrow.hide(); - - this.connect("enter", Lang.bind(this, function (w) { - if(this._clickable_category) - this._arrow.show(); - })); - this.connect("leave", Lang.bind(this, function (w) { - this._arrow.hide(); - })); }, get image_uri() { @@ -152,8 +138,6 @@ const CategoryButton = new Lang.Class({ if(this._is_main_category) { let context = this._label.get_style_context(); context.add_class(EndlessWikipedia.STYLE_CLASS_MAIN); - this._label.margin_bottom = 0; - this._label.width_chars = 8; this._label.max_width_chars = 9; } }, diff --git a/wikipedia/widgets/composite_button.js b/wikipedia/widgets/composite_button.js new file mode 100644 index 0000000..71fb559 --- /dev/null +++ b/wikipedia/widgets/composite_button.js @@ -0,0 +1,57 @@ +// Copyright 2014 Endless Mobile, Inc. + +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +// Class for buttons whose :hover and :active CSS pseudoclass states should be +// inherited by some of their child widgets, since as of GTK 3.10 these flags no +// longer propagate from a widget to its children. Widgets in sensitiveChildren +// will listen to this widget's state-flags-changed event and inherit all flag +// values listed in _INHERITED_FLAGS. + +const CompositeButton = new Lang.Class({ + Name: 'CompositeButton', + GTypeName: 'CompositeButton', + Extends: Gtk.Button, + + _INHERITED_FLAGS: [Gtk.StateFlags.PRELIGHT, Gtk.StateFlags.ACTIVE], + + _init: function (props) { + this._handlerSet = false; + this._sensitiveChildren = []; + this.parent(props); + }, + + // Set the list of child widgets which will inherit the CompositeButton's + // hover/active state flags. + setSensitiveChildren: function (children) { + this._sensitiveChildren = children; + // If the handlers for mouse events aren't already set, connect them + if (!this._handlerSet) { + this._connectStateChangedHandler(); + } + }, + + _connectStateChangedHandler: function () { + this.connect('state-flags-changed', + Lang.bind(this, this._stateChangedHandler)); + this._handlerSet = true; + }, + + _stateChangedHandler: function (widget, flags) { + let myFlags = this.get_state_flags(); + this._sensitiveChildren.forEach(function (child) { + this._INHERITED_FLAGS.forEach(function (flag) { + // for each flag we want the children to inherit, grab this + // widget's flag value, and set the child's matching flag + // accordingly + let myFlag = myFlags & flag; + if (myFlag !== 0) + child.set_state_flags(flag, true); + else + child.unset_state_flags(flag); + + }); + }, this); + } +}); |