diff options
27 files changed, 1781 insertions, 26 deletions
@@ -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, ©right_holder, + COLUMN_COPYRIGHT_YEAR, ©right_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 Binary files differnew file mode 100644 index 0000000..396edba --- /dev/null +++ b/test/smoke-tests/images/Fahrradrheinpromenade.jpg 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 Binary files differnew file mode 100644 index 0000000..f9fd56a --- /dev/null +++ b/test/smoke-tests/images/test1.jpg diff --git a/test/smoke-tests/images/test2.jpg b/test/smoke-tests/images/test2.jpg Binary files differnew file mode 100644 index 0000000..e75380e --- /dev/null +++ b/test/smoke-tests/images/test2.jpg diff --git a/test/smoke-tests/images/test3.jpg b/test/smoke-tests/images/test3.jpg Binary files differnew file mode 100644 index 0000000..81056d0 --- /dev/null +++ b/test/smoke-tests/images/test3.jpg |