/* Copyright 2013 Endless Mobile, Inc. */ #include "config.h" #include "eosapplication.h" #include "eosattribution-private.h" #include #include #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 * @short_description: Start here with your application * @title: Applications * * The #EosApplication class is where you start when programming your * application. * You should create a class that extends #EosApplication. * * You also need to think up an application ID. * This takes the form of a reverse domain name, and it should be unique. * This ID is used to make sure that only one copy of your application is * running at any time; if a user tries to start a second copy, then the first * copy is brought to the front. * * To set up your application's data and window, override the * #GApplication::startup function, like this example do-nothing application, * Smoke Grinder: * |[ * const Lang = imports.lang; * const Endless = imports.gi.Endless; * * const SmokeGrinder = new Lang.Class ({ * Name: 'SmokeGrinder', * Extends: Endless.Application, * * vfunc_startup: function() { * this.parent(); * this._window = new Endless.Window({application: this}); * this._window.show_all(); * }, * }); * * let app = new SmokeGrinder({ application_id: "com.example.smokegrinder", * 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 . * See #EosApplication:image-attribution-file. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
PropertyRequired?TypeDescription
thumb_uriYesstring * A URI to show in the dialog. (e.g. resource:///com/endlessm/...) *
uriNostring * URI where the original image is to be found: e.g., a Flickr link. *
license*string * Text identifying the license under which you are using this image. * This field is not free-form; the allowed values are listed in . * If the license is not listed there, leave this field blank and clarify * the license in the comment field. *
license_uri*string * URI linking to the text of the image license. * If you use the license 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. *
credit*string * 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. *
credit_contactNostring * URI at which the author can be contacted. * (If this is an e-mail address, prefix it with mailto: so * that it is a valid URI.) *
copyright_holder*string * Copyright holder of the image. *
copyright_yearNointeger * Copyright year of the image. * This will be displayed along with the copyright holder. *
permissionNoboolean * Whether the image is used with permission. * If this is specified, a string such as Used with * permission may be displayed. *
commentNostring * Any other comments about the image license, terms of use, or source. *
*At least one of these properties is required.
Allowed properties of the objects in the image attribution JSON * file
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
StringDescription
Public domainPublic domain
CC0 1.0CC0 * 1.0 Universal (Public domain)
CC BY 2.0Creative * Commons Attribution 2.0
CC BY 3.0Creative * Commons Attribution 3.0
CC BY-SA 2.0Creative * Commons Attribution-ShareAlike 2.0
CC BY-SA 3.0Creative * Commons Attribution-ShareAlike 3.0
CC BY-ND 2.0Creative * Commons Attribution-NoDerivs 2.0
CC BY-ND 3.0Creative * Commons Attribution-NoDerivs 3.0
Allowed values for the license property in the image * attribution JSON file
*/ typedef struct { GOnce init_config_dir_once; GFile *config_dir; GFile *image_attribution_file; EosWindow *main_application_window; } EosApplicationPrivate; G_DEFINE_TYPE_WITH_PRIVATE (EosApplication, eos_application, GTK_TYPE_APPLICATION) 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, GValue *value, GParamSpec *pspec) { EosApplication *self = EOS_APPLICATION (object); switch (property_id) { case PROP_CONFIG_DIR: 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); } } static void 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); } static void eos_application_activate (GApplication *application) { EosApplication *self = EOS_APPLICATION (application); EosApplicationPrivate *priv = eos_application_get_instance_private (self); G_APPLICATION_CLASS (eos_application_parent_class)->activate (application); /* Raise the main application window if it is iconified. This behavior will be default in GTK at some future point, in which case the following paragraph can be removed. */ if (priv->main_application_window) { gtk_window_present (GTK_WINDOW (priv->main_application_window)); } /* TODO: Should it be required to override activate() as in GApplication? */ } static gpointer ensure_config_dir_exists_and_is_writable (EosApplication *self) { EosApplicationPrivate *priv = eos_application_get_instance_private (self); const gchar *xdg_path = g_get_user_config_dir (); const gchar *app_id = g_application_get_application_id (G_APPLICATION (self)); GFile *xdg_dir = g_file_new_for_path (xdg_path); GFile *config_dir = g_file_get_child (xdg_dir, app_id); gchar *config_path = g_file_get_path (config_dir); /* For error reporting */ g_object_unref (xdg_dir); GError *error = NULL; if (!g_file_make_directory_with_parents (config_dir, NULL, &error)) { if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_EXISTS) { g_clear_error (&error); /* Ignore G_IO_ERROR_EXISTS */ } else { g_error ("There was an error creating the user config directory %s: " "%s", config_path, error->message); } } GFileInfo *info = g_file_query_info (config_dir, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, G_FILE_QUERY_INFO_NONE, NULL, &error); if (info == NULL) { g_error ("Checking the user config directory %s failed. This means " "something strange is going on in your home directory: %s", config_path, error->message); } if (!g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) { g_error ("Your user config directory %s is not writable. This means " "something strange is going on in your home directory.", config_path); } g_object_unref (info); g_free (config_path); priv->config_dir = config_dir; return NULL; } static void 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[] = { "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 is so that we do not interfere with existing, complicated Adwaita theming. */ GFile *css_file = g_file_new_for_uri (CSS_THEME_URI); gtk_css_provider_load_from_file (provider, css_file, NULL); g_object_unref (css_file); gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_SETTINGS); g_debug ("Initialized theme\n"); g_object_unref (provider); EosApplication *self = EOS_APPLICATION (application); EosApplicationPrivate *priv = eos_application_get_instance_private (self); g_once (&priv->init_config_dir_once, (GThreadFunc)ensure_config_dir_exists_and_is_writable, self); } static void eos_application_window_added (GtkApplication *application, GtkWindow *window) { EosApplication *self = EOS_APPLICATION (application); EosApplicationPrivate *priv = eos_application_get_instance_private (self); GTK_APPLICATION_CLASS (eos_application_parent_class)->window_added ( application, window); /* If the new window is an EosWindow, then it is our main application window; it should be raised when the application is activated */ if (EOS_IS_WINDOW (window)) { if (priv->main_application_window != NULL) { g_error ("You should not add more than one application window."); } g_object_ref (window); priv->main_application_window = EOS_WINDOW (window); } } static void eos_application_window_removed (GtkApplication *application, GtkWindow *window) { EosApplication *self = EOS_APPLICATION (application); EosApplicationPrivate *priv = eos_application_get_instance_private (self); GTK_APPLICATION_CLASS (eos_application_parent_class)->window_removed ( application, window); if (EOS_IS_WINDOW (window)) { if (priv->main_application_window == NULL) { g_warning ("EosWindow is being removed from EosApplication, although " "none was added."); return; } if (priv->main_application_window != EOS_WINDOW (window)) g_warning ("A different EosWindow is being removed from EosApplication " "than the one that was added."); g_object_unref (window); priv->main_application_window = NULL; } } static void on_app_id_set (EosApplication *self) { const gchar *id = g_application_get_application_id (G_APPLICATION (self)); g_set_prgname (id); /* Just in case, since g_set_prgname() does not always update the GDK program class, under mysterious circumstances */ gchar *capitalized_id = g_strdup (id); if (capitalized_id != NULL && capitalized_id[0] != '\0') capitalized_id[0] = g_ascii_toupper (capitalized_id[0]); gdk_set_program_class (capitalized_id); } static void eos_application_class_init (EosApplicationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GApplicationClass *g_application_class = G_APPLICATION_CLASS (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; gtk_application_class->window_added = eos_application_window_added; gtk_application_class->window_removed = eos_application_window_removed; /** * EosApplication:config-dir: * * A directory appropriate for storing per-user configuration information for * this application. * Accessing this property guarantees that the directory exists and is * writable. * See also eos_application_get_config_dir() for more information. */ eos_application_props[PROP_CONFIG_DIR] = g_param_spec_object ("config-dir", "Config dir", "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 resource_path, which is the path * to the image in the resource file. * * The recognized properties are shown in . * * Nothing is guaranteed about how the application uses this information. * It can display it to the user or make it available to other programs. * * * Currently, pressing Control * ShiftA brings up a credits * dialog. * This is liable to change in future versions. * * * 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); } /* Public API */ /** * eos_application_new: * @application_id: a unique identifier for the application, for example a * reverse domain name. * @flags: flags to apply to the application; see #GApplicationFlags. * * Create a new application. For the application ID, use a reverse domain name, * such as com.endlessm.weather. See g_application_id_is_valid() * for the full rules for application IDs. * * Returns: a pointer to the application. */ EosApplication * eos_application_new (const gchar *application_id, GApplicationFlags flags) { return g_object_new (EOS_TYPE_APPLICATION, "application-id", application_id, "flags", flags, NULL); } /** * eos_application_get_config_dir: * @self: the application * * Gets a #GFile pointing to the application-specific user configuration * directory. * This directory is located in XDG_USER_CONFIG_DIR, which usually * expands to ~/.config. * The directory name is the same as the application's unique ID (see * #GApplication:application-id.) * * You should use this directory to store configuration data specific to your * application and specific to one user, such as cookies. * * Calling this function will also ensure that the directory exists and is * writable. * If it does not exist, it will be created. * If it cannot be created, or it exists but is not writable, the program will * abort. * * Returns: (transfer none): A #GFile pointing to the user config directory. */ GFile * eos_application_get_config_dir (EosApplication *self) { EosApplicationPrivate *priv = eos_application_get_instance_private (self); g_once (&priv->init_config_dir_once, (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]); }