summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--configure.ac3
-rw-r--r--docs/reference/endless/Makefile.am6
-rw-r--r--docs/reference/endless/endless-docs.xml5
-rw-r--r--docs/reference/endless/endless-sections.txt2
-rw-r--r--endless/Makefile.am3
-rw-r--r--endless/eosapplication.c404
-rw-r--r--endless/eosapplication.h7
-rw-r--r--endless/eosattribution-private.h57
-rw-r--r--endless/eosattribution.c683
-rw-r--r--endless/eoscellrendererpixbuflink-private.h52
-rw-r--r--endless/eoscellrendererpixbuflink.c59
-rw-r--r--endless/eoscellrenderertextlink-private.h52
-rw-r--r--endless/eoscellrenderertextlink.c85
-rw-r--r--endless/eostopbar-private.h5
-rw-r--r--endless/eostopbar.c175
-rw-r--r--endless/eoswindow.c41
-rw-r--r--po/POTFILES.in2
-rw-r--r--test/endless/test-application.c69
-rw-r--r--test/smoke-tests/Makefile.am.inc15
-rw-r--r--test/smoke-tests/credits.js44
-rw-r--r--test/smoke-tests/images/Fahrradrheinpromenade.jpgbin0 -> 30304 bytes
-rw-r--r--test/smoke-tests/images/attribution.json27
-rw-r--r--test/smoke-tests/images/credits.gresource.xml10
-rw-r--r--test/smoke-tests/images/test1.jpgbin0 -> 91158 bytes
-rw-r--r--test/smoke-tests/images/test2.jpgbin0 -> 22951 bytes
-rw-r--r--test/smoke-tests/images/test3.jpgbin0 -> 60084 bytes
27 files changed, 1781 insertions, 26 deletions
diff --git a/.gitignore b/.gitignore
index 21142f6..3c8c7a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ endless-0.pc
test/endless/run-tests
test/demos/flexy-grid
test/smoke-tests/hello
+test/smoke-tests/images/credits.gresource
Endless-0.gir
Endless-0.typelib
endless/eosresource.c
diff --git a/configure.ac b/configure.ac
index 2d4f1b9..a802732 100644
--- a/configure.ac
+++ b/configure.ac
@@ -80,10 +80,11 @@ GLIB_REQUIREMENT="glib-2.0 >= 2.38"
GOBJECT_REQUIREMENT="gobject-2.0"
GIO_REQUIREMENT="gio-2.0"
GTK_REQUIREMENT="gtk+-3.0 >= 3.10"
+JSON_GLIB_REQUIREMENT="json-glib-1.0 >= 0.12"
# These go into the pkg-config file as Requires: and Requires.private:
# (Generally, use Requires.private: instead of Requires:)
EOS_REQUIRED_MODULES=
-EOS_REQUIRED_MODULES_PRIVATE="$GLIB_REQUIREMENT $GOBJECT_REQUIREMENT $GIO_REQUIREMENT $GTK_REQUIREMENT"
+EOS_REQUIRED_MODULES_PRIVATE="$GLIB_REQUIREMENT $GOBJECT_REQUIREMENT $GIO_REQUIREMENT $GTK_REQUIREMENT $JSON_GLIB_REQUIREMENT"
AC_SUBST(EOS_REQUIRED_MODULES)
AC_SUBST(EOS_REQUIRED_MODULES_PRIVATE)
diff --git a/docs/reference/endless/Makefile.am b/docs/reference/endless/Makefile.am
index 63219eb..a4091ba 100644
--- a/docs/reference/endless/Makefile.am
+++ b/docs/reference/endless/Makefile.am
@@ -54,7 +54,11 @@ IGNORE_HFILES= eosinit-private.h \
eosmainarea-private.h \
eosactionmenu-private.h \
eospagemanager-private.h \
- eosflexygrid-private.h
+ eosflexygrid-private.h \
+ eosattribution-private.h \
+ eoscellrendererpixbuflink-private.h \
+ eoscellrenderertextlink-private.h \
+ $(NULL)
# Images to copy into HTML directory.
# e.g. HTML_IMAGES=$(top_srcdir)/gtk/stock-icons/stock_about_24.png
diff --git a/docs/reference/endless/endless-docs.xml b/docs/reference/endless/endless-docs.xml
index bf70d99..e94162d 100644
--- a/docs/reference/endless/endless-docs.xml
+++ b/docs/reference/endless/endless-docs.xml
@@ -35,6 +35,11 @@
<xi:include href="xml/api-index-full.xml"><xi:fallback /></xi:include>
</index>
+ <index id="api-index-0.2">
+ <title>Index of new API in 0.2</title>
+ <xi:include href="xml/api-index-0.2.xml"><xi:fallback /></xi:include>
+ </index>
+
<index id="deprecated-api-index" role="deprecated">
<title>Index of deprecated API</title>
<xi:include href="xml/api-index-deprecated.xml"><xi:fallback /></xi:include>
diff --git a/docs/reference/endless/endless-sections.txt b/docs/reference/endless/endless-sections.txt
index 0bab0b8..ed2e6bd 100644
--- a/docs/reference/endless/endless-sections.txt
+++ b/docs/reference/endless/endless-sections.txt
@@ -27,6 +27,8 @@ EOS_ENUM_VALUE
EosApplication
eos_application_new
eos_application_get_config_dir
+eos_application_get_image_attribution_file
+eos_application_set_image_attribution_file
<SUBSECTION Standard>
EosApplicationClass
EOS_APPLICATION
diff --git a/endless/Makefile.am b/endless/Makefile.am
index 414a029..2cdca90 100644
--- a/endless/Makefile.am
+++ b/endless/Makefile.am
@@ -37,6 +37,9 @@ endless_private_installed_headers = \
endless_library_sources = \
endless/eosapplication.c \
+ endless/eosattribution.c endless/eosattribution-private.h \
+ endless/eoscellrendererpixbuflink.c endless/eoscellrendererpixbuflink-private.h \
+ endless/eoscellrenderertextlink.c endless/eoscellrenderertextlink-private.h \
endless/eoscustomcontainer.c \
endless/eoshello.c \
endless/eosinit.c endless/eosinit-private.h \
diff --git a/endless/eosapplication.c b/endless/eosapplication.c
index 72016c1..510ff3b 100644
--- a/endless/eosapplication.c
+++ b/endless/eosapplication.c
@@ -2,12 +2,16 @@
#include "config.h"
#include "eosapplication.h"
+#include "eosattribution-private.h"
+#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>
#include "eoswindow.h"
#define CSS_THEME_URI "resource:///com/endlessm/sdk/css/endless-widgets.css"
+#define _CREDITS_DIALOG_DEFAULT_HEIGHT 450
+#define _CREDITS_DIALOG_DEFAULT_WIDTH 750
/**
* SECTION:application
@@ -46,12 +50,186 @@
* flags: 0 });
* app.run(ARGV);
* ]|
+ *
+ * You can specify attribution for images used in your application.
+ * This is important if you use images that require you to credit the original
+ * author, or Creative Commons-licenses.
+ * The attribution takes the form of a JSON file, an array of objects with the
+ * properties listed in <xref linkend="image-attribution-properties"/>.
+ * See #EosApplication:image-attribution-file.
+ *
+ * <table id="image-attribution-properties">
+ * <thead>
+ * <tr>
+ * <th align="left">Property</th>
+ * <th align="left">Required?</th>
+ * <th align="left">Type</th>
+ * <th align="left">Description</th>
+ * </tr>
+ * </thead>
+ * <tr>
+ * <td><code>resource_path</code></td>
+ * <td>Yes</td>
+ * <td>string</td>
+ * <td>
+ * Resource path to the image (e.g. <code>/com/example/...</code>)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>uri</code></td>
+ * <td>No</td>
+ * <td>string</td>
+ * <td>
+ * URI where the original image is to be found: e.g., a Flickr link.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>license</code></td>
+ * <td>*</td>
+ * <td>string</td>
+ * <td>
+ * Text identifying the license under which you are using this image.
+ * This field is not free-form; the allowed values are listed in <xref
+ * linkend="image-attribution-licenses"/>.
+ * If the license is not listed there, leave this field blank and clarify
+ * the license in the <code>comment</code> field.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>license_uri</code></td>
+ * <td>*</td>
+ * <td>string</td>
+ * <td>
+ * URI linking to the text of the image license.
+ * If you use the <code>license</code> field, this field may be
+ * automatically filled in, so you can leave it blank.
+ * If you do specify a value, then your value will override any automatic
+ * value.
+ * Note that you will then lose any localization from the automatic value;
+ * localization of this field is planned for later.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>credit</code></td>
+ * <td>*</td>
+ * <td>string</td>
+ * <td>
+ * The name or username of the author of the image.
+ * This is appropriate when the terms of use specify that the author is to
+ * be credited when the image is used.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>credit_contact</code></td>
+ * <td>No</td>
+ * <td>string</td>
+ * <td>
+ * URI at which the author can be contacted.
+ * (If this is an e-mail address, prefix it with <code>mailto:</code> so
+ * that it is a valid URI.)
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>copyright_holder</code></td>
+ * <td>*</td>
+ * <td>string</td>
+ * <td>
+ * Copyright holder of the image.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>copyright_year</code></td>
+ * <td>No</td>
+ * <td>integer</td>
+ * <td>
+ * Copyright year of the image.
+ * This will be displayed along with the copyright holder.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>permission</code></td>
+ * <td>No</td>
+ * <td>boolean</td>
+ * <td>
+ * Whether the image is used with permission.
+ * If this is specified, a string such as <quote>Used with
+ * permission</quote> may be displayed.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>comment</code></td>
+ * <td>No</td>
+ * <td>string</td>
+ * <td>
+ * Any other comments about the image license, terms of use, or source.
+ * </td>
+ * </tr>
+ * <tfoot>
+ * <tr>
+ * <td colspan="4">*At least one of these properties is required.</td>
+ * </tr>
+ * </tfoot>
+ * <caption>Allowed properties of the objects in the image attribution JSON
+ * file</caption>
+ * </table>
+ * <para></para>
+ * <table id="image-attribution-licenses">
+ * <thead>
+ * <tr>
+ * <th align="left">String</th>
+ * <th align="left">Description</th>
+ * </tr>
+ * </thead>
+ * <tr>
+ * <td>Public domain</td>
+ * <td>Public domain</td>
+ * </tr>
+ * <tr>
+ * <td>CC0 1.0</td>
+ * <td><ulink url="http://creativecommons.org/publicdomain/zero/1.0/">CC0
+ * 1.0 Universal (Public domain)</ulink></td>
+ * </tr>
+ * <tr>
+ * <td>CC BY 2.0</td>
+ * <td><ulink url="http://creativecommons.org/licenses/by/2.0/">Creative
+ * Commons Attribution 2.0</ulink></td>
+ * </tr>
+ * <tr>
+ * <td>CC BY 3.0</td>
+ * <td><ulink url="http://creativecommons.org/licenses/by/3.0/">Creative
+ * Commons Attribution 3.0</ulink></td>
+ * </tr>
+ * <tr>
+ * <td>CC BY-SA 2.0</td>
+ * <td><ulink url="http://creativecommons.org/licenses/by-sa/2.0/">Creative
+ * Commons Attribution-ShareAlike 2.0</ulink></td>
+ * </tr>
+ * <tr>
+ * <td>CC BY-SA 3.0</td>
+ * <td><ulink url="http://creativecommons.org/licenses/by-sa/3.0/">Creative
+ * Commons Attribution-ShareAlike 3.0</ulink></td>
+ * </tr>
+ * <tr>
+ * <td>CC BY-ND 2.0</td>
+ * <td><ulink url="http://creativecommons.org/licenses/by-nd/2.0/">Creative
+ * Commons Attribution-NoDerivs 2.0</ulink></td>
+ * </tr>
+ * <tr>
+ * <td>CC BY-ND 3.0</td>
+ * <td><ulink url="http://creativecommons.org/licenses/by-nd/3.0/">Creative
+ * Commons Attribution-NoDerivs 3.0</ulink></td>
+ * </tr>
+ * <caption>Allowed values for the <code>license</code> property in the image
+ * attribution JSON file</caption>
+ * </table>
*/
typedef struct {
GOnce init_config_dir_once;
GFile *config_dir;
+ GFile *image_attribution_file;
+
EosWindow *main_application_window;
} EosApplicationPrivate;
@@ -61,11 +239,65 @@ enum
{
PROP_0,
PROP_CONFIG_DIR,
+ PROP_IMAGE_ATTRIBUTION_FILE,
NPROPS
};
static GParamSpec *eos_application_props[NPROPS] = { NULL, };
+/* Signal handler for attribution widget requesting to show uri */
+static void
+on_attribution_show_uri (EosAttribution *attribution,
+ const gchar *uri,
+ EosApplication *self)
+{
+ GError *error = NULL;
+ if (!gtk_show_uri (NULL, uri, GDK_CURRENT_TIME, &error))
+ {
+ g_critical ("Error showing URI %s: %s", uri, error->message);
+ g_error_free (error);
+ }
+}
+
+/* Signal handler for app.image-credits::activate action */
+static void
+on_image_credits_activate (GSimpleAction *action,
+ GVariant *parameter,
+ gpointer data)
+{
+ EosApplication *self = EOS_APPLICATION (data);
+ EosApplicationPrivate *priv = eos_application_get_instance_private (self);
+ GtkWidget *dialog, *attribution, *content;
+ GError *error = NULL;
+
+ attribution = eos_attribution_new_sync (priv->image_attribution_file, NULL,
+ &error);
+ if (attribution == NULL)
+ {
+ g_warning ("Error loading image attribution file: %s", error->message);
+ return;
+ }
+ gtk_widget_set_hexpand (attribution, TRUE);
+ gtk_widget_set_vexpand (attribution, TRUE);
+ g_signal_connect (attribution, "show-uri",
+ G_CALLBACK (on_attribution_show_uri), self);
+ gtk_widget_show_all (attribution);
+
+ dialog = g_object_new (GTK_TYPE_DIALOG,
+ "default-height", _CREDITS_DIALOG_DEFAULT_HEIGHT,
+ "default-width", _CREDITS_DIALOG_DEFAULT_WIDTH,
+ "destroy-with-parent", TRUE,
+ "modal", TRUE,
+ "title", _("Image credits"),
+ "transient-for", GTK_WINDOW (priv->main_application_window),
+ "use-header-bar", TRUE,
+ NULL);
+ content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+ gtk_container_add (GTK_CONTAINER (content), attribution);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+}
+
static void
eos_application_get_property (GObject *object,
guint property_id,
@@ -80,6 +312,30 @@ eos_application_get_property (GObject *object,
g_value_set_object (value, eos_application_get_config_dir (self));
break;
+ case PROP_IMAGE_ATTRIBUTION_FILE:
+ g_value_set_object (value, eos_application_get_image_attribution_file (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+eos_application_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EosApplication *self = EOS_APPLICATION (object);
+
+ switch (property_id)
+ {
+ case PROP_IMAGE_ATTRIBUTION_FILE:
+ eos_application_set_image_attribution_file (self,
+ g_value_get_object (value));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
@@ -91,6 +347,7 @@ eos_application_finalize (GObject *object)
EosApplication *self = EOS_APPLICATION (object);
EosApplicationPrivate *priv = eos_application_get_instance_private (self);
g_clear_object (&priv->config_dir);
+ g_clear_object (&priv->image_attribution_file);
G_OBJECT_CLASS (eos_application_parent_class)->finalize (object);
}
@@ -172,6 +429,12 @@ eos_application_startup (GApplication *application)
{
G_APPLICATION_CLASS (eos_application_parent_class)->startup (application);
+ /* Set up the hotkey for the image credit dialog */
+ static const gchar * const accelerators[] = { "<Primary><Shift>a", NULL };
+ gtk_application_set_accels_for_action (GTK_APPLICATION (application),
+ "app.image-credits",
+ accelerators);
+
GtkCssProvider *provider = gtk_css_provider_new ();
/* Reset CSS for SDK applications and apply our own theme on top of it. This
@@ -265,6 +528,7 @@ eos_application_class_init (EosApplicationClass *klass)
GtkApplicationClass *gtk_application_class = GTK_APPLICATION_CLASS (klass);
object_class->get_property = eos_application_get_property;
+ object_class->set_property = eos_application_set_property;
object_class->finalize = eos_application_finalize;
g_application_class->activate = eos_application_activate;
g_application_class->startup = eos_application_startup;
@@ -285,16 +549,92 @@ eos_application_class_init (EosApplicationClass *klass)
"User configuration directory for this application",
G_TYPE_FILE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+ /**
+ * EosApplication:image-attribution-file:
+ *
+ * A #GFile handle to a file for storing attribution information for the
+ * images included in this application's resource file.
+ *
+ * This attribution file must be a JSON file.
+ * Here is an example of the required format:
+ * |[
+ * [
+ * {
+ * "resource_path": "/com/example/smokegrinder/image1.jpg",
+ * "license": "Public domain",
+ * "uri": "http://www.photos.com/photos/12345",
+ * "comment": "No known copyright restrictions"
+ * },
+ * {
+ * "resource_path": "/com/example/smokegrinder/image2.jpg",
+ * "license_uri": "http://example.com/image-license",
+ * "uri": "http://www.photos.com/photos/54321",
+ * "credit": "Edward X. Ample",
+ * "credit_contact": "http://www.photos.com/users/example"
+ * },
+ * {
+ * "resource_path": "/com/example/smokegrinder/image3.jpg",
+ * "copyright_holder": "Jane Q. Hacker",
+ * "copyright_year": 2014,
+ * "permission": true
+ * }
+ * ]
+ * ]|
+ *
+ * The JSON object is an array of objects that each contain information about
+ * one image.
+ * The only required property is <code>resource_path</code>, which is the path
+ * to the image in the resource file.
+ *
+ * The recognized properties are shown in <xref
+ * linkend="image-attribution-properties"/>.
+ *
+ * Nothing is guaranteed about how the application uses this information.
+ * It can display it to the user or make it available to other programs.
+ *
+ * <note><para>
+ * Currently, pressing <keycombo><keycap>Control</keycap>
+ * <keycap>Shift</keycap><keycap>A</keycap></keycombo> brings up a credits
+ * dialog.
+ * This is liable to change in future versions.
+ * </para></note>
+ *
+ * Since: 0.2
+ */
+ eos_application_props[PROP_IMAGE_ATTRIBUTION_FILE] =
+ g_param_spec_object ("image-attribution-file", "Image attribution file",
+ "File with attribution information for images in this application",
+ G_TYPE_FILE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, NPROPS,
eos_application_props);
}
static void
+set_image_credits_action_enabled (EosApplication *self,
+ gboolean enabled)
+{
+ GAction *action = g_action_map_lookup_action (G_ACTION_MAP (self),
+ "image-credits");
+ g_simple_action_set_enabled (G_SIMPLE_ACTION (action), enabled);
+ /* action map owns action */
+}
+
+static void
eos_application_init (EosApplication *self)
{
EosApplicationPrivate *priv = eos_application_get_instance_private (self);
priv->init_config_dir_once = (GOnce)G_ONCE_INIT;
+
+ /* Set up app actions */
+ static const GActionEntry actions[] = {
+ { "image-credits", on_image_credits_activate },
+ };
+ g_action_map_add_action_entries (G_ACTION_MAP (self), actions,
+ G_N_ELEMENTS (actions), self);
+ set_image_credits_action_enabled (self, FALSE);
+
g_signal_connect (self, "notify::application-id",
G_CALLBACK (on_app_id_set), self);
}
@@ -353,3 +693,67 @@ eos_application_get_config_dir (EosApplication *self)
(GThreadFunc)ensure_config_dir_exists_and_is_writable, self);
return priv->config_dir;
}
+
+/**
+ * eos_application_get_image_attribution_file:
+ * @self: the application
+ *
+ * Gets a #GFile pointing to a JSON file containing credits for images included
+ * in the app's resources.
+ * See #EosApplication:image-attribution-file.
+ *
+ * Returns: (transfer none) (allow-none): A #GFile pointing to the image
+ * attribution file, or %NULL if one has not been set.
+ *
+ * Since: 0.2
+ */
+GFile *
+eos_application_get_image_attribution_file (EosApplication *self)
+{
+ g_return_val_if_fail (self != NULL && EOS_IS_APPLICATION (self), NULL);
+
+ EosApplicationPrivate *priv = eos_application_get_instance_private (self);
+ return priv->image_attribution_file;
+}
+
+/**
+ * eos_application_set_image_attribution_file:
+ * @self: the application
+ * @file: (allow-none): a #GFile pointing to a file in the proper format, or
+ * %NULL to unset.
+ *
+ * You can provide attribution and credit for images included in the application
+ * by giving this function a JSON file with image credits.
+ * See #EosApplication:image-attribution-file for the JSON file's required
+ * format.
+ *
+ * Since: 0.2
+ */
+void
+eos_application_set_image_attribution_file (EosApplication *self,
+ GFile *file)
+{
+ g_return_if_fail (self != NULL && EOS_IS_APPLICATION (self));
+ g_return_if_fail (file == NULL || G_IS_FILE (file));
+
+ EosApplicationPrivate *priv = eos_application_get_instance_private (self);
+
+ if (priv->image_attribution_file == file ||
+ (priv->image_attribution_file != NULL && file != NULL &&
+ g_file_equal (file, priv->image_attribution_file)))
+ return;
+
+ if (priv->image_attribution_file == NULL || file == NULL)
+ {
+ gboolean enabled = (file != NULL);
+ set_image_credits_action_enabled (self, enabled);
+ }
+
+ g_clear_object (&priv->image_attribution_file);
+ if (file != NULL)
+ g_object_ref (file);
+ priv->image_attribution_file = file;
+
+ g_object_notify_by_pspec (G_OBJECT (self),
+ eos_application_props[PROP_IMAGE_ATTRIBUTION_FILE]);
+}
diff --git a/endless/eosapplication.h b/endless/eosapplication.h
index 67d4194..e74a3eb 100644
--- a/endless/eosapplication.h
+++ b/endless/eosapplication.h
@@ -65,6 +65,13 @@ EosApplication *eos_application_new (const gchar *application_id
EOS_SDK_AVAILABLE_IN_0_0
GFile *eos_application_get_config_dir (EosApplication *self);
+EOS_SDK_AVAILABLE_IN_0_2
+GFile *eos_application_get_image_attribution_file (EosApplication *self);
+
+EOS_SDK_AVAILABLE_IN_0_2
+void eos_application_set_image_attribution_file (EosApplication *self,
+ GFile *file);
+
G_END_DECLS
#endif /* EOS_APPLICATION_H */
diff --git a/endless/eosattribution-private.h b/endless/eosattribution-private.h
new file mode 100644
index 0000000..d23066f
--- /dev/null
+++ b/endless/eosattribution-private.h
@@ -0,0 +1,57 @@
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#ifndef EOS_ATTRIBUTION_H
+#define EOS_ATTRIBUTION_H
+
+#include "eostypes.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EOS_TYPE_ATTRIBUTION eos_attribution_get_type()
+
+#define EOS_ATTRIBUTION(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+ EOS_TYPE_ATTRIBUTION, EosAttribution))
+
+#define EOS_ATTRIBUTION_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST ((klass), \
+ EOS_TYPE_ATTRIBUTION, EosAttributionClass))
+
+#define EOS_IS_ATTRIBUTION(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+ EOS_TYPE_ATTRIBUTION))
+
+#define EOS_IS_ATTRIBUTION_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+ EOS_TYPE_ATTRIBUTION))
+
+#define EOS_ATTRIBUTION_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+ EOS_TYPE_ATTRIBUTION, EosAttributionClass))
+
+typedef struct _EosAttribution EosAttribution;
+typedef struct _EosAttributionClass EosAttributionClass;
+
+struct _EosAttribution
+{
+ GtkScrolledWindow parent;
+};
+
+struct _EosAttributionClass
+{
+ GtkScrolledWindowClass parent_class;
+};
+
+GType eos_attribution_get_type (void) G_GNUC_CONST;
+
+GtkWidget *eos_attribution_new_sync (GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+
+GFile * eos_attribution_get_file (EosAttribution *self);
+
+G_END_DECLS
+
+#endif /* EOS_ATTRIBUTION_H */
diff --git a/endless/eosattribution.c b/endless/eosattribution.c
new file mode 100644
index 0000000..474f4e9
--- /dev/null
+++ b/endless/eosattribution.c
@@ -0,0 +1,683 @@
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+#include <json-glib/json-glib.h>
+
+#include "eosattribution-private.h"
+#include "eoscellrendererpixbuflink-private.h"
+#include "eoscellrenderertextlink-private.h"
+
+typedef struct
+{
+ GFile *file;
+ GtkWidget *view;
+ GtkListStore *model;
+} EosAttributionPrivate;
+
+/* Forward declarations */
+static void eos_attribution_init_initable (GInitableIface *, GInterfaceInfo *);
+
+G_DEFINE_TYPE_WITH_CODE (EosAttribution, eos_attribution,
+ GTK_TYPE_SCROLLED_WINDOW,
+ G_ADD_PRIVATE (EosAttribution)
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+ eos_attribution_init_initable))
+
+#define ROW_HEIGHT 50 /* Height of the pixbufs in each row */
+
+enum {
+ SHOW_URI,
+ LAST_SIGNAL
+};
+
+static guint attribution_signals[LAST_SIGNAL] = { 0 };
+
+enum
+{
+ PROP_0,
+ PROP_FILE,
+ NPROPS
+};
+
+static GParamSpec *eos_attribution_props[NPROPS] = { NULL, };
+
+/* These are the recognized string values for the "license" field. Any other
+license must be clarified in the comments, or linked to with the "license_uri"
+field. Make sure to add new values to the table "image-attribution-licenses" in
+the documentation of EosApplication and to the two arrays below this one. */
+static gchar * const recognized_licenses[] = {
+ "Public domain",
+ "CC0 1.0",
+ "CC BY 2.0",
+ "CC BY 3.0",
+ "CC BY-SA 2.0",
+ "CC BY-SA 3.0",
+ "CC BY-ND 2.0",
+ "CC BY-ND 3.0",
+ NULL
+};
+
+static gchar * const license_display_names[] = {
+ /* TRANSLATORS: These names should be translated as the official names of the
+ licenses in your language. */
+ N_("Public domain"),
+ N_("CC0 1.0 (Public domain)"),
+ N_("Creative Commons Attribution 2.0"),
+ N_("Creative Commons Attribution 3.0"),
+ N_("Creative Commons Attribution-ShareAlike 2.0"),
+ N_("Creative Commons Attribution-ShareAlike 3.0"),
+ N_("Creative Commons Attribution-NoDerivs 2.0"),
+ N_("Creative Commons Attribution-NoDerivs 3.0"),
+ NULL
+};
+
+static gchar * const license_uris[] = {
+ NULL,
+ /* TRANSLATORS: These URIs should be translated as a link to the license page
+ in your language. For example, for Spanish,
+ "http://creativecommons.org/licenses/by/3.0/" should become
+ "http://creativecommons.org/licenses/by/3.0/deed.es". However, if no such page
+ exists, then leave these untranslated. */
+ N_("http://creativecommons.org/publicdomain/zero/1.0/"),
+ N_("http://creativecommons.org/licenses/by/2.0/"),
+ N_("http://creativecommons.org/licenses/by/3.0/"),
+ N_("http://creativecommons.org/licenses/by-sa/2.0/"),
+ N_("http://creativecommons.org/licenses/by-sa/3.0/"),
+ N_("http://creativecommons.org/licenses/by-nd/2.0/"),
+ N_("http://creativecommons.org/licenses/by-nd/3.0/"),
+ NULL
+};
+
+enum
+{
+ LICENSE_PUBLIC_DOMAIN,
+ LICENSE_CC0,
+ LICENSE_CC_BY_2_0,
+ LICENSE_CC_BY_3_0,
+ LICENSE_CC_BY_SA_2_0,
+ LICENSE_CC_BY_SA_3_0,
+ LICENSE_GFDL_1_2,
+ LICENSE_GFDL_1_3,
+ NUM_RECOGNIZED_LICENSES
+};
+
+enum
+{
+ COLUMN_PIXBUF,
+ COLUMN_ORIGINAL_URI,
+ COLUMN_LICENSE,
+ COLUMN_LICENSE_URI,
+ COLUMN_CREDIT,
+ COLUMN_CREDIT_CONTACT,
+ COLUMN_COPYRIGHT_HOLDER,
+ COLUMN_COPYRIGHT_YEAR,
+ COLUMN_PERMISSION,
+ COLUMN_COMMENT,
+ NUM_MODEL_COLUMNS
+};
+
+static void
+eos_attribution_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EosAttribution *self = EOS_ATTRIBUTION (object);
+
+ switch (property_id)
+ {
+ case PROP_FILE:
+ g_value_set_object (value, eos_attribution_get_file (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+eos_attribution_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EosAttribution *self = EOS_ATTRIBUTION (object);
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+
+ switch (property_id)
+ {
+ case PROP_FILE:
+ priv->file = g_value_dup_object (value); /* construct only */
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+eos_attribution_finalize (GObject *object)
+{
+ EosAttribution *self = EOS_ATTRIBUTION (object);
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+
+ g_object_unref (priv->model);
+
+ G_OBJECT_CLASS (eos_attribution_parent_class)->finalize (object);
+}
+
+static void
+eos_attribution_class_init (EosAttributionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = eos_attribution_get_property;
+ object_class->set_property = eos_attribution_set_property;
+ object_class->finalize = eos_attribution_finalize;
+
+ attribution_signals[SHOW_URI] =
+ g_signal_new ("show-uri", EOS_TYPE_ATTRIBUTION,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE,
+ 1, G_TYPE_STRING);
+
+ eos_attribution_props[PROP_FILE] =
+ g_param_spec_object ("file", "File",
+ "JSON file with attribution information for images",
+ G_TYPE_FILE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, NPROPS,
+ eos_attribution_props);
+}
+
+static void
+on_pixbuf_cell_clicked (GtkCellRenderer *renderer,
+ const gchar *path,
+ EosAttribution *self)
+{
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+ GtkTreeIter iter;
+ gchar *original_uri;
+ GtkTreeModel *model = GTK_TREE_MODEL (priv->model);
+
+ if (!gtk_tree_model_get_iter_from_string (model, &iter, path))
+ {
+ g_warning ("Apparently someone clicked on a nonexistent cell renderer.");
+ return;
+ }
+ gtk_tree_model_get (model, &iter,
+ COLUMN_ORIGINAL_URI, &original_uri,
+ -1);
+ if (original_uri != NULL)
+ g_signal_emit (self, attribution_signals[SHOW_URI], 0, original_uri);
+ g_free (original_uri);
+}
+
+static void
+on_license_cell_clicked (GtkCellRenderer *renderer,
+ const gchar *path,
+ EosAttribution *self)
+{
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+ GtkTreeIter iter;
+ gchar *license_uri;
+ GtkTreeModel *model = GTK_TREE_MODEL (priv->model);
+
+ if (!gtk_tree_model_get_iter_from_string (model, &iter, path))
+ {
+ g_warning ("Apparently someone clicked on a nonexistent cell renderer.");
+ return;
+ }
+ gtk_tree_model_get (model, &iter,
+ COLUMN_LICENSE_URI, &license_uri,
+ -1);
+ if (license_uri != NULL)
+ g_signal_emit (self, attribution_signals[SHOW_URI], 0, license_uri);
+ g_free (license_uri);
+}
+
+static void
+on_contact_cell_clicked (GtkCellRenderer *renderer,
+ const gchar *path,
+ EosAttribution *self)
+{
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+ GtkTreeIter iter;
+ gchar *contact;
+ GtkTreeModel *model = GTK_TREE_MODEL (priv->model);
+
+ if (!gtk_tree_model_get_iter_from_string (model, &iter, path))
+ {
+ g_warning ("Apparently someone clicked on a nonexistent cell renderer.");
+ return;
+ }
+ gtk_tree_model_get (model, &iter,
+ COLUMN_CREDIT_CONTACT, &contact,
+ -1);
+ if (contact != NULL)
+ g_signal_emit (self, attribution_signals[SHOW_URI], 0, contact);
+ g_free (contact);
+}
+
+static void
+render_license_link (GtkTreeViewColumn *column,
+ GtkCellRenderer *renderer,
+ GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ gchar *license_uri;
+ gint license_index;
+ gtk_tree_model_get (model, iter,
+ COLUMN_LICENSE, &license_index,
+ COLUMN_LICENSE_URI, &license_uri,
+ -1);
+ if (license_index != -1)
+ {
+ /* TRANSLATORS: %s will be replaced with the name of an image license,
+ such as "Public domain" or "Creative Commons Attribution". These names are
+ translated elsewhere in this file. Make sure %s is still in the translated
+ string. */
+ gchar *license_string = g_strdup_printf (_("%s."),
+ gettext (license_display_names[license_index]));
+ gboolean behave_like_link = (license_uri != NULL);
+ g_object_set (renderer,
+ "markup", license_string,
+ "visible", TRUE,
+ "foreground-set", behave_like_link,
+ NULL);
+ g_free (license_string);
+ }
+ else if (license_uri != NULL)
+ {
+ g_object_set (renderer,
+ "markup", _("Click for image license."),
+ "visible", TRUE,
+ NULL);
+ }
+ else
+ {
+ g_object_set (renderer, "visible", FALSE, NULL);
+ }
+
+ g_free (license_uri);
+}
+
+static void
+render_contact_link (GtkTreeViewColumn *column,
+ GtkCellRenderer *renderer,
+ GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ gchar *credit, *credit_contact;
+ gtk_tree_model_get (model, iter,
+ COLUMN_CREDIT, &credit,
+ COLUMN_CREDIT_CONTACT, &credit_contact,
+ -1);
+ if (credit != NULL)
+ {
+ /* TRANSLATORS: %s will be replaced with the name or account name of the
+ person that the image should be credited to. Make sure %s is still in the
+ translated string. */
+ gchar *credit_string = g_strdup_printf (_("Image credit: %s."), credit);
+ g_object_set (renderer,
+ "markup", credit_string,
+ "visible", TRUE,
+ "foreground-set", FALSE,
+ NULL);
+ g_free (credit_string);
+ }
+ else
+ {
+ g_object_set (renderer, "visible", FALSE, NULL);
+ }
+
+ g_free (credit);
+ g_free (credit_contact);
+}
+
+static void
+render_usage_notes (GtkTreeViewColumn *column,
+ GtkCellRenderer *renderer,
+ GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ gchar *copyright_holder, *comment;
+ gint copyright_year;
+ gboolean permission;
+ GString *builder = g_string_new ("");
+
+ gtk_tree_model_get (model, iter,
+ COLUMN_COPYRIGHT_HOLDER, &copyright_holder,
+ COLUMN_COPYRIGHT_YEAR, &copyright_year,
+ COLUMN_PERMISSION, &permission,
+ COLUMN_COMMENT, &comment,
+ -1);
+ if (copyright_holder != NULL)
+ {
+ if (copyright_year != -1)
+ {
+ /* TRANSLATORS: %d will be replaced with the copyright year, %s with
+ the copyright holder. Make sure these tokens are in the translated
+ string. */
+ g_string_append_printf (builder, _("Copyright %d %s."),
+ copyright_year, copyright_holder);
+ }
+ else
+ {
+ /* TRANSLATORS: %s will be replaced with the name of the copyright
+ holder. Make sure %s is still in the translated string. */
+ g_string_append_printf (builder, _("Copyright %s."),
+ copyright_holder);
+ }
+ if (permission || comment != NULL)
+ g_string_append_c (builder, ' ');
+ }
+ if (permission)
+ {
+ g_string_append (builder, _("Used with permission."));
+ if (comment != NULL)
+ g_string_append_c (builder, ' ');
+ }
+ if (comment != NULL)
+ g_string_append (builder, comment);
+
+ g_free (copyright_holder);
+ g_free (comment);
+
+ gchar *resulting_text = g_string_free (builder, FALSE);
+ g_object_set (renderer, "markup", resulting_text, NULL);
+ g_free (resulting_text);
+}
+
+static void
+eos_attribution_init (EosAttribution *self)
+{
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+ priv->model = gtk_list_store_new (NUM_MODEL_COLUMNS,
+ GDK_TYPE_PIXBUF,
+ G_TYPE_STRING,
+ G_TYPE_INT,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_INT,
+ G_TYPE_BOOLEAN,
+ G_TYPE_STRING);
+ priv->view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (priv->model));
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (priv->view), FALSE);
+
+ GtkTreeViewColumn *column;
+ GtkCellRenderer *renderer;
+
+ /* Column showing a reduced representation of the image */
+ renderer = eos_cell_renderer_pixbuf_link_new ();
+ g_signal_connect (renderer, "clicked",
+ G_CALLBACK (on_pixbuf_cell_clicked), self);
+ column = gtk_tree_view_column_new_with_attributes ("", renderer,
+ "pixbuf", COLUMN_PIXBUF,
+ NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (priv->view), column);
+
+ GtkCellArea *area = gtk_cell_area_box_new ();
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (area),
+ GTK_ORIENTATION_HORIZONTAL);
+
+ /* Put all the text renderers in the same column */
+ column = gtk_tree_view_column_new_with_area (area);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (priv->view), column);
+
+ /* Renderer for license link */
+ renderer = eos_cell_renderer_text_link_new ();
+ g_signal_connect (renderer, "clicked",
+ G_CALLBACK (on_license_cell_clicked), self);
+ gtk_cell_area_box_pack_start (GTK_CELL_AREA_BOX (area), renderer,
+ FALSE, FALSE, FALSE);
+ gtk_tree_view_column_set_cell_data_func (column, renderer,
+ (GtkTreeCellDataFunc)render_license_link,
+ NULL, NULL);
+
+ /* Renderer for original image link */
+ renderer = eos_cell_renderer_text_link_new ();
+ g_signal_connect (renderer, "clicked",
+ G_CALLBACK (on_contact_cell_clicked), self);
+ gtk_cell_area_box_pack_start (GTK_CELL_AREA_BOX (area), renderer,
+ FALSE, FALSE, FALSE);
+ gtk_tree_view_column_set_cell_data_func (column, renderer,
+ (GtkTreeCellDataFunc)render_contact_link,
+ NULL, NULL);
+
+ /* Renderer for general notes */
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_area_box_pack_start (GTK_CELL_AREA_BOX (area), renderer,
+ FALSE, FALSE, FALSE);
+ gtk_tree_view_column_set_cell_data_func (column, renderer,
+ (GtkTreeCellDataFunc)render_usage_notes,
+ NULL, NULL);
+
+ gtk_container_add (GTK_CONTAINER (self), priv->view);
+}
+
+/* Utility function, returns the index if an array of strings @strv terminated
+by NULL contains the string @entry, -1 if it does not */
+static gint strv_index (gchar * const *, const gchar *) G_GNUC_PURE;
+static gint
+strv_index (gchar * const *strv, const gchar *entry)
+{
+ if (strv == NULL)
+ return -1;
+
+ gchar * const *iter;
+ gint index;
+ for (iter = strv, index = 0; *iter != NULL; iter++, index++)
+ {
+ if (strcmp (*iter, entry) == 0)
+ return index;
+ }
+ return -1;
+}
+
+static gboolean
+eos_attribution_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EosAttribution *self = EOS_ATTRIBUTION (initable);
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+
+ GInputStream *stream = G_INPUT_STREAM (g_file_read (priv->file, cancellable,
+ error));
+ if (stream == NULL)
+ return FALSE;
+
+ JsonParser *parser = json_parser_new ();
+ gboolean success = json_parser_load_from_stream (parser, stream, cancellable,
+ error);
+ if (!success)
+ {
+ g_object_unref (stream);
+ goto fail;
+ }
+
+ success = g_input_stream_close (stream, cancellable, error);
+ g_object_unref (stream);
+ if (!success && g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ goto fail;
+ /* Ignore errors other than cancellation */
+
+ JsonReader *reader = json_reader_new (json_parser_get_root (parser));
+ gint num_images = json_reader_count_elements (reader);
+ if (num_images == -1)
+ {
+ *error = g_error_copy (json_reader_get_error (reader));
+ goto fail2;
+ }
+ gint count;
+ for (count = 0; count < num_images; count++)
+ {
+ /* Required info */
+ const gchar *resource_path;
+ /* Optional info; some combinations of these are required though */
+ const gchar *original_uri = NULL, *license = NULL, *license_uri = NULL,
+ *credit = NULL, *credit_contact = NULL, *copyright_holder = NULL,
+ *comment = NULL;
+ gint64 copyright_year = -1;
+ gboolean permission = FALSE;
+
+ if (!json_reader_read_element (reader, count))
+ {
+ g_warning ("Could not read element %d of attribution file", count);
+ json_reader_end_element (reader);
+ continue;
+ }
+
+ if (!json_reader_is_object (reader))
+ {
+ g_warning ("Expected element %d in attribution file to be a dict",
+ count);
+ json_reader_end_element (reader);
+ continue;
+ }
+
+ /* "resource_path" is required */
+ if (!json_reader_read_member (reader, "resource_path"))
+ {
+ g_warning ("Element %d in attribution file must contain a 'resource_path'",
+ count);
+ json_reader_end_member (reader);
+ json_reader_end_element (reader);
+ continue;
+ }
+ resource_path = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ /* Read all optional elements */
+
+ gchar **members = json_reader_list_members (reader);
+
+#define READ_MEMBER_IF_PRESENT(member_name, read_func, storage) \
+ if (strv_index (members, member_name) != -1) \
+ { \
+ json_reader_read_member (reader, member_name); \
+ storage = json_reader_get_##read_func (reader); \
+ json_reader_end_member (reader); \
+ }
+
+ READ_MEMBER_IF_PRESENT("uri", string_value, original_uri)
+ READ_MEMBER_IF_PRESENT("license", string_value, license)
+ READ_MEMBER_IF_PRESENT("license_uri", string_value, license_uri)
+ READ_MEMBER_IF_PRESENT("credit", string_value, credit)
+ READ_MEMBER_IF_PRESENT("credit_contact", string_value, credit_contact)
+ READ_MEMBER_IF_PRESENT("copyright_holder", string_value, copyright_holder)
+ READ_MEMBER_IF_PRESENT("comment", string_value, comment)
+ READ_MEMBER_IF_PRESENT("copyright_year", int_value, copyright_year)
+ READ_MEMBER_IF_PRESENT("permission", boolean_value, permission);
+
+#undef READ_MEMBER_IF_PRESENT
+
+ g_strfreev (members);
+ json_reader_end_element (reader);
+
+ /* Validate the data */
+
+ if (license == NULL && license_uri == NULL && credit == NULL &&
+ copyright_holder == NULL)
+ {
+ g_warning ("Image %s must have at least one of the following "
+ "specified: license, license_uri, credit, or "
+ "copyright_holder.", resource_path);
+ continue;
+ }
+ gint license_index = -1;
+ if (license != NULL)
+ {
+ license_index = strv_index (recognized_licenses, license);
+ if (license_index == -1)
+ {
+ g_warning ("License string for image %s not recognized: %s",
+ resource_path, license);
+ continue;
+ }
+ if (license_uri == NULL)
+ license_uri = license_uris[license_index];
+ }
+
+ /* Populate a row of the model */
+
+ GError *inner_error = NULL;
+ GdkPixbuf *pixbuf = gdk_pixbuf_new_from_resource_at_scale (resource_path,
+ -1, ROW_HEIGHT,
+ TRUE,
+ &inner_error);
+ if (pixbuf == NULL)
+ {
+ g_warning ("Not able to load pixbuf from '%s': %s", resource_path,
+ inner_error->message);
+ g_clear_error (&inner_error);
+ continue;
+ }
+
+ GtkTreeIter new_row;
+ gtk_list_store_append (priv->model, &new_row);
+ gtk_list_store_set (priv->model, &new_row,
+ COLUMN_PIXBUF, pixbuf,
+ COLUMN_ORIGINAL_URI, original_uri,
+ COLUMN_LICENSE, license_index,
+ COLUMN_LICENSE_URI, license_uri,
+ COLUMN_CREDIT, credit,
+ COLUMN_CREDIT_CONTACT, credit_contact,
+ COLUMN_COPYRIGHT_HOLDER, copyright_holder,
+ COLUMN_COPYRIGHT_YEAR, (gint) copyright_year,
+ COLUMN_PERMISSION, permission,
+ COLUMN_COMMENT, comment,
+ -1);
+ g_object_unref (pixbuf); /* List store now holds the reference */
+ }
+
+ g_object_unref (reader);
+ g_object_unref (parser);
+ return TRUE;
+
+fail2:
+ g_object_unref (reader);
+fail:
+ g_object_unref (parser);
+ return FALSE;
+}
+
+static void
+eos_attribution_init_initable (GInitableIface *iface,
+ GInterfaceInfo *info)
+{
+ iface->init = eos_attribution_initable_init;
+}
+
+GtkWidget *
+eos_attribution_new_sync (GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (G_IS_FILE (file), NULL);
+ g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable),
+ NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ return GTK_WIDGET (g_initable_new (EOS_TYPE_ATTRIBUTION, cancellable, error,
+ "file", file,
+ NULL));
+}
+
+GFile *
+eos_attribution_get_file (EosAttribution *self)
+{
+ g_return_val_if_fail (EOS_IS_ATTRIBUTION (self), NULL);
+
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+ return priv->file;
+}
diff --git a/endless/eoscellrendererpixbuflink-private.h b/endless/eoscellrendererpixbuflink-private.h
new file mode 100644
index 0000000..076098d
--- /dev/null
+++ b/endless/eoscellrendererpixbuflink-private.h
@@ -0,0 +1,52 @@
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#ifndef EOS_CELL_RENDERER_PIXBUF_LINK_H
+#define EOS_CELL_RENDERER_PIXBUF_LINK_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EOS_TYPE_CELL_RENDERER_PIXBUF_LINK eos_cell_renderer_pixbuf_link_get_type()
+
+#define EOS_CELL_RENDERER_PIXBUF_LINK(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+ EOS_TYPE_CELL_RENDERER_PIXBUF_LINK, EosCellRendererPixbufLink))
+
+#define EOS_CELL_RENDERER_PIXBUF_LINK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST ((klass), \
+ EOS_TYPE_CELL_RENDERER_PIXBUF_LINK, EosCellRendererPixbufLinkClass))
+
+#define EOS_IS_CELL_RENDERER_PIXBUF_LINK(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+ EOS_TYPE_CELL_RENDERER_PIXBUF_LINK))
+
+#define EOS_IS_CELL_RENDERER_PIXBUF_LINK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+ EOS_TYPE_CELL_RENDERER_PIXBUF_LINK))
+
+#define EOS_CELL_RENDERER_PIXBUF_LINK_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+ EOS_TYPE_CELL_RENDERER_PIXBUF_LINK, EosCellRendererPixbufLinkClass))
+
+typedef struct _EosCellRendererPixbufLink EosCellRendererPixbufLink;
+typedef struct _EosCellRendererPixbufLinkClass EosCellRendererPixbufLinkClass;
+
+struct _EosCellRendererPixbufLink
+{
+ GtkCellRendererPixbuf parent;
+};
+
+struct _EosCellRendererPixbufLinkClass
+{
+ GtkCellRendererPixbufClass parent_class;
+};
+
+GType eos_cell_renderer_pixbuf_link_get_type (void) G_GNUC_CONST;
+
+GtkCellRenderer *eos_cell_renderer_pixbuf_link_new (void);
+
+G_END_DECLS
+
+#endif /* EOS_CELL_RENDERER_PIXBUF_LINK_H */
diff --git a/endless/eoscellrendererpixbuflink.c b/endless/eoscellrendererpixbuflink.c
new file mode 100644
index 0000000..c3af966
--- /dev/null
+++ b/endless/eoscellrendererpixbuflink.c
@@ -0,0 +1,59 @@
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include <gtk/gtk.h>
+
+#include "eoscellrendererpixbuflink-private.h"
+
+G_DEFINE_TYPE (EosCellRendererPixbufLink, eos_cell_renderer_pixbuf_link, GTK_TYPE_CELL_RENDERER_PIXBUF)
+
+enum {
+ CLICKED,
+ LAST_SIGNAL
+};
+
+static guint pixbuf_link_signals[LAST_SIGNAL] = { 0 };
+
+static gboolean
+eos_cell_renderer_pixbuf_link_activate (GtkCellRenderer *renderer,
+ GdkEvent *event,
+ GtkWidget *widget,
+ const gchar *path,
+ const GdkRectangle *background_area,
+ const GdkRectangle *cell_area,
+ GtkCellRendererState flags)
+{
+ g_signal_emit (renderer, pixbuf_link_signals[CLICKED], 0, path);
+ return TRUE;
+}
+
+static void
+eos_cell_renderer_pixbuf_link_class_init (EosCellRendererPixbufLinkClass *klass)
+{
+ GtkCellRendererClass *renderer_class = GTK_CELL_RENDERER_CLASS (klass);
+
+ renderer_class->activate = eos_cell_renderer_pixbuf_link_activate;
+
+ pixbuf_link_signals[CLICKED] =
+ g_signal_new ("clicked", EOS_TYPE_CELL_RENDERER_PIXBUF_LINK,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE,
+ 1, G_TYPE_STRING);
+}
+
+static void
+eos_cell_renderer_pixbuf_link_init (EosCellRendererPixbufLink *self)
+{
+ g_object_set (self,
+ "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE,
+ NULL);
+}
+
+GtkCellRenderer *
+eos_cell_renderer_pixbuf_link_new (void)
+{
+ return GTK_CELL_RENDERER (g_object_new (EOS_TYPE_CELL_RENDERER_PIXBUF_LINK,
+ NULL));
+}
diff --git a/endless/eoscellrenderertextlink-private.h b/endless/eoscellrenderertextlink-private.h
new file mode 100644
index 0000000..09fc665
--- /dev/null
+++ b/endless/eoscellrenderertextlink-private.h
@@ -0,0 +1,52 @@
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#ifndef EOS_CELL_RENDERER_TEXT_LINK_H
+#define EOS_CELL_RENDERER_TEXT_LINK_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EOS_TYPE_CELL_RENDERER_TEXT_LINK eos_cell_renderer_text_link_get_type()
+
+#define EOS_CELL_RENDERER_TEXT_LINK(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+ EOS_TYPE_CELL_RENDERER_TEXT_LINK, EosCellRendererTextLink))
+
+#define EOS_CELL_RENDERER_TEXT_LINK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST ((klass), \
+ EOS_TYPE_CELL_RENDERER_TEXT_LINK, EosCellRendererTextLinkClass))
+
+#define EOS_IS_CELL_RENDERER_TEXT_LINK(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+ EOS_TYPE_CELL_RENDERER_TEXT_LINK))
+
+#define EOS_IS_CELL_RENDERER_TEXT_LINK_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+ EOS_TYPE_CELL_RENDERER_TEXT_LINK))
+
+#define EOS_CELL_RENDERER_TEXT_LINK_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+ EOS_TYPE_CELL_RENDERER_TEXT_LINK, EosCellRendererTextLinkClass))
+
+typedef struct _EosCellRendererTextLink EosCellRendererTextLink;
+typedef struct _EosCellRendererTextLinkClass EosCellRendererTextLinkClass;
+
+struct _EosCellRendererTextLink
+{
+ GtkCellRendererText parent;
+};
+
+struct _EosCellRendererTextLinkClass
+{
+ GtkCellRendererTextClass parent_class;
+};
+
+GType eos_cell_renderer_text_link_get_type (void) G_GNUC_CONST;
+
+GtkCellRenderer *eos_cell_renderer_text_link_new (void);
+
+G_END_DECLS
+
+#endif /* EOS_CELL_RENDERER_TEXT_LINK_H */
diff --git a/endless/eoscellrenderertextlink.c b/endless/eoscellrenderertextlink.c
new file mode 100644
index 0000000..0a783bf
--- /dev/null
+++ b/endless/eoscellrenderertextlink.c
@@ -0,0 +1,85 @@
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include <gtk/gtk.h>
+
+#include "eoscellrenderertextlink-private.h"
+
+#define LINK_NORMAL_FOREGROUND_COLOR "#3465a4" /* sky blue 2 */
+#define LINK_HOVER_FOREGROUND_COLOR "#729fcf" /* sky blue 3 */
+
+G_DEFINE_TYPE (EosCellRendererTextLink, eos_cell_renderer_text_link, GTK_TYPE_CELL_RENDERER_TEXT)
+
+enum {
+ CLICKED,
+ LAST_SIGNAL
+};
+
+static guint text_link_signals[LAST_SIGNAL] = { 0 };
+
+static void
+eos_cell_renderer_text_link_render (GtkCellRenderer *renderer,
+ cairo_t *cr,
+ GtkWidget *widget,
+ const GdkRectangle *background_area,
+ const GdkRectangle *cell_area,
+ GtkCellRendererState flags)
+{
+ /* FIXME: the prelit flag is TRUE when the mouse is over the row that this
+ renderer is in - even if the mouse is not over the renderer itself. */
+ if (flags & GTK_CELL_RENDERER_PRELIT)
+ g_object_set (renderer,
+ "foreground", LINK_HOVER_FOREGROUND_COLOR,
+ NULL);
+ else
+ g_object_set (renderer,
+ "foreground", LINK_NORMAL_FOREGROUND_COLOR,
+ NULL);
+ GTK_CELL_RENDERER_CLASS (eos_cell_renderer_text_link_parent_class)->
+ render (renderer, cr, widget, background_area, cell_area, flags);
+}
+
+static gboolean
+eos_cell_renderer_text_link_activate (GtkCellRenderer *renderer,
+ GdkEvent *event,
+ GtkWidget *widget,
+ const gchar *path,
+ const GdkRectangle *background_area,
+ const GdkRectangle *cell_area,
+ GtkCellRendererState flags)
+{
+ g_signal_emit (renderer, text_link_signals[CLICKED], 0, path);
+ return TRUE;
+}
+
+static void
+eos_cell_renderer_text_link_class_init (EosCellRendererTextLinkClass *klass)
+{
+ GtkCellRendererClass *renderer_class = GTK_CELL_RENDERER_CLASS (klass);
+
+ renderer_class->render = eos_cell_renderer_text_link_render;
+ renderer_class->activate = eos_cell_renderer_text_link_activate;
+
+ text_link_signals[CLICKED] =
+ g_signal_new ("clicked", EOS_TYPE_CELL_RENDERER_TEXT_LINK,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE,
+ 1, G_TYPE_STRING);
+}
+
+static void
+eos_cell_renderer_text_link_init (EosCellRendererTextLink *self)
+{
+ g_object_set (self,
+ "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE,
+ NULL);
+}
+
+GtkCellRenderer *
+eos_cell_renderer_text_link_new (void)
+{
+ return GTK_CELL_RENDERER (g_object_new (EOS_TYPE_CELL_RENDERER_TEXT_LINK,
+ NULL));
+}
diff --git a/endless/eostopbar-private.h b/endless/eostopbar-private.h
index 3d0f4a7..aa6e44b 100644
--- a/endless/eostopbar-private.h
+++ b/endless/eostopbar-private.h
@@ -57,6 +57,11 @@ void eos_top_bar_set_center_widget (EosTopBar *self,
void eos_top_bar_update_window_maximized (EosTopBar *self,
gboolean is_maximized);
+gboolean eos_top_bar_get_show_credits_button (EosTopBar *self);
+
+void eos_top_bar_set_show_credits_button (EosTopBar *self,
+ gboolean show_credits_button);
+
G_END_DECLS
#endif /* EOS_TOP_BAR_H */
diff --git a/endless/eostopbar.c b/endless/eostopbar.c
index 9d7f276..3c1596d 100644
--- a/endless/eostopbar.c
+++ b/endless/eostopbar.c
@@ -29,6 +29,7 @@
#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"
+#define _EOS_TOP_BAR_CREDITS_ICON_NAME "user-info-symbolic"
typedef struct {
GtkWidget *actions_grid;
@@ -44,6 +45,13 @@ typedef struct {
GtkWidget *maximize_icon;
GtkWidget *close_button;
GtkWidget *close_icon;
+ GtkWidget *credits_button;
+ GtkWidget *credits_icon;
+ GtkWidget *credits_stack;
+
+ gboolean show_credits_button;
+ guint credits_enter_handler;
+ guint credits_leave_handler;
} EosTopBarPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (EosTopBar, eos_top_bar, GTK_TYPE_EVENT_BOX)
@@ -52,11 +60,58 @@ enum {
CLOSE_CLICKED,
MINIMIZE_CLICKED,
MAXIMIZE_CLICKED,
+ CREDITS_CLICKED,
LAST_SIGNAL
};
static guint top_bar_signals[LAST_SIGNAL] = { 0 };
+enum {
+ PROP_0,
+ PROP_SHOW_CREDITS_BUTTON,
+ NPROPS
+};
+
+static GParamSpec *eos_top_bar_props[NPROPS] = { NULL, };
+
+static void
+eos_top_bar_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EosTopBar *self = EOS_TOP_BAR (object);
+
+ switch (property_id)
+ {
+ case PROP_SHOW_CREDITS_BUTTON:
+ g_value_set_boolean (value, eos_top_bar_get_show_credits_button (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+eos_top_bar_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EosTopBar *self = EOS_TOP_BAR (object);
+
+ switch (property_id)
+ {
+ case PROP_SHOW_CREDITS_BUTTON:
+ eos_top_bar_set_show_credits_button (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
static void
eos_top_bar_get_preferred_height (GtkWidget *widget,
int *minimum,
@@ -98,6 +153,8 @@ eos_top_bar_class_init (EosTopBarClass *klass)
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ object_class->get_property = eos_top_bar_get_property;
+ object_class->set_property = eos_top_bar_set_property;
widget_class->get_preferred_height = eos_top_bar_get_preferred_height;
widget_class->draw = eos_top_bar_draw;
@@ -133,6 +190,19 @@ eos_top_bar_class_init (EosTopBarClass *klass)
0,
NULL, NULL, NULL,
G_TYPE_NONE, 0);
+
+ top_bar_signals[CREDITS_CLICKED] =
+ g_signal_new ("credits-clicked", G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ eos_top_bar_props[PROP_SHOW_CREDITS_BUTTON] =
+ g_param_spec_boolean ("show-credits-button", "Show credits button",
+ "Whether the credits button is discoverable",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, NPROPS, eos_top_bar_props);
}
static void
@@ -159,6 +229,23 @@ on_close_clicked_cb (GtkButton *button,
g_signal_emit (self, top_bar_signals[CLOSE_CLICKED], 0);
}
+static gboolean
+on_stack_hover (GtkStack *stack,
+ GdkEvent *event,
+ gpointer data)
+{
+ gboolean show = GPOINTER_TO_INT (data);
+ gtk_stack_set_visible_child_name (stack, show ? "button" : "blank");
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+on_credits_clicked (GtkButton *button,
+ EosTopBar *self)
+{
+ g_signal_emit (self, top_bar_signals[CREDITS_CLICKED], 0);
+}
+
static void
eos_top_bar_init (EosTopBar *self)
{
@@ -236,10 +323,36 @@ eos_top_bar_init (EosTopBar *self)
gtk_container_add (GTK_CONTAINER (priv->close_button),
priv->close_icon);
+ /* This works like a revealer but it's really a GtkStack so that it takes up
+ space and presents a target even when it's not shown. */
+ priv->credits_stack = gtk_stack_new ();
+ gtk_stack_set_transition_type (GTK_STACK (priv->credits_stack),
+ GTK_STACK_TRANSITION_TYPE_CROSSFADE);
+ gtk_widget_add_events (priv->credits_stack,
+ GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
+ gtk_stack_add_named (GTK_STACK (priv->credits_stack),
+ gtk_event_box_new (), "blank");
+ priv->credits_button = g_object_new (GTK_TYPE_BUTTON,
+ "can-focus", FALSE,
+ "halign", GTK_ALIGN_END,
+ "valign", GTK_ALIGN_CENTER,
+ NULL);
+ priv->credits_icon =
+ gtk_image_new_from_icon_name (_EOS_TOP_BAR_CREDITS_ICON_NAME,
+ GTK_ICON_SIZE_SMALL_TOOLBAR);
+ g_object_set (priv->credits_icon,
+ "pixel-size", _EOS_TOP_BAR_ICON_SIZE_PX,
+ "margin", _EOS_TOP_BAR_BUTTON_PADDING_PX,
+ NULL);
+ gtk_container_add (GTK_CONTAINER (priv->credits_button), priv->credits_icon);
+ gtk_stack_add_named (GTK_STACK (priv->credits_stack),
+ priv->credits_button, "button");
+
gtk_container_add (GTK_CONTAINER (priv->actions_grid),
priv->left_top_bar_attach);
gtk_container_add (GTK_CONTAINER (priv->actions_grid),
priv->center_top_bar_attach);
+ gtk_container_add (GTK_CONTAINER (priv->actions_grid), priv->credits_stack);
gtk_container_add (GTK_CONTAINER (priv->actions_grid),
priv->minimize_button);
gtk_container_add (GTK_CONTAINER (priv->actions_grid),
@@ -258,6 +371,16 @@ eos_top_bar_init (EosTopBar *self)
G_CALLBACK (on_maximize_clicked_cb), self);
g_signal_connect (priv->close_button, "clicked",
G_CALLBACK (on_close_clicked_cb), self);
+ priv->credits_enter_handler =
+ g_signal_connect (priv->credits_stack, "enter-notify-event",
+ G_CALLBACK (on_stack_hover), GINT_TO_POINTER (TRUE));
+ priv->credits_leave_handler =
+ g_signal_connect (priv->credits_stack, "leave-notify-event",
+ G_CALLBACK (on_stack_hover), GINT_TO_POINTER (FALSE));
+ g_signal_handler_block (priv->credits_stack, priv->credits_enter_handler);
+ g_signal_handler_block (priv->credits_stack, priv->credits_leave_handler);
+ g_signal_connect (priv->credits_button, "clicked",
+ G_CALLBACK (on_credits_clicked), self);
}
GtkWidget *
@@ -355,3 +478,55 @@ eos_top_bar_update_window_maximized (EosTopBar *self,
else
gtk_style_context_remove_class (context, _EOS_STYLE_CLASS_UNMAXIMIZED);
}
+
+/*
+ * eos_top_bar_get_show_credits_button:
+ * @self: the top bar
+ *
+ * See eos_top_bar_set_show_credits_button().
+ *
+ * Returns: %TRUE if credits button should be discoverable, %FALSE if not.
+ */
+gboolean
+eos_top_bar_get_show_credits_button (EosTopBar *self)
+{
+ EosTopBarPrivate *priv = eos_top_bar_get_instance_private (self);
+ return priv->show_credits_button;
+}
+
+/*
+ * eos_top_bar_set_show_credits_button:
+ * @self: the top bar
+ * @show_credits_button: whether the credits button should be discoverable.
+ *
+ * Gets whether the credits button should be discoverable.
+ * Note that the credits button is not visible as such, but when the mouse
+ * hovers over it, it becomes visible if this is set to %TRUE.
+ * If this is %FALSE, the button never becomes visible.
+ */
+void
+eos_top_bar_set_show_credits_button (EosTopBar *self,
+ gboolean show_credits_button)
+{
+ EosTopBarPrivate *priv = eos_top_bar_get_instance_private (self);
+ if (priv->show_credits_button == show_credits_button)
+ return;
+
+ priv->show_credits_button = show_credits_button;
+ if (show_credits_button)
+ {
+ g_signal_handler_unblock (priv->credits_stack,
+ priv->credits_enter_handler);
+ g_signal_handler_unblock (priv->credits_stack,
+ priv->credits_leave_handler);
+ }
+ else
+ {
+ gtk_stack_set_visible_child_name (GTK_STACK (priv->credits_stack),
+ "blank");
+ g_signal_handler_block (priv->credits_stack, priv->credits_enter_handler);
+ g_signal_handler_block (priv->credits_stack, priv->credits_leave_handler);
+ }
+ g_object_notify_by_pspec (G_OBJECT (self),
+ eos_top_bar_props[PROP_SHOW_CREDITS_BUTTON]);
+}
diff --git a/endless/eoswindow.c b/endless/eoswindow.c
index 0b39ffb..d128a5e 100644
--- a/endless/eoswindow.c
+++ b/endless/eoswindow.c
@@ -342,6 +342,34 @@ update_page (EosWindow *self)
}
static void
+on_image_credits_enabled_changed (GActionGroup *group,
+ const gchar *action_name,
+ gboolean enabled,
+ EosWindow *self)
+{
+ EosWindowPrivate *priv = eos_window_get_instance_private (self);
+ eos_top_bar_set_show_credits_button (EOS_TOP_BAR (priv->top_bar), enabled);
+}
+
+static void
+eos_window_constructed (GObject *object)
+{
+ EosWindow *self = EOS_WINDOW (object);
+ EosWindowPrivate *priv = eos_window_get_instance_private (self);
+
+ G_OBJECT_CLASS (eos_window_parent_class)->constructed (object);
+
+ GtkApplication *application =
+ gtk_window_get_application (GTK_WINDOW (object));
+ GFile *credits_file =
+ eos_application_get_image_attribution_file (EOS_APPLICATION (application));
+ eos_top_bar_set_show_credits_button (EOS_TOP_BAR (priv->top_bar),
+ (credits_file != NULL));
+ g_signal_connect (application, "action-enabled-changed::image-credits",
+ G_CALLBACK (on_image_credits_enabled_changed), self);
+}
+
+static void
eos_window_get_property (GObject *object,
guint property_id,
GValue *value,
@@ -510,6 +538,7 @@ eos_window_class_init (EosWindowClass *klass)
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ object_class->constructed = eos_window_constructed;
object_class->get_property = eos_window_get_property;
object_class->set_property = eos_window_set_property;
object_class->finalize = eos_window_finalize;
@@ -631,6 +660,16 @@ on_close_clicked_cb (GtkWidget *top_bar,
gtk_window_close (GTK_WINDOW (self));
}
+static void
+on_credits_clicked (GtkWidget *top_bar,
+ EosWindow *self)
+{
+ GtkApplication *application = gtk_window_get_application (GTK_WINDOW (self));
+ /* application cannot be NULL */
+ g_action_group_activate_action (G_ACTION_GROUP (application), "image-credits",
+ NULL);
+}
+
static gboolean
on_window_state_event_cb (GtkWidget *widget,
GdkEventWindowState *event)
@@ -748,6 +787,8 @@ eos_window_init (EosWindow *self)
G_CALLBACK (on_maximize_clicked_cb), self);
g_signal_connect (priv->top_bar, "close-clicked",
G_CALLBACK (on_close_clicked_cb), self);
+ g_signal_connect (priv->top_bar, "credits-clicked",
+ G_CALLBACK (on_credits_clicked), self);
g_signal_connect (self, "window-state-event",
G_CALLBACK (on_window_state_event_cb), NULL);
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 667e27c..588e6ab 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1 +1,3 @@
# List of source files which contain translatable strings.
+endless/eosapplication.c
+endless/eosattribution.c
diff --git a/test/endless/test-application.c b/test/endless/test-application.c
index 75165d4..13a53b4 100644
--- a/test/endless/test-application.c
+++ b/test/endless/test-application.c
@@ -14,7 +14,7 @@ typedef struct
{
gchar *unique_id;
EosApplication *app;
-} ConfigDirFixture;
+} UniqueAppFixture;
static void
test_two_windows (EosApplication *app)
@@ -36,7 +36,7 @@ test_two_windows (EosApplication *app)
}
static void
-config_dir_setup (ConfigDirFixture *fixture,
+unique_app_setup (UniqueAppFixture *fixture,
gconstpointer unused)
{
fixture->unique_id = generate_unique_app_id ();
@@ -45,7 +45,7 @@ config_dir_setup (ConfigDirFixture *fixture,
}
static void
-config_dir_teardown (ConfigDirFixture *fixture,
+unique_app_teardown (UniqueAppFixture *fixture,
gconstpointer unused)
{
/* Clean up the temporary config directory */
@@ -57,7 +57,7 @@ config_dir_teardown (ConfigDirFixture *fixture,
}
static void
-test_config_dir_get (ConfigDirFixture *fixture,
+test_config_dir_get (UniqueAppFixture *fixture,
gconstpointer unused)
{
GFile *dir1 = eos_application_get_config_dir (fixture->app);
@@ -72,7 +72,30 @@ test_config_dir_get (ConfigDirFixture *fixture,
}
static void
-test_config_dir_returns_expected_path (ConfigDirFixture *fixture,
+test_image_attribution_file_get_set (UniqueAppFixture *fixture,
+ gconstpointer unused)
+{
+ GFile *file1, *file2;
+ GFileIOStream *stream;
+ g_object_get (fixture->app, "image-attribution-file", &file1, NULL);
+
+ g_assert_null (file1);
+
+ file1 = g_file_new_tmp (NULL, &stream, NULL);
+ g_assert_nonnull (file1);
+ g_io_stream_close (G_IO_STREAM (stream), NULL, NULL);
+ g_object_unref (stream);
+ g_object_set (fixture->app, "image-attribution-file", file1, NULL);
+ g_object_get (fixture->app, "image-attribution-file", &file2, NULL);
+
+ g_assert_true (g_file_equal (file1, file2));
+
+ g_object_unref (file1);
+ g_object_unref (file2);
+}
+
+static void
+test_config_dir_returns_expected_path (UniqueAppFixture *fixture,
gconstpointer unused)
{
GFile *config_dir = eos_application_get_config_dir (fixture->app);
@@ -96,7 +119,7 @@ test_config_dir_returns_expected_path (ConfigDirFixture *fixture,
}
static void
-test_config_dir_exists (ConfigDirFixture *fixture,
+test_config_dir_exists (UniqueAppFixture *fixture,
gconstpointer unused)
{
GFile *config_dir = eos_application_get_config_dir (fixture->app);
@@ -119,7 +142,7 @@ set_writable (GFile *file,
}
static void
-test_config_dir_fails_if_not_writable (ConfigDirFixture *fixture,
+test_config_dir_fails_if_not_writable (UniqueAppFixture *fixture,
gconstpointer unused)
{
/* Pre-create the config dir and make it non-writable */
@@ -150,27 +173,25 @@ void
add_application_tests (void)
{
ADD_APP_WINDOW_TEST ("/application/two-windows", test_two_windows);
- g_test_add ("/application/config-dir-get", ConfigDirFixture, NULL,
- config_dir_setup,
- test_config_dir_get,
- config_dir_teardown);
- g_test_add ("/application/config-dir-expected-path", ConfigDirFixture, NULL,
- config_dir_setup,
- test_config_dir_returns_expected_path,
- config_dir_teardown);
- g_test_add ("/application/config-dir-exists", ConfigDirFixture, NULL,
- config_dir_setup,
- test_config_dir_exists,
- config_dir_teardown);
+
+#define ADD_APP_TEST(path, func) \
+ g_test_add((path), UniqueAppFixture, NULL, \
+ unique_app_setup, (func), unique_app_teardown)
+
+ ADD_APP_TEST ("/application/config-dir-get", test_config_dir_get);
+ ADD_APP_TEST ("/application/image-attribution-file-get-set",
+ test_image_attribution_file_get_set);
+ ADD_APP_TEST ("/application/config-dir-expected-path",
+ test_config_dir_returns_expected_path);
+ ADD_APP_TEST ("/application/config-dir-exists", test_config_dir_exists);
/* Only run this test if UID is not root; root can write to any directory no
matter what its permissions. */
if (getuid() > 0 && geteuid() > 0)
{
- g_test_add ("/application/config-dir-fails-if-not-writable",
- ConfigDirFixture, NULL,
- config_dir_setup,
- test_config_dir_fails_if_not_writable,
- config_dir_teardown);
+ ADD_APP_TEST ("/application/config-dir-fails-if-not-writable",
+ test_config_dir_fails_if_not_writable);
}
+
+#undef ADD_APP_TEST
}
diff --git a/test/smoke-tests/Makefile.am.inc b/test/smoke-tests/Makefile.am.inc
index 2c0fba3..170b335 100644
--- a/test/smoke-tests/Makefile.am.inc
+++ b/test/smoke-tests/Makefile.am.inc
@@ -3,3 +3,18 @@
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)
+
+credits_resource_files = \
+ test/smoke-tests/images/test1.jpg \
+ test/smoke-tests/images/test2.jpg \
+ test/smoke-tests/images/test3.jpg \
+ test/smoke-tests/images/attribution.json \
+ $(NULL)
+test/smoke-tests/images/credits.gresource: test/smoke-tests/images/credits.gresource.xml $(credits_resource_files)
+ $(AM_V_GEN)$(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir)/test/smoke-tests/images $<
+dist_noinst_DATA = test/smoke-tests/images/credits.gresource
+CLEANFILES += test/smoke-tests/images/credits.gresource
+EXTRA_DIST += \
+ test/smoke-tests/images/credits.gresource.xml \
+ $(credits_resource_files) \
+ $(NULL)
diff --git a/test/smoke-tests/credits.js b/test/smoke-tests/credits.js
new file mode 100644
index 0000000..5d4be70
--- /dev/null
+++ b/test/smoke-tests/credits.js
@@ -0,0 +1,44 @@
+const Endless = imports.gi.Endless;
+const GdkPixbuf = imports.gi.GdkPixbuf;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const TestApp = new Lang.Class({
+ Name: 'TestApp',
+ Extends: Endless.Application,
+
+ vfunc_startup: function () {
+ this.parent();
+ let win = new TestWindow({ application: this });
+ win.show_all();
+ }
+});
+
+const TestWindow = new Lang.Class({
+ Name: 'TestWindow',
+ Extends: Endless.Window,
+
+ _init: function (props) {
+ this.parent(props);
+
+ let grid = new Gtk.Grid({ orientation: Gtk.Orientation.HORIZONTAL });
+ ['test1', 'test2', 'test3', 'Fahrradrheinpromenade'].forEach((key) => {
+ let pixbuf = GdkPixbuf.Pixbuf.new_from_resource_at_scale('/com/example/attributiontest/' + key + '.jpg',
+ 200, -1, true);
+ let image = Gtk.Image.new_from_pixbuf(pixbuf);
+ grid.add(image);
+ });
+ this.page_manager.add(grid);
+ }
+});
+
+let resource = Gio.Resource.load(Endless.getCurrentFileDir() + '/images/credits.gresource');
+resource._register();
+
+let credits = Gio.File.new_for_uri('resource:///com/example/attributiontest/attribution.json');
+let app = new TestApp({
+ application_id: 'com.example.attribution',
+ image_attribution_file: credits
+});
+app.run(ARGV);
diff --git a/test/smoke-tests/images/Fahrradrheinpromenade.jpg b/test/smoke-tests/images/Fahrradrheinpromenade.jpg
new file mode 100644
index 0000000..396edba
--- /dev/null
+++ b/test/smoke-tests/images/Fahrradrheinpromenade.jpg
Binary files differ
diff --git a/test/smoke-tests/images/attribution.json b/test/smoke-tests/images/attribution.json
new file mode 100644
index 0000000..bfed553
--- /dev/null
+++ b/test/smoke-tests/images/attribution.json
@@ -0,0 +1,27 @@
+[
+ {
+ "resource_path": "/com/example/attributiontest/test2.jpg",
+ "license": "Public domain",
+ "uri": "http://www.flickr.com/photos/lsuc_archives/10613348833/",
+ "comment": "No known copyright restrictions"
+ },
+ {
+ "resource_path": "/com/example/attributiontest/test1.jpg",
+ "license_uri": "http://www.sxc.hu/txt/license.html",
+ "uri": "http://www.sxc.hu/browse.phtml?f=view&id=146768&rnd=1",
+ "credit": "cressida",
+ "credit_contact": "http://www.sxc.hu/profile/cressida"
+ },
+ {
+ "resource_path": "/com/example/attributiontest/test3.jpg",
+ "copyright_holder": "Philip Chimento",
+ "copyright_year": 2011,
+ "permission": true
+ },
+ {
+ "resource_path": "/com/example/attributiontest/Fahrradrheinpromenade.jpg",
+ "credit": "Florian Scholz",
+ "license": "CC BY 2.0",
+ "uri": "https://www.flickr.com/photos/fscholz/13540636975/"
+ }
+]
diff --git a/test/smoke-tests/images/credits.gresource.xml b/test/smoke-tests/images/credits.gresource.xml
new file mode 100644
index 0000000..51f1656
--- /dev/null
+++ b/test/smoke-tests/images/credits.gresource.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/com/example/attributiontest">
+ <file>test1.jpg</file>
+ <file>test2.jpg</file>
+ <file>test3.jpg</file>
+ <file>Fahrradrheinpromenade.jpg</file>
+ <file>attribution.json</file>
+ </gresource>
+</gresources>
diff --git a/test/smoke-tests/images/test1.jpg b/test/smoke-tests/images/test1.jpg
new file mode 100644
index 0000000..f9fd56a
--- /dev/null
+++ b/test/smoke-tests/images/test1.jpg
Binary files differ
diff --git a/test/smoke-tests/images/test2.jpg b/test/smoke-tests/images/test2.jpg
new file mode 100644
index 0000000..e75380e
--- /dev/null
+++ b/test/smoke-tests/images/test2.jpg
Binary files differ
diff --git a/test/smoke-tests/images/test3.jpg b/test/smoke-tests/images/test3.jpg
new file mode 100644
index 0000000..81056d0
--- /dev/null
+++ b/test/smoke-tests/images/test3.jpg
Binary files differ