summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.gitmodules3
-rw-r--r--Makefile.am2
-rwxr-xr-xautogen.sh4
-rw-r--r--data/assets/category_hover_arrow.pngbin3047 -> 0 bytes
-rw-r--r--data/assets/category_splash_separator_shadow.pngbin1601 -> 0 bytes
-rw-r--r--data/assets/introduction_back_button_hover.pngbin9252 -> 0 bytes
-rw-r--r--data/assets/introduction_back_button_normal.pngbin8932 -> 0 bytes
-rw-r--r--data/assets/introduction_back_button_pressed.pngbin8029 -> 0 bytes
-rw-r--r--data/assets/wikipedia-category-back-symbolic.svg9
-rw-r--r--data/assets/wikipedia-category-forward-symbolic.svg8
-rw-r--r--data/css/eos-wikipedia-domain.css60
-rw-r--r--data/eos-wikipedia-domain.gresource.xml7
-rw-r--r--docs/reference/endless/endless-docs.xml1
-rw-r--r--docs/reference/endless/endless-sections.txt23
-rw-r--r--endless/Makefile.am2
-rw-r--r--endless/endless.h1
-rw-r--r--endless/eoscustomcontainer.c114
-rw-r--r--endless/eoscustomcontainer.h67
-rw-r--r--endless/eospagemanager.c6
-rw-r--r--endless/eostopbar-private.h16
-rw-r--r--endless/eostopbar.c66
-rw-r--r--endless/eoswindow.c367
-rw-r--r--endless/eoswindow.h38
-rw-r--r--overrides/endless_private/asset_button.js2
-rw-r--r--test/Makefile.am.inc (renamed from test/Makefile.am)46
-rw-r--r--test/demos/Makefile.am.inc5
-rw-r--r--test/demos/flexy-grid.c (renamed from test/flexy-grid.c)0
-rw-r--r--test/endless/Makefile.am.inc22
-rw-r--r--test/endless/run-tests.c (renamed from test/run-tests.c)1
-rw-r--r--test/endless/run-tests.h (renamed from test/run-tests.h)1
-rw-r--r--test/endless/test-action-button.c (renamed from test/test-action-button.c)0
-rw-r--r--test/endless/test-action-menu.c (renamed from test/test-action-menu.c)0
-rw-r--r--test/endless/test-application.c (renamed from test/test-application.c)0
-rw-r--r--test/endless/test-custom-container.c83
-rw-r--r--test/endless/test-flexy-grid.c (renamed from test/test-flexy-grid.c)0
-rw-r--r--test/endless/test-hello.c (renamed from test/test-hello.c)0
-rw-r--r--test/endless/test-init.c (renamed from test/test-init.c)0
-rw-r--r--test/endless/test-page-manager.c (renamed from test/test-page-manager.c)0
-rw-r--r--test/endless/test-splash-page-manager.c (renamed from test/test-splash-page-manager.c)0
-rw-r--r--test/endless/test-window.c (renamed from test/test-window.c)65
-rw-r--r--test/endless/testCustomContainer.js26
m---------test/jasmine0
-rw-r--r--test/smoke-tests/Makefile.am.inc5
-rw-r--r--test/smoke-tests/app-window.js24
-rw-r--r--test/smoke-tests/custom-container.js44
-rw-r--r--test/smoke-tests/webhelper/webview.js (renamed from test/webhelper/smoke-tests/webview.js)0
-rw-r--r--test/webhelper/testTranslate.js117
-rw-r--r--test/webhelper/testWebActions.js215
-rw-r--r--test/wikipedia/models/testArticleModel.js172
-rw-r--r--test/wikipedia/models/testCategoryModel.js248
-rw-r--r--test/wikipedia/models/testDomainWikiModel.js346
-rw-r--r--tools/eos-application-manifest/eos-application-manifest.in2
-rw-r--r--tools/eos-json-extractor/eos-json-extractor.in2
-rw-r--r--tools/eos-run-test.in2
-rw-r--r--wikipedia/Makefile.am.inc2
-rw-r--r--wikipedia/PrebuiltCategoryPage.js28
-rw-r--r--wikipedia/presenters/domain_wiki_presenter.js1
-rw-r--r--wikipedia/utils.js6
-rw-r--r--wikipedia/views/domain_wiki_view.js16
-rw-r--r--wikipedia/widgets/ListTextButton.js4
-rw-r--r--wikipedia/widgets/category_back_button.js50
-rw-r--r--wikipedia/widgets/category_button.js62
-rw-r--r--wikipedia/widgets/composite_button.js57
64 files changed, 1860 insertions, 594 deletions
diff --git a/.gitignore b/.gitignore
index c86f245..5b72c1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/autogen.sh b/autogen.sh
index 6c20886..4f7b9e3 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -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
deleted file mode 100644
index 13a2fb0..0000000
--- a/data/assets/category_hover_arrow.png
+++ /dev/null
Binary files differ
diff --git a/data/assets/category_splash_separator_shadow.png b/data/assets/category_splash_separator_shadow.png
deleted file mode 100644
index 6ef03e0..0000000
--- a/data/assets/category_splash_separator_shadow.png
+++ /dev/null
Binary files differ
diff --git a/data/assets/introduction_back_button_hover.png b/data/assets/introduction_back_button_hover.png
deleted file mode 100644
index 418fee7..0000000
--- a/data/assets/introduction_back_button_hover.png
+++ /dev/null
Binary files differ
diff --git a/data/assets/introduction_back_button_normal.png b/data/assets/introduction_back_button_normal.png
deleted file mode 100644
index 54ea015..0000000
--- a/data/assets/introduction_back_button_normal.png
+++ /dev/null
Binary files differ
diff --git a/data/assets/introduction_back_button_pressed.png b/data/assets/introduction_back_button_pressed.png
deleted file mode 100644
index a1d3bc0..0000000
--- a/data/assets/introduction_back_button_pressed.png
+++ /dev/null
Binary files differ
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);
+ }
+});