/* * Copyright © 2013 Lars Uebernickel * * SPDX-License-Identifier: LGPL-2.1-or-later * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, see . * * Authors: Lars Uebernickel */ #include "config.h" #include "gnotification-private.h" #include "gdbusutils.h" #include "gicon.h" #include "gaction.h" #include "gioenumtypes.h" /** * GNotification: * * `GNotification` is a mechanism for creating a notification to be shown * to the user — typically as a pop-up notification presented by the * desktop environment shell. * * The key difference between `GNotification` and other similar APIs is * that, if supported by the desktop environment, notifications sent * with `GNotification` will persist after the application has exited, * and even across system reboots. * * Since the user may click on a notification while the application is * not running, applications using `GNotification` should be able to be * started as a D-Bus service, using [class@Gio.Application]. * * In order for `GNotification` to work, the application must have installed * a `.desktop` file. For example: * ``` * [Desktop Entry] * Name=Test Application * Comment=Description of what Test Application does * Exec=gnome-test-application * Icon=org.gnome.TestApplication * Terminal=false * Type=Application * Categories=GNOME;GTK;TestApplication Category; * StartupNotify=true * DBusActivatable=true * X-GNOME-UsesNotifications=true * ``` * * The `X-GNOME-UsesNotifications` key indicates to GNOME Control Center * that this application uses notifications, so it can be listed in the * Control Center’s ‘Notifications’ panel. * * The `.desktop` file must be named as `org.gnome.TestApplication.desktop`, * where `org.gnome.TestApplication` is the ID passed to * [ctor@Gio.Application.new]. * * User interaction with a notification (either the default action, or * buttons) must be associated with actions on the application (ie: * `app.` actions). It is not possible to route user interaction * through the notification itself, because the object will not exist if * the application is autostarted as a result of a notification being * clicked. * * A notification can be sent with [method@Gio.Application.send_notification]. * * Since: 2.40 **/ typedef GObjectClass GNotificationClass; struct _GNotification { GObject parent; gchar *title; gchar *body; GIcon *icon; GNotificationPriority priority; gchar *category; GPtrArray *buttons; gchar *default_action; GVariant *default_action_target; /* (nullable) (owned), not floating */ }; typedef struct { gchar *label; gchar *action_name; GVariant *target; } Button; G_DEFINE_TYPE (GNotification, g_notification, G_TYPE_OBJECT) static void button_free (gpointer data) { Button *button = data; g_free (button->label); g_free (button->action_name); if (button->target) g_variant_unref (button->target); g_slice_free (Button, button); } static void g_notification_dispose (GObject *object) { GNotification *notification = G_NOTIFICATION (object); g_clear_object (¬ification->icon); G_OBJECT_CLASS (g_notification_parent_class)->dispose (object); } static void g_notification_finalize (GObject *object) { GNotification *notification = G_NOTIFICATION (object); g_free (notification->title); g_free (notification->body); g_free (notification->category); g_free (notification->default_action); if (notification->default_action_target) g_variant_unref (notification->default_action_target); g_ptr_array_free (notification->buttons, TRUE); G_OBJECT_CLASS (g_notification_parent_class)->finalize (object); } static void g_notification_class_init (GNotificationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = g_notification_dispose; object_class->finalize = g_notification_finalize; } static void g_notification_init (GNotification *notification) { notification->buttons = g_ptr_array_new_full (2, button_free); } /** * g_notification_new: * @title: the title of the notification * * Creates a new #GNotification with @title as its title. * * After populating @notification with more details, it can be sent to * the desktop shell with g_application_send_notification(). Changing * any properties after this call will not have any effect until * resending @notification. * * Returns: a new #GNotification instance * * Since: 2.40 */ GNotification * g_notification_new (const gchar *title) { GNotification *notification; g_return_val_if_fail (title != NULL, NULL); notification = g_object_new (G_TYPE_NOTIFICATION, NULL); notification->title = g_strdup (title); return notification; } /*< private > * g_notification_get_title: * @notification: a #GNotification * * Gets the title of @notification. * * Returns: the title of @notification * * Since: 2.40 */ const gchar * g_notification_get_title (GNotification *notification) { g_return_val_if_fail (G_IS_NOTIFICATION (notification), NULL); return notification->title; } /** * g_notification_set_title: * @notification: a #GNotification * @title: the new title for @notification * * Sets the title of @notification to @title. * * Since: 2.40 */ void g_notification_set_title (GNotification *notification, const gchar *title) { g_return_if_fail (G_IS_NOTIFICATION (notification)); g_return_if_fail (title != NULL); g_free (notification->title); notification->title = g_strdup (title); } /*< private > * g_notification_get_body: * @notification: a #GNotification * * Gets the current body of @notification. * * Returns: (nullable): the body of @notification * * Since: 2.40 */ const gchar * g_notification_get_body (GNotification *notification) { g_return_val_if_fail (G_IS_NOTIFICATION (notification), NULL); return notification->body; } /** * g_notification_set_body: * @notification: a #GNotification * @body: (nullable): the new body for @notification, or %NULL * * Sets the body of @notification to @body. * * Since: 2.40 */ void g_notification_set_body (GNotification *notification, const gchar *body) { g_return_if_fail (G_IS_NOTIFICATION (notification)); g_return_if_fail (body != NULL); g_free (notification->body); notification->body = g_strdup (body); } /*< private > * g_notification_get_icon: * @notification: a #GNotification * * Gets the icon currently set on @notification. * * Returns: (transfer none): the icon associated with @notification * * Since: 2.40 */ GIcon * g_notification_get_icon (GNotification *notification) { g_return_val_if_fail (G_IS_NOTIFICATION (notification), NULL); return notification->icon; } /** * g_notification_set_icon: * @notification: a #GNotification * @icon: the icon to be shown in @notification, as a #GIcon * * Sets the icon of @notification to @icon. * * Since: 2.40 */ void g_notification_set_icon (GNotification *notification, GIcon *icon) { g_return_if_fail (G_IS_NOTIFICATION (notification)); if (notification->icon) g_object_unref (notification->icon); notification->icon = g_object_ref (icon); } /*< private > * g_notification_get_priority: * @notification: a #GNotification * * Returns the priority of @notification * * Since: 2.42 */ GNotificationPriority g_notification_get_priority (GNotification *notification) { g_return_val_if_fail (G_IS_NOTIFICATION (notification), G_NOTIFICATION_PRIORITY_NORMAL); return notification->priority; } /** * g_notification_set_urgent: * @notification: a #GNotification * @urgent: %TRUE if @notification is urgent * * Deprecated in favor of g_notification_set_priority(). * * Since: 2.40 * Deprecated: 2.42: Since 2.42, this has been deprecated in favour of * g_notification_set_priority(). */ void g_notification_set_urgent (GNotification *notification, gboolean urgent) { g_return_if_fail (G_IS_NOTIFICATION (notification)); notification->priority = urgent ? G_NOTIFICATION_PRIORITY_URGENT : G_NOTIFICATION_PRIORITY_NORMAL; } /*< private > * g_notification_get_category: * @notification: a #GNotification * * Gets the category of @notification. * * This will be %NULL if no category is set. * * Returns: (nullable): the category of @notification * * Since: 2.70 */ const gchar * g_notification_get_category (GNotification *notification) { g_return_val_if_fail (G_IS_NOTIFICATION (notification), NULL); return notification->category; } /** * g_notification_set_category: * @notification: a #GNotification * @category: (nullable): the category for @notification, or %NULL for no category * * Sets the type of @notification to @category. Categories have a main * type like `email`, `im` or `device` and can have a detail separated * by a `.`, e.g. `im.received` or `email.arrived`. Setting the category * helps the notification server to select proper feedback to the user. * * Standard categories are [listed in the specification](https://specifications.freedesktop.org/notification-spec/latest/ar01s06.html). * * Since: 2.70 */ void g_notification_set_category (GNotification *notification, const gchar *category) { g_return_if_fail (G_IS_NOTIFICATION (notification)); g_return_if_fail (category == NULL || *category != '\0'); g_free (notification->category); notification->category = g_strdup (category); } /** * g_notification_set_priority: * @notification: a #GNotification * @priority: a #GNotificationPriority * * Sets the priority of @notification to @priority. See * #GNotificationPriority for possible values. */ void g_notification_set_priority (GNotification *notification, GNotificationPriority priority) { g_return_if_fail (G_IS_NOTIFICATION (notification)); notification->priority = priority; } /** * g_notification_add_button: * @notification: a #GNotification * @label: label of the button * @detailed_action: a detailed action name * * Adds a button to @notification that activates the action in * @detailed_action when clicked. That action must be an * application-wide action (starting with "app."). If @detailed_action * contains a target, the action will be activated with that target as * its parameter. * * See g_action_parse_detailed_name() for a description of the format * for @detailed_action. * * Since: 2.40 */ void g_notification_add_button (GNotification *notification, const gchar *label, const gchar *detailed_action) { gchar *action; GVariant *target; GError *error = NULL; g_return_if_fail (detailed_action != NULL); if (!g_action_parse_detailed_name (detailed_action, &action, &target, &error)) { g_warning ("%s: %s", G_STRFUNC, error->message); g_error_free (error); return; } g_notification_add_button_with_target_value (notification, label, action, target); g_free (action); if (target) g_variant_unref (target); } /** * g_notification_add_button_with_target: (skip) * @notification: a #GNotification * @label: label of the button * @action: an action name * @target_format: (nullable): a #GVariant format string, or %NULL * @...: positional parameters, as determined by @target_format * * Adds a button to @notification that activates @action when clicked. * @action must be an application-wide action (it must start with "app."). * * If @target_format is given, it is used to collect remaining * positional parameters into a #GVariant instance, similar to * g_variant_new(). @action will be activated with that #GVariant as its * parameter. * * Since: 2.40 */ void g_notification_add_button_with_target (GNotification *notification, const gchar *label, const gchar *action, const gchar *target_format, ...) { va_list args; GVariant *target = NULL; if (target_format) { va_start (args, target_format); target = g_variant_new_va (target_format, NULL, &args); va_end (args); } g_notification_add_button_with_target_value (notification, label, action, target); } /** * g_notification_add_button_with_target_value: (rename-to g_notification_add_button_with_target) * @notification: a #GNotification * @label: label of the button * @action: an action name * @target: (nullable): a #GVariant to use as @action's parameter, or %NULL * * Adds a button to @notification that activates @action when clicked. * @action must be an application-wide action (it must start with "app."). * * If @target is non-%NULL, @action will be activated with @target as * its parameter. * * Since: 2.40 */ void g_notification_add_button_with_target_value (GNotification *notification, const gchar *label, const gchar *action, GVariant *target) { Button *button; g_return_if_fail (G_IS_NOTIFICATION (notification)); g_return_if_fail (label != NULL); g_return_if_fail (action != NULL && g_action_name_is_valid (action)); if (!g_str_has_prefix (action, "app.")) { g_warning ("%s: action '%s' does not start with 'app.'." "This is unlikely to work properly.", G_STRFUNC, action); } button = g_slice_new0 (Button); button->label = g_strdup (label); button->action_name = g_strdup (action); if (target) button->target = g_variant_ref_sink (target); g_ptr_array_add (notification->buttons, button); } /*< private > * g_notification_get_n_buttons: * @notification: a #GNotification * * Returns: the amount of buttons added to @notification. */ guint g_notification_get_n_buttons (GNotification *notification) { return notification->buttons->len; } /*< private > * g_notification_get_button: * @notification: a #GNotification * @index: index of the button * @label: (): return location for the button's label * @action: (): return location for the button's associated action * @target: (): return location for the target @action should be * activated with * * Returns a description of a button that was added to @notification * with g_notification_add_button(). * * @index must be smaller than the value returned by * g_notification_get_n_buttons(). */ void g_notification_get_button (GNotification *notification, gint index, gchar **label, gchar **action, GVariant **target) { Button *button; button = g_ptr_array_index (notification->buttons, index); if (label) *label = g_strdup (button->label); if (action) *action = g_strdup (button->action_name); if (target) *target = button->target ? g_variant_ref (button->target) : NULL; } /*< private > * g_notification_get_button_with_action: * @notification: a #GNotification * @action: an action name * * Returns the index of the button in @notification that is associated * with @action, or -1 if no such button exists. */ gint g_notification_get_button_with_action (GNotification *notification, const gchar *action) { guint i; for (i = 0; i < notification->buttons->len; i++) { Button *button; button = g_ptr_array_index (notification->buttons, i); if (g_str_equal (action, button->action_name)) return i; } return -1; } /*< private > * g_notification_get_default_action: * @notification: a #GNotification * @action: (out) (optional) (nullable) (transfer full): return location for the * default action, or %NULL if unset * @target: (out) (optional) (nullable) (transfer full): return location for the * target of the default action, or %NULL if unset * * Gets the action and target for the default action of @notification. * * If this function returns %TRUE, @action is guaranteed to be set to a non-%NULL * value (if a pointer is passed to @action). @target may still return a %NULL * value, as the default action may have no target. * * Returns: %TRUE if @notification has a default action */ gboolean g_notification_get_default_action (GNotification *notification, gchar **action, GVariant **target) { if (notification->default_action == NULL) return FALSE; if (action) *action = g_strdup (notification->default_action); if (target) { if (notification->default_action_target) *target = g_variant_ref (notification->default_action_target); else *target = NULL; } return TRUE; } /** * g_notification_set_default_action: * @notification: a #GNotification * @detailed_action: a detailed action name * * Sets the default action of @notification to @detailed_action. This * action is activated when the notification is clicked on. * * The action in @detailed_action must be an application-wide action (it * must start with "app."). If @detailed_action contains a target, the * given action will be activated with that target as its parameter. * See g_action_parse_detailed_name() for a description of the format * for @detailed_action. * * When no default action is set, the application that the notification * was sent on is activated. * * Since: 2.40 */ void g_notification_set_default_action (GNotification *notification, const gchar *detailed_action) { gchar *action; GVariant *target; GError *error = NULL; if (!g_action_parse_detailed_name (detailed_action, &action, &target, &error)) { g_warning ("%s: %s", G_STRFUNC, error->message); g_error_free (error); return; } g_notification_set_default_action_and_target_value (notification, action, target); g_free (action); if (target) g_variant_unref (target); } /** * g_notification_set_default_action_and_target: (skip) * @notification: a #GNotification * @action: an action name * @target_format: (nullable): a #GVariant format string, or %NULL * @...: positional parameters, as determined by @target_format * * Sets the default action of @notification to @action. This action is * activated when the notification is clicked on. It must be an * application-wide action (it must start with "app."). * * If @target_format is given, it is used to collect remaining * positional parameters into a #GVariant instance, similar to * g_variant_new(). @action will be activated with that #GVariant as its * parameter. * * When no default action is set, the application that the notification * was sent on is activated. * * Since: 2.40 */ void g_notification_set_default_action_and_target (GNotification *notification, const gchar *action, const gchar *target_format, ...) { va_list args; GVariant *target = NULL; if (target_format) { va_start (args, target_format); target = g_variant_new_va (target_format, NULL, &args); va_end (args); } g_notification_set_default_action_and_target_value (notification, action, target); } /** * g_notification_set_default_action_and_target_value: (rename-to g_notification_set_default_action_and_target) * @notification: a #GNotification * @action: an action name * @target: (nullable): a #GVariant to use as @action's parameter, or %NULL * * Sets the default action of @notification to @action. This action is * activated when the notification is clicked on. It must be an * application-wide action (start with "app."). * * If @target is non-%NULL, @action will be activated with @target as * its parameter. If @target is floating, it will be consumed. * * When no default action is set, the application that the notification * was sent on is activated. * * Since: 2.40 */ void g_notification_set_default_action_and_target_value (GNotification *notification, const gchar *action, GVariant *target) { g_return_if_fail (G_IS_NOTIFICATION (notification)); g_return_if_fail (action != NULL && g_action_name_is_valid (action)); if (!g_str_has_prefix (action, "app.")) { g_warning ("%s: action '%s' does not start with 'app.'." "This is unlikely to work properly.", G_STRFUNC, action); } g_free (notification->default_action); g_clear_pointer (¬ification->default_action_target, g_variant_unref); notification->default_action = g_strdup (action); if (target) notification->default_action_target = g_variant_ref_sink (target); } static GVariant * g_notification_serialize_button (Button *button) { GVariantBuilder builder; g_variant_builder_init_static (&builder, G_VARIANT_TYPE ("a{sv}")); g_variant_builder_add (&builder, "{sv}", "label", g_variant_new_string (button->label)); g_variant_builder_add (&builder, "{sv}", "action", g_variant_new_string (button->action_name)); if (button->target) g_variant_builder_add (&builder, "{sv}", "target", button->target); return g_variant_builder_end (&builder); } static GVariant * g_notification_get_priority_nick (GNotification *notification) { GEnumClass *enum_class; GEnumValue *value; GVariant *nick; enum_class = g_type_class_ref (G_TYPE_NOTIFICATION_PRIORITY); value = g_enum_get_value (enum_class, g_notification_get_priority (notification)); g_assert (value != NULL); nick = g_variant_new_string (value->value_nick); g_type_class_unref (enum_class); return nick; } /*< private > * g_notification_serialize: * * Serializes @notification into a floating variant of type a{sv}. * * Returns: the serialized @notification as a floating variant. */ GVariant * g_notification_serialize (GNotification *notification) { GVariantBuilder builder; g_variant_builder_init_static (&builder, G_VARIANT_TYPE ("a{sv}")); if (notification->title) g_variant_builder_add (&builder, "{sv}", "title", g_variant_new_string (notification->title)); if (notification->body) g_variant_builder_add (&builder, "{sv}", "body", g_variant_new_string (notification->body)); if (notification->icon) { GVariant *serialized_icon; if ((serialized_icon = g_icon_serialize (notification->icon))) { g_variant_builder_add (&builder, "{sv}", "icon", serialized_icon); g_variant_unref (serialized_icon); } } g_variant_builder_add (&builder, "{sv}", "priority", g_notification_get_priority_nick (notification)); if (notification->default_action) { g_variant_builder_add (&builder, "{sv}", "default-action", g_variant_new_string (notification->default_action)); if (notification->default_action_target) g_variant_builder_add (&builder, "{sv}", "default-action-target", notification->default_action_target); } if (notification->buttons->len > 0) { GVariantBuilder actions_builder; guint i; g_variant_builder_init_static (&actions_builder, G_VARIANT_TYPE ("aa{sv}")); for (i = 0; i < notification->buttons->len; i++) { Button *button = g_ptr_array_index (notification->buttons, i); g_variant_builder_add (&actions_builder, "@a{sv}", g_notification_serialize_button (button)); } g_variant_builder_add (&builder, "{sv}", "buttons", g_variant_builder_end (&actions_builder)); } return g_variant_builder_end (&builder); }