summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Chimento <philip@endlessm.com>2014-01-08 16:51:39 -0500
committerPhilip Chimento <philip@endlessm.com>2015-03-25 17:42:34 -0700
commit67c212e47a1db5f3e3dd1abaf552052e46512c93 (patch)
tree84d7d7906b033e502561c4136efeda68312cc6bc
parent88a142a202a3ae5e82f222cbb5ce75372d66e719 (diff)
Initial implementation of image credits
This is a rough implementation of an "About"-like dialog for image attribution in SDK applications. You can press the 'secret' hotkey Shift+Ctrl+A to get a dialog with thumbnails and attribution information for the images in the app's GResource. Clicking on the thumbnail opens the image's original URI in the browser, if known. Clicking on the text opens the image's license text in the browser, if known. For this, you need to add a JSON file to the app's GResource and pass its GResource path to the EosApplication:image-attribution-file property. The format of this JSON file is described in the documentation for EosApplication. The dialog uses GtkTreeView because we didn't have GtkListBox when I started implementing it over a year ago. This places some limitations on the UI; the links behave weirdly and the mouse pointer doesn't change to a hand when hovering over the links. [endlessm/eos-sdk#2934]
-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.c398
-rw-r--r--endless/eosapplication.h7
-rw-r--r--endless/eosattribution-private.h57
-rw-r--r--endless/eosattribution.c582
-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--po/POTFILES.in2
-rw-r--r--test/endless/test-application.c25
-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
24 files changed, 1433 insertions, 2 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..9ca109b 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,67 @@ 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 ();
+ if (!eos_attribution_populate_from_json_file (EOS_ATTRIBUTION (attribution),
+ priv->image_attribution_file,
+ &error))
+ {
+ g_warning ("Error loading image attribution file: %s", error->message);
+ gtk_widget_destroy (attribution);
+ 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 +314,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 +349,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 +431,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 +530,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,6 +551,63 @@ 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);
@@ -295,6 +618,14 @@ 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);
+
g_signal_connect (self, "notify::application-id",
G_CALLBACK (on_app_id_set), self);
}
@@ -353,3 +684,70 @@ 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)
+ {
+ GAction *action = g_action_map_lookup_action (G_ACTION_MAP (self),
+ "image-credits");
+ gboolean enabled = (file == NULL);
+ g_simple_action_set_enabled (G_SIMPLE_ACTION (self), enabled);
+ /* action map owns action */
+ }
+
+ 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..b468a71
--- /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 (void);
+
+gboolean eos_attribution_populate_from_json_file (EosAttribution *self,
+ GFile *file,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* EOS_ATTRIBUTION_H */
diff --git a/endless/eosattribution.c b/endless/eosattribution.c
new file mode 100644
index 0000000..ea22fe0
--- /dev/null
+++ b/endless/eosattribution.c
@@ -0,0 +1,582 @@
+/* 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
+{
+ GtkWidget *view;
+ GtkListStore *model;
+} EosAttributionPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (EosAttribution, eos_attribution,
+ GTK_TYPE_SCROLLED_WINDOW)
+
+#define ROW_HEIGHT 50 /* Height of the pixbufs in each row */
+
+enum {
+ SHOW_URI,
+ LAST_SIGNAL
+};
+
+static guint attribution_signals[LAST_SIGNAL] = { 0 };
+
+/* 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_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->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);
+}
+
+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);
+}
+
+GtkWidget *
+eos_attribution_new (void)
+{
+ return GTK_WIDGET (g_object_new (EOS_TYPE_ATTRIBUTION, NULL));
+}
+
+/* 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;
+}
+
+gboolean
+eos_attribution_populate_from_json_file (EosAttribution *self,
+ GFile *file,
+ GError **error)
+{
+ EosAttributionPrivate *priv = eos_attribution_get_instance_private (self);
+
+ GInputStream *stream = G_INPUT_STREAM (g_file_read (file, NULL, error));
+ if (stream == NULL)
+ return FALSE;
+
+ JsonParser *parser = json_parser_new ();
+ gboolean success = json_parser_load_from_stream (parser, stream, NULL, error);
+ g_input_stream_close (stream, NULL, NULL); /* ignore errors */
+ g_object_unref (stream);
+ if (!success)
+ goto fail;
+
+ 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;
+}
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/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 bb31b8f..13a53b4 100644
--- a/test/endless/test-application.c
+++ b/test/endless/test-application.c
@@ -72,6 +72,29 @@ test_config_dir_get (UniqueAppFixture *fixture,
}
static void
+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)
{
@@ -156,6 +179,8 @@ add_application_tests (void)
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);
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